diff --git a/.gitignore b/.gitignore index 35cb3e67c9..815a359c11 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ *.min.js.map *.min.css *.min.css.map +*.js.map *.sql *.diff *.patch @@ -43,6 +44,8 @@ /sambaadmin/ /sitemgr/ /stylite/ +/swool/ +/test/ /tracker/ /usage/ /vendor/ diff --git a/admin/js/app.js b/admin/js/app.js index 966e2d7a7b..babd615654 100644 --- a/admin/js/app.js +++ b/admin/js/app.js @@ -1223,3 +1223,4 @@ var AdminApp = /** @class */ (function (_super) { */ )); app.classes.admin = AdminApp; +//# sourceMappingURL=app.js.map \ No newline at end of file diff --git a/admin/js/app.ts b/admin/js/app.ts index f9e23f0a29..b567d2dc90 100644 --- a/admin/js/app.ts +++ b/admin/js/app.ts @@ -17,7 +17,7 @@ import 'jqueryui'; import '../jsapi/egw_global'; import '../etemplate/et2_types'; -import {EgwApp} from '../../api/js/jsapi/egw_app'; +import { EgwApp } from '../../api/js/jsapi/egw_app'; /** * UI for Admin diff --git a/api/js/egw_action/egw_action.d.ts b/api/js/egw_action/egw_action.d.ts index 9a289bcd03..1d1a25ec11 100644 --- a/api/js/egw_action/egw_action.d.ts +++ b/api/js/egw_action/egw_action.d.ts @@ -145,7 +145,7 @@ declare class egwAction { previous: string; close: string; }; - updateActions(_actions: any, _app: string): void; + updateActions(_actions: any, _app?: string): void; not_disableClass(_action: any, _senders: any, _target: any): boolean; enableClass(_action: any, _senders: any, _target: any): boolean; enableId(_action: any, _senders: any, _target: any): any; @@ -250,7 +250,7 @@ declare class egwActionLink { * @param {number} _flags a set of additional flags being applied to the object, * defaults to 0 */ -declare function egwActionObject(_id: string, _parent: egwActionObject, _iface: egwActionObjectInterface, _manager: typeof egwActionManager, _flags: number): void; +declare function egwActionObject(_id: string, _parent: egwActionObject, _iface?: egwActionObjectInterface, _manager?: typeof egwActionManager, _flags?: number): void; declare class egwActionObject { /** * The egwActionObject represents an abstract object to which actions may be @@ -268,7 +268,7 @@ declare class egwActionObject { * @param {number} _flags a set of additional flags being applied to the object, * defaults to 0 */ - constructor(_id: string, _parent: egwActionObject, _iface: egwActionObjectInterface, _manager: typeof egwActionManager, _flags: number); + constructor(_id: string, _parent: egwActionObject, _iface?: egwActionObjectInterface, _manager?: typeof egwActionManager, _flags?: number); id: string; parent: egwActionObject; children: any[]; @@ -284,7 +284,7 @@ declare class egwActionObject { iface: egwActionObjectInterface; getObjectById(_id: string, _search_depth?: number): egwActionObject; addObject(_id: any, _interface: any, _flags?: number): any; - insertObject(_index: number, _id: any, _iface: any, _flags: number): any; + insertObject(_index: number | boolean, _id: any, _iface?: any, _flags?: number): any; clear(): void; remove(): void; getRootObject(): any; @@ -478,7 +478,7 @@ declare class egwEventQueue { * @param array _acceptedTypes is an array of types which contains the "typeof" * strings of accepted non-functions in setValue */ -declare function egwFnct(_context: any, _default: any, _acceptedTypes: any): void; +declare function egwFnct(_context: any, _default: any, _acceptedTypes?: any): void; declare class egwFnct { /** * Class which is used to be able to handle references to JavaScript functions @@ -491,7 +491,7 @@ declare class egwFnct { * @param array _acceptedTypes is an array of types which contains the "typeof" * strings of accepted non-functions in setValue */ - constructor(_context: any, _default: any, _acceptedTypes: any); + constructor(_context: any, _default: any, _acceptedTypes?: any); context: any; acceptedTypes: any; fnct: any; diff --git a/api/js/etemplate/et2_core_DOMWidget.js b/api/js/etemplate/et2_core_DOMWidget.js index c4720bbbd6..140b42907b 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,32 @@ * @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"); +require("./et2_types"); /** * 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 +40,669 @@ * * @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) { + __extends(et2_DOMWidget, _super); + /** + * 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.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_DOMWidget._attributes, _child || {})) || this; + _this.parentNode = null; + _this.disabled = false; + _this._attachSet = { + "node": null, + "parent": null + }; + _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.destroy(); + this._surroundingsMgr = null; + } + _super.prototype.destroy.call(this); + }; + /** + * 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 && 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 && + (!this._attachSet || this._attachSet && 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.getParent(); + } while (parent !== this.getRoot() && parent.getType() !== 'tabbox'); + // No tab + if (parent === this.getRoot()) { + return null; + } + var tabbox = parent; + // Find the tab index + for (var i = 0; i < tabbox.tabData.length; i++) { + // Find the tab by DOM heritage + // @ts-ignore + if (tabbox.tabData[i].contentDiv.has(this.div).length) { + return tabbox.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.getParent(); + // @ts-ignore + } while (template !== tabbox && template.getType() !== 'template'); + for (var i = tabbox.tabData.length - 1; i >= 0; i--) { + if (template && template.id && template.id === tabbox.tabData[i].id) { + return tabbox.tabData[i]; + } + } + // Fallback + var fallback = this.getParent(); + if (typeof fallback.get_tab_info === 'function') { + return fallback.get_tab_info(); + } + return null; + }; + /** + * 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 = window.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)).getAOI(), this._actionManager || objectManager.manager.getActionById(this.id) || objectManager.manager)); + } + else { + widget_object.setAOI((new et2_action_object_impl(this, this.getDOMNode())).getAOI()); + } + // 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)); +exports.et2_DOMWidget = et2_DOMWidget; /** * 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) { + __extends(et2_surroundingsMgr, _super); + /** + * Constructor + * + * @memberOf et2_surroundingsMgr + * @param _widget + */ + function et2_surroundingsMgr(_widget) { + var _this = _super.call(this) || this; + _this._widgetContainer = null; + _this._widgetSurroundings = []; + _this._widgetPlaceholder = null; + _this._widgetNode = null; + _this._ownPlaceholder = true; + _this._surroundingsUpdated = false; + _this.widget = _widget; + return _this; + } + 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; + }; + et2_surroundingsMgr.prototype.getWidgetSurroundings = function () { + return this._widgetSurroundings; + }; + 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 +713,40 @@ 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; -}; +var et2_action_object_impl = /** @class */ (function () { + function et2_action_object_impl(_widget, _node) { + var widget = _widget; + var objectNode = _node; + this.aoi = new egwActionObjectInterface(); + this.aoi.getWidget = function () { + return widget; + }; + this.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. + this.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 + this.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; + } + }; + } + et2_action_object_impl.prototype.getAOI = function () { + return this.aoi; + }; + return et2_action_object_impl; +}()); +exports.et2_action_object_impl = et2_action_object_impl; +//# sourceMappingURL=et2_core_DOMWidget.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_core_DOMWidget.ts b/api/js/etemplate/et2_core_DOMWidget.ts new file mode 100644 index 0000000000..d4f7695a5c --- /dev/null +++ b/api/js/etemplate/et2_core_DOMWidget.ts @@ -0,0 +1,930 @@ +/** + * 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, WidgetConfig} from "./et2_core_widget"; +import '../egw_action/egw_action.js'; +import './et2_types'; + +/** + * 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 + */ +export abstract 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 = null; + disabled : boolean = false; + protected _attachSet : any = { + "node": null, + "parent": null + }; + protected _actionManager: any; + width: number; + height: number; + dom_id: string; + overflow: string; + + /** + * 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._surroundingsMgr = null; + } + + abstract getDOMNode(_sender?: et2_widget): HTMLElement + + /** + * 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.destroy(); + this._surroundingsMgr = null; + } + + super.destroy(); + } + + /** + * Attaches the container node of this widget to the DOM-Tree + */ + doLoadingFinished() : boolean | JQueryPromise + { + // 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 && 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 && + (!this._attachSet || this._attachSet && 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() : et2_surroundingsMgr + { + 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() : {id: string, label: string, widget: et2_widget, contentDiv: JQuery, flagDiv: JQuery} | null + { + var parent : et2_widget = this; + do { + parent = parent.getParent(); + } while (parent !== this.getRoot() && parent.getType() !== 'tabbox'); + + // No tab + if(parent === this.getRoot()) + { + return null; + } + + let tabbox : et2_tabbox = parent; + + // Find the tab index + for(var i = 0; i < tabbox.tabData.length; i++) + { + // Find the tab by DOM heritage + // @ts-ignore + if(tabbox.tabData[i].contentDiv.has(this.div).length) + { + return tabbox.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 : et2_widget = this; + do { + template = template.getParent(); + // @ts-ignore + } while (template !== tabbox && template.getType() !== 'template'); + for (var i = tabbox.tabData.length - 1; i >= 0; i--) + { + if (template && template.id && template.id === tabbox.tabData[i].id) + { + return tabbox.tabData[i]; + } + } + // Fallback + let fallback = this.getParent(); + if (typeof fallback.get_tab_info === 'function') + { + return fallback.get_tab_info(); + } + return null; + } + + /** + * 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 : HTMLElement) + { + 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() : HTMLElement + { + return this.parentNode; + } + + /** + * Returns the index of this element in the DOM tree + */ + getDOMIndex() : number + { + 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 = window.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)).getAOI(), + this._actionManager || objectManager.manager.getActionById(this.id) || objectManager.manager + )); + } + else + { + widget_object.setAOI((new et2_action_object_impl(this, this.getDOMNode())).getAOI()); + } + + // 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. + */ +class et2_surroundingsMgr extends ClassWithAttributes +{ + widget: et2_DOMWidget; + private _widgetContainer: any = null; + private _widgetSurroundings: any[] = []; + private _widgetPlaceholder: any = null; + private _widgetNode: HTMLElement = null; + private _ownPlaceholder: boolean = true; + private _surroundingsUpdated: boolean = false; + + /** + * Constructor + * + * @memberOf et2_surroundingsMgr + * @param _widget + */ + constructor(_widget : et2_DOMWidget) + { + super(); + this.widget = _widget; + } + + 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; + } + } + + private _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; + } + + getWidgetSurroundings() : HTMLElement [] + { + return this._widgetSurroundings; + } +} + +/** + * 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 + * + */ +export class et2_action_object_impl +{ + aoi : egwActionObjectInterface; + + constructor(_widget : et2_DOMWidget, _node? : HTMLElement) + { + var widget = _widget; + var objectNode = _node; + this.aoi = new egwActionObjectInterface(); + this.aoi.getWidget = function () { + return widget; + }; + this.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. + this.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 + this.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; + } + }; + } + + getAOI() + { + return this.aoi; + } +} diff --git a/api/js/etemplate/et2_core_arrayMgr.js b/api/js/etemplate/et2_core_arrayMgr.js index bd030a4a7c..8a5d12f269 100644 --- a/api/js/etemplate/et2_core_arrayMgr.js +++ b/api/js/etemplate/et2_core_arrayMgr.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - JS content array manager * @@ -6,488 +7,415 @@ * @subpackage api * @link http://www.egroupware.org * @author Andreas Stöckel - * @copyright Stylite 2011 - * @version $Id$ - */ + * /*egw:uses - et2_core_common; - egw_inheritance; - et2_core_phpExpressionCompiler; + et2_core_common; + egw_inheritance; + et2_core_phpExpressionCompiler; */ - +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 }); /** - * @augments Class + * Manage access to various template customisation arrays passed to etemplate->exec(). + * + * This manages access to content, modifications and readonlys arrays */ -var et2_arrayMgr = (function(){ "use strict"; return Class.extend( -{ - splitIds: true, - - /** - * Constructor - * - * @memberOf et2_arrayMgr - * @param _data - * @param _parentMgr - */ - init: function(_data, _parentMgr) { - if (typeof _parentMgr == "undefined") - { - _parentMgr = null; - } - - // Copy the parent manager which is needed to access relative data when - // being in a relative perspective of the manager - this.parentMgr = _parentMgr; - - // Hold a reference to the data - if (typeof _data == "undefined" || !_data) - { - egw.debug("log", "No data passed to content array manager. Probably a mismatch between template namespaces and data."); - _data = {}; - } - - // Expand sub-arrays that have been shmushed together, so further perspectives work - // Shmushed keys look like: ${row}[info_cat] - // Expanded: ${row}: Object{info_cat: ..value} - if (this.splitIds) - { - // For each index, we need a key: {..} sub array - for(var key in _data) { - // Split up indexes - var indexes = key.replace(/[/g,"[").split('['); - - // Put data in the proper place - if(indexes.length > 1) - { - var value = _data[key]; - var target = _data; - for(var i = 0; i < indexes.length; i++) { - indexes[i] = indexes[i].replace(/]/g,'').replace(']',''); - if(typeof target[indexes[i]] == "undefined" || target[indexes[i]] === null) { - target[indexes[i]] = i == indexes.length-1 ? value : {}; - } - target = target[indexes[i]]; - } - delete _data[key]; - } - } - } - - this.data = _data; - - // Holds information about the current perspective - this.perspectiveData = { - "owner": null, - "key": null, - "row": null - }; - }, - - /** - * Returns the root content array manager object - */ - getRoot : function() { - if (this.parentMgr != null) - { - return this.parentMgr.getRoot(); - } - - return this; - }, - -/* getValueForID : function(_id) { - if (typeof this.data[_id] != "undefined") - { - return this.data[_id]; - } - - return null; - },*/ - - /** - * Explodes compound keys (eg IDs) into a list of namespaces - * This uses no internal values, just expands - * - * eg: - * a[b][c] => [a,b,c] - * col_filter[tr_tracker] => [col_filter, tr_tracker] - * - * @param {string} _key - * - * @return {string[]} - */ - explodeKey: function(_key) - { - // Parse the given key by removing the "]"-chars and splitting at "[" - var indexes = [_key]; - - if(typeof _key === "string") - { - _key = _key.replace(/[/g,"[").replace(/]/g,"]"); - indexes = _key.split('['); - } - if (indexes.length > 1) - { - indexes = [indexes.shift(), indexes.join('[')]; - indexes[1] = indexes[1].substring(0,indexes[1].length-1); - var children = indexes[1].split(']['); - if(children.length) - { - indexes = jQuery.merge([indexes[0]], children); - } - } - return indexes; - }, - - /** - * Returns the path to this content array manager perspective as an array - * containing the key values - * - * @param _path is used internally, do not supply it manually. - */ - getPath : function(_path) { - if (typeof _path == "undefined") - { - _path = []; - } - - if (this.perspectiveData.key != null) - { - // prepend components of this.perspectiveData.key to path, can be more then one eg. "nm[rows]" - _path = this.perspectiveData.key.replace(/]/g, '').split('[').concat(_path); - } - - if (this.parentMgr != null) - { - _path = this.parentMgr.getPath(_path); - } - - return _path; - }, - - /** - * Get array entry is the equivalent to the boetemplate get_array function. - * It returns a reference to the (sub) array with the given key. This also works - * for keys using the ETemplate referencing scheme like a[b][c] - * - * @param _key is the string index, may contain sub-indices like a[b] - * @param _referenceInto if true none-existing sub-arrays/-indices get created - * to be returned as reference, else false is returned. Defaults to false - * @param _skipEmpty returns null if _key is not present in this content array. - * Defaults to false. - */ - getEntry : function(_key, _referenceInto, _skipEmpty) { - if (typeof _referenceInto == "undefined") - { - _referenceInto = false; - } - - if (typeof _skipEmpty == "undefined") - { - _skipEmpty = false; - } - - // Parse the given key by removing the "]"-chars and splitting at "[" - var indexes = this.explodeKey(_key); - - var entry = this.data; - for (var i = 0; i < indexes.length; i++) - { - // Abort if the current entry is not an object (associative array) and - // we should descend further into it. - var isObject = typeof entry === 'object'; - if (!isObject && !_referenceInto || entry == null || jQuery.isEmptyObject(entry)) - { - return null; - } - - // Check whether the entry actually exists - var idx = indexes[i]; - if (_skipEmpty && (!isObject || typeof entry[idx] == "undefined")) - { - return null; - } - - entry = entry[idx]; - } - - return entry; - }, - - compiledExpressions: {}, - - /** - * Equivaltent to the boetemplate::expand_name function. - * - * Expands variables inside the given identifier to their values inside the - * content array. - * - * @param {string} _ident Key used to reference into managed array - * @return {*} - */ - expandName : function(_ident) { - // Check whether the identifier refers to an index in the content array - var is_index_in_content = _ident.charAt(0) == '@'; - - // Check whether "$" occurs in the given identifier - var pos_var = _ident.indexOf('$'); - if (pos_var >= 0 && (this.perspectiveData.row != null || !_ident.match(/\$\{?row\}?/))) - { - // Get the content array for the current row - var row = this.perspectiveData.row; - var row_cont = this.data[row] || {}; - // $cont is NOT root but current name-space in old eTemplate - var cont = this.data;//getRoot().data; - var _cont = this.data;// according to a grep only used in ImportExport just twice - - // Check whether the expression has already been compiled - if not, - // try to compile it first. If an error occurs, the identifier - // function is set to null - var proto = this.constructor.prototype; - if (typeof proto.compiledExpressions[_ident] == "undefined") - { - try - { - if(this.perspectiveData.row == null) - { - // No row, compile for only top level content - proto.compiledExpressions[_ident] = et2_compilePHPExpression( - _ident, ["cont","_cont"]); - } - else - { - proto.compiledExpressions[_ident] = et2_compilePHPExpression( - _ident, ["row", "cont", "row_cont", "_cont"]); - } - } - catch(e) - { - proto.compiledExpressions[_ident] = null; - egw.debug("error", "Error while compiling PHP->JS ", e); - } - } - - // Execute the previously compiled expression, if it is not "null" - // because compilation failed. The parameters have to be in the same - // order as defined during compilation. - if (proto.compiledExpressions[_ident]) - { - try - { - if(this.perspectiveData.row == null) - { - // No row, exec with only top level content - _ident = proto.compiledExpressions[_ident](cont,_cont); - } - else - { - _ident = proto.compiledExpressions[_ident](row, cont, row_cont,_cont); - } - } - catch(e) - { - // only log error, as they are no real errors but missing data - egw.debug("log", typeof e == 'object' ? e.message : e); - _ident = null; - } - } - } - - if (is_index_in_content) - { - // If an additional "@" is specified, this means that we have to return - // the entry from the root element - if (_ident.charAt(1) == '@') - { - _ident = this.getRoot().getEntry(_ident.substr(2)); - } - else - { - _ident = this.getEntry(_ident.substr(1)); - } - } - - return _ident; - }, - - parseBoolExpression: function(_expression) { - // If the first char of the expression is a '!' this means, that the value - // is to be negated. - if (_expression.charAt(0) == '!') - { - return !this.parseBoolExpression(_expression.substr(1)); - } - - // Split the expression at a possible "=" - var parts = _expression.split('='); - - // Expand the first value - var val = this.expandName(parts[0]); - - // If a second expression existed, test that one - if (typeof parts[1] != "undefined") - { - // Expand the second value - var checkVal = this.expandName(parts[1]); - - // Values starting with / are treated as regular expression. It is - // checked whether the first value matches the regular expression - if (checkVal.charAt(0) == '/') - { - return (new RegExp(checkVal.substr(1, checkVal.length - 2))) - .test(val) ? true : false; - } - - // Otherwise check for simple equality - return val == checkVal; - } - - return et2_evalBool(val); - }, - - /** - * ? - * - * @param {object} _owner owner object - * @param {(string|null|object)} _root string with key, null for whole data or object with data - * @param {number?} _row key for into the _root for the desired row - */ - openPerspective: function(_owner, _root, _row) - { - // Get the root node - var root = typeof _root == "string" ? this.data[_root] : - (_root == null ? this.data : _root); - if(typeof root == "undefined" && typeof _root == "string") root = this.getEntry(_root); - - // Create a new content array manager with the given root - var constructor = this.isReadOnly ? et2_readonlysArrayMgr : et2_arrayMgr; - var mgr = new constructor(root, this); - - // Set the owner - mgr.perspectiveData.owner = _owner; - - // Set the root key - if (typeof _root == "string") - { - mgr.perspectiveData.key = _root; - } - - // Set _row parameter - if (typeof _row != "undefined") - { - mgr.perspectiveData.row = _row; - } - - return mgr; - } - -});}).call(this); - +var et2_arrayMgr = /** @class */ (function () { + /** + * Constructor + * + * @memberOf et2_arrayMgr + * @param _data + * @param _parentMgr + */ + function et2_arrayMgr(_data, _parentMgr) { + if (_data === void 0) { _data = {}; } + this.splitIds = true; + // Holds information about the current perspective + this.perspectiveData = { + "owner": null, + "key": null, + "row": null + }; + this.readOnly = false; + if (typeof _parentMgr == "undefined") { + _parentMgr = null; + } + // Copy the parent manager which is needed to access relative data when + // being in a relative perspective of the manager + this._parentMgr = _parentMgr; + // Hold a reference to the data + if (typeof _data == "undefined" || !_data) { + egw.debug("log", "No data passed to content array manager. Probably a mismatch between template namespaces and data."); + _data = {}; + } + // Expand sub-arrays that have been shmushed together, so further perspectives work + // Shmushed keys look like: ${row}[info_cat] + // Expanded: ${row}: Object{info_cat: ..value} + if (this.splitIds) { + // For each index, we need a key: {..} sub array + for (var key in _data) { + // Split up indexes + var indexes = key.replace(/[/g, "[").split('['); + // Put data in the proper place + if (indexes.length > 1) { + var value = _data[key]; + var target = _data; + for (var i = 0; i < indexes.length; i++) { + indexes[i] = indexes[i].replace(/]/g, '').replace(']', ''); + if (typeof target[indexes[i]] == "undefined" || target[indexes[i]] === null) { + target[indexes[i]] = i == indexes.length - 1 ? value : {}; + } + target = target[indexes[i]]; + } + delete _data[key]; + } + } + } + this.data = _data; + } + /** + * Returns the root content array manager object + */ + et2_arrayMgr.prototype.getRoot = function () { + if (this._parentMgr != null) { + return this._parentMgr.getRoot(); + } + return this; + }; + et2_arrayMgr.prototype.getParentMgr = function () { + return this._parentMgr; + }; + et2_arrayMgr.prototype.getPerspectiveData = function () { + return this.perspectiveData; + }; + et2_arrayMgr.prototype.setPerspectiveData = function (new_perspective) { + this.perspectiveData = new_perspective; + }; + et2_arrayMgr.prototype.setRow = function (new_row) { + this.perspectiveData.row = new_row; + }; + /** + * Explodes compound keys (eg IDs) into a list of namespaces + * This uses no internal values, just expands + * + * eg: + * a[b][c] => [a,b,c] + * col_filter[tr_tracker] => [col_filter, tr_tracker] + * + * @param {string} _key + * + * @return {string[]} + */ + et2_arrayMgr.prototype.explodeKey = function (_key) { + // Parse the given key by removing the "]"-chars and splitting at "[" + var indexes = [_key]; + if (typeof _key === "string") { + _key = _key.replace(/[/g, "[").replace(/]/g, "]"); + indexes = _key.split('['); + } + if (indexes.length > 1) { + indexes = [indexes.shift(), indexes.join('[')]; + indexes[1] = indexes[1].substring(0, indexes[1].length - 1); + var children = indexes[1].split(']['); + if (children.length) { + indexes = jQuery.merge([indexes[0]], children); + } + } + return indexes; + }; + /** + * Returns the path to this content array manager perspective as an array + * containing the key values + * + * @param _path is used internally, do not supply it manually. + */ + et2_arrayMgr.prototype.getPath = function (_path) { + if (typeof _path == "undefined") { + _path = []; + } + if (this.perspectiveData.key != null) { + // prepend components of this.perspectiveData.key to path, can be more then one eg. "nm[rows]" + _path = this.perspectiveData.key.replace(/]/g, '').split('[').concat(_path); + } + if (this._parentMgr != null) { + _path = this._parentMgr.getPath(_path); + } + return _path; + }; + /** + * Get array entry is the equivalent to the boetemplate get_array function. + * It returns a reference to the (sub) array with the given key. This also works + * for keys using the ETemplate referencing scheme like a[b][c] + * + * @param _key is the string index, may contain sub-indices like a[b] + * @param _referenceInto if true none-existing sub-arrays/-indices get created + * to be returned as reference, else false is returned. Defaults to false + * @param _skipEmpty returns null if _key is not present in this content array. + * Defaults to false. + */ + et2_arrayMgr.prototype.getEntry = function (_key, _referenceInto, _skipEmpty) { + if (typeof _referenceInto == "undefined") { + _referenceInto = false; + } + if (typeof _skipEmpty == "undefined") { + _skipEmpty = false; + } + // Parse the given key by removing the "]"-chars and splitting at "[" + var indexes = this.explodeKey(_key); + var entry = this.data; + for (var i = 0; i < indexes.length; i++) { + // Abort if the current entry is not an object (associative array) and + // we should descend further into it. + var isObject = typeof entry === 'object'; + if (!isObject && !_referenceInto || entry == null || jQuery.isEmptyObject(entry)) { + return null; + } + // Check whether the entry actually exists + var idx = indexes[i]; + if (_skipEmpty && (!isObject || typeof entry[idx] == "undefined")) { + return null; + } + entry = entry[idx]; + } + return entry; + }; + /** + * Equivalent to the boetemplate::expand_name function. + * + * Expands variables inside the given identifier to their values inside the + * content array. + * + * @param {string} _ident Key used to reference into managed array + * @return {*} + */ + et2_arrayMgr.prototype.expandName = function (_ident) { + // Check whether the identifier refers to an index in the content array + var is_index_in_content = _ident.charAt(0) == '@'; + // Check whether "$" occurs in the given identifier + var pos_var = _ident.indexOf('$'); + if (pos_var >= 0 && (this.perspectiveData.row != null || !_ident.match(/\$\{?row\}?/))) { + // Get the content array for the current row + var row = typeof this.perspectiveData.row == 'number' ? this.perspectiveData.row : ''; + var row_cont = this.data[row] || {}; + // $cont is NOT root but current name-space in old eTemplate + var cont = this.data; //getRoot().data; + var _cont = this.data; // according to a grep only used in ImportExport just twice + // Check whether the expression has already been compiled - if not, + // try to compile it first. If an error occurs, the identifier + // function is set to null + if (typeof et2_arrayMgr.compiledExpressions[_ident] == "undefined") { + try { + if (this.perspectiveData.row == null) { + // No row, compile for only top level content + // @ts-ignore + et2_arrayMgr.compiledExpressions[_ident] = et2_compilePHPExpression(_ident, ["cont", "_cont"]); + } + else { + // @ts-ignore + et2_arrayMgr.compiledExpressions[_ident] = et2_compilePHPExpression(_ident, ["row", "cont", "row_cont", "_cont"]); + } + } + catch (e) { + et2_arrayMgr.compiledExpressions[_ident] = null; + egw.debug("error", "Error while compiling PHP->JS ", e); + } + } + // Execute the previously compiled expression, if it is not "null" + // because compilation failed. The parameters have to be in the same + // order as defined during compilation. + if (et2_arrayMgr.compiledExpressions[_ident]) { + try { + if (this.perspectiveData.row == null) { + // No row, exec with only top level content + _ident = et2_arrayMgr.compiledExpressions[_ident](cont, _cont); + } + else { + _ident = et2_arrayMgr.compiledExpressions[_ident](row, cont, row_cont, _cont); + } + } + catch (e) { + // only log error, as they are no real errors but missing data + egw.debug("log", typeof e == 'object' ? e.message : e); + _ident = null; + } + } + } + if (is_index_in_content && _ident) { + // If an additional "@" is specified, this means that we have to return + // the entry from the root element + if (_ident.charAt(1) == '@') { + return this.getRoot().getEntry(_ident.substr(2)); + } + else { + return this.getEntry(_ident.substr(1)); + } + } + return _ident; + }; + et2_arrayMgr.prototype.parseBoolExpression = function (_expression) { + // If the first char of the expression is a '!' this means, that the value + // is to be negated. + if (_expression.charAt(0) == '!') { + return !this.parseBoolExpression(_expression.substr(1)); + } + // Split the expression at a possible "=" + var parts = _expression.split('='); + // Expand the first value + var val = this.expandName(parts[0]); + val = typeof val == "undefined" ? '' : '' + val; + // If a second expression existed, test that one + if (typeof parts[1] != "undefined") { + // Expand the second value + var checkVal = '' + this.expandName(parts[1]); + // Values starting with / are treated as regular expression. It is + // checked whether the first value matches the regular expression + if (checkVal.charAt(0) == '/') { + return (new RegExp(checkVal.substr(1, checkVal.length - 2))) + .test(val); + } + // Otherwise check for simple equality + return val == checkVal; + } + return et2_evalBool(val); + }; + /** + * ? + * + * @param {object} _owner owner object + * @param {(string|null|object)} _root string with key, null for whole data or object with data + * @param {number?} _row key for into the _root for the desired row + */ + et2_arrayMgr.prototype.openPerspective = function (_owner, _root, _row) { + // Get the root node + var root = typeof _root == "string" ? this.data[_root] : + (_root == null ? this.data : _root); + if (typeof root == "undefined" && typeof _root == "string") + root = this.getEntry(_root); + // Create a new content array manager with the given root + var constructor = this.readOnly ? et2_readonlysArrayMgr : et2_arrayMgr; + var mgr = new constructor(root, this); + // Set the owner + mgr.perspectiveData.owner = _owner; + // Set the root key + if (typeof _root == "string") { + mgr.perspectiveData.key = _root; + } + // Set _row parameter + if (typeof _row != "undefined") { + mgr.perspectiveData.row = _row; + } + return mgr; + }; + et2_arrayMgr.compiledExpressions = {}; + return et2_arrayMgr; +}()); +exports.et2_arrayMgr = et2_arrayMgr; /** * @augments et2_arrayMgr */ -var et2_readonlysArrayMgr = (function(){ "use strict"; return et2_arrayMgr.extend( -{ - - /** - * @memberOf et2_readonlysArrayMgr - * @param _id - * @param _attr - * @param _parent - * @returns - */ - isReadOnly: function(_id, _attr, _parent) { - var entry = null; - - if (_id != null) - { - if(_id.indexOf('$') >= 0 || _id.indexOf('@') >= 0) - { - _id = this.expandName(_id); - } - // readonlys was not namespaced in old eTemplate, therefore if we dont find data - // under current namespace, we look into parent - // (if there is anything namespaced, we will NOT look for parent!) - var mgr = this; - while (mgr.parentMgr && jQuery.isEmptyObject(mgr.data)) - { - mgr = mgr.parentMgr; - } - entry = mgr.getEntry(_id); - } - - // Let the array entry override the read only attribute entry - if (typeof entry != "undefined" && !(typeof entry === 'object')) - { - return entry; - } - - // If the attribute is set, return that - if (typeof _attr != "undefined" && _attr !== null) - { - // Accept 'editable', but otherwise boolean - return this.expandName(_attr) === 'editable' ? 'editable' : et2_evalBool(_attr); - } - - // Otherwise take into accounf whether the parent is readonly - if (typeof _parent != "undefined" && _parent) - { - return true; - } - - // Otherwise return the default value - entry = this.getEntry("__ALL__"); - return entry !== null && (typeof entry != "undefined"); - }, - - /** - * Override parent to handle cont and row_cont. - * - * Normally these should refer to the readonlys data, but that's not - * useful, so we use the owner inside perspective data to expand using content. - * - * @param {string} ident Key for searching into the array. - * @returns {*} - */ - expandName: function(ident) - { - return this.perspectiveData.owner.getArrayMgr('content').expandName(ident); - } -});}).call(this); - +var et2_readonlysArrayMgr = /** @class */ (function (_super) { + __extends(et2_readonlysArrayMgr, _super); + function et2_readonlysArrayMgr() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.readOnly = true; + return _this; + } + /** + * Find out if the given ID is readonly, according to the array data + * + * @memberOf et2_readonlysArrayMgr + * @param _id + * @param _attr + * @param _parent + * @returns + */ + et2_readonlysArrayMgr.prototype.isReadOnly = function (_id, _attr, _parent) { + var entry = null; + if (_id != null) { + if (_id.indexOf('$') >= 0 || _id.indexOf('@') >= 0) { + _id = this.expandName(_id); + } + // readonlys was not namespaced in old eTemplate, therefore if we dont find data + // under current namespace, we look into parent + // (if there is anything namespaced, we will NOT look for parent!) + var mgr = this; + while (mgr.getParentMgr() && jQuery.isEmptyObject(mgr.data)) { + mgr = mgr.getParentMgr(); + } + entry = mgr.getEntry(_id); + } + // Let the array entry override the read only attribute entry + if (typeof entry != "undefined" && !(typeof entry === 'object')) { + return entry; + } + // If the attribute is set, return that + if (typeof _attr != "undefined" && _attr !== null) { + // Accept 'editable', but otherwise boolean + return this.expandName(_attr) === 'editable' ? 'editable' : et2_evalBool(_attr); + } + // Otherwise take into accounf whether the parent is readonly + if (typeof _parent != "undefined" && _parent) { + return true; + } + // Otherwise return the default value + entry = this.getEntry("__ALL__"); + return entry !== null && (typeof entry != "undefined"); + }; + /** + * Override parent to handle cont and row_cont. + * + * Normally these should refer to the readonlys data, but that's not + * useful, so we use the owner inside perspective data to expand using content. + * + * @param {string} ident Key for searching into the array. + * @returns {*} + */ + et2_readonlysArrayMgr.prototype.expandName = function (ident) { + return this.perspectiveData.owner.getArrayMgr('content').expandName(ident); + }; + return et2_readonlysArrayMgr; +}(et2_arrayMgr)); +exports.et2_readonlysArrayMgr = et2_readonlysArrayMgr; /** * Creates a new set of array managers * * @param _owner is the owner object of the array managers - this object (a widget) - * will free the array manager + * will free the array manager * @param _mgrs is the original set of array managers, the array managers are - * inside an associative array as recived from et2_widget::getArrayMgrs() + * inside an associative array as recived from et2_widget::getArrayMgrs() * @param _data is an associative array of new data which will be merged into the - * existing array managers. + * existing array managers. * @param _row is the row for which the array managers will be opened. */ -function et2_arrayMgrs_expand(_owner, _mgrs, _data, _row) -{ - // Create a copy of the given _mgrs associative array - var result = {}; - - // Merge the given data associative array into the existing array managers - for (var key in _mgrs) - { - result[key] = _mgrs[key]; - - if (typeof _data[key] != "undefined") - { - // Open a perspective for the given data row - var rowData = {}; - rowData[_row] = _data[key]; - - result[key] = _mgrs[key].openPerspective(_owner, rowData, _row); - } - } - - // Return the resulting managers object - return result; +function et2_arrayMgrs_expand(_owner, _mgrs, _data, _row) { + // Create a copy of the given _mgrs associative array + var result = {}; + // Merge the given data associative array into the existing array managers + for (var key in _mgrs) { + result[key] = _mgrs[key]; + if (typeof _data[key] != "undefined") { + // Open a perspective for the given data row + var rowData = {}; + rowData[_row] = _data[key]; + result[key] = _mgrs[key].openPerspective(_owner, rowData, _row); + } + } + // Return the resulting managers object + return result; } - +exports.et2_arrayMgrs_expand = et2_arrayMgrs_expand; +//# sourceMappingURL=et2_core_arrayMgr.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_core_arrayMgr.ts b/api/js/etemplate/et2_core_arrayMgr.ts new file mode 100644 index 0000000000..cfa62c179b --- /dev/null +++ b/api/js/etemplate/et2_core_arrayMgr.ts @@ -0,0 +1,456 @@ +/** + * EGroupware eTemplate2 - JS content array manager + * + * @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_common; + egw_inheritance; + et2_core_phpExpressionCompiler; +*/ + +import {et2_widget} from "./et2_core_widget"; + +/** + * Manage access to various template customisation arrays passed to etemplate->exec(). + * + * This manages access to content, modifications and readonlys arrays + */ +export class et2_arrayMgr +{ + splitIds: boolean = true; + public data: object; + // Holds information about the current perspective + public perspectiveData: { owner: et2_widget; row: number; key: string } = { + "owner": null, + "key": null, + "row": null + }; + protected static compiledExpressions: object = {}; + private readonly _parentMgr: et2_arrayMgr; + protected readOnly: boolean = false; + + /** + * Constructor + * + * @memberOf et2_arrayMgr + * @param _data + * @param _parentMgr + */ + constructor(_data: object = {}, _parentMgr?: et2_arrayMgr) { + if (typeof _parentMgr == "undefined") { + _parentMgr = null; + } + + // Copy the parent manager which is needed to access relative data when + // being in a relative perspective of the manager + this._parentMgr = _parentMgr; + + // Hold a reference to the data + if (typeof _data == "undefined" || !_data) { + egw.debug("log", "No data passed to content array manager. Probably a mismatch between template namespaces and data."); + _data = {}; + } + + // Expand sub-arrays that have been shmushed together, so further perspectives work + // Shmushed keys look like: ${row}[info_cat] + // Expanded: ${row}: Object{info_cat: ..value} + if (this.splitIds) { + // For each index, we need a key: {..} sub array + for (let key in _data) { + // Split up indexes + const indexes = key.replace(/[/g, "[").split('['); + + // Put data in the proper place + if (indexes.length > 1) { + const value = _data[key]; + let target = _data; + for (let i = 0; i < indexes.length; i++) { + indexes[i] = indexes[i].replace(/]/g, '').replace(']', ''); + if (typeof target[indexes[i]] == "undefined" || target[indexes[i]] === null) { + target[indexes[i]] = i == indexes.length - 1 ? value : {}; + } + target = target[indexes[i]]; + } + delete _data[key]; + } + } + } + + this.data = _data; + } + + /** + * Returns the root content array manager object + */ + getRoot(): et2_arrayMgr { + if (this._parentMgr != null) { + return this._parentMgr.getRoot(); + } + + return this; + } + + getParentMgr(): et2_arrayMgr { + return this._parentMgr; + } + + getPerspectiveData(): { owner: et2_widget; row: number; key: string } { + return this.perspectiveData; + } + + setPerspectiveData(new_perspective: { owner: et2_widget; row: number; key: string }) { + this.perspectiveData = new_perspective; + } + + setRow(new_row: number) { + this.perspectiveData.row = new_row; + } + + /** + * Explodes compound keys (eg IDs) into a list of namespaces + * This uses no internal values, just expands + * + * eg: + * a[b][c] => [a,b,c] + * col_filter[tr_tracker] => [col_filter, tr_tracker] + * + * @param {string} _key + * + * @return {string[]} + */ + explodeKey(_key: string): string[] { + // Parse the given key by removing the "]"-chars and splitting at "[" + let indexes = [_key]; + + if (typeof _key === "string") { + _key = _key.replace(/[/g, "[").replace(/]/g, "]"); + indexes = _key.split('['); + } + if (indexes.length > 1) { + indexes = [indexes.shift(), indexes.join('[')]; + indexes[1] = indexes[1].substring(0, indexes[1].length - 1); + const children = indexes[1].split(']['); + if (children.length) { + indexes = jQuery.merge([indexes[0]], children); + } + } + return indexes; + } + + /** + * Returns the path to this content array manager perspective as an array + * containing the key values + * + * @param _path is used internally, do not supply it manually. + */ + getPath(_path?: string[]): string[] { + if (typeof _path == "undefined") { + _path = []; + } + + if (this.perspectiveData.key != null) { + // prepend components of this.perspectiveData.key to path, can be more then one eg. "nm[rows]" + _path = this.perspectiveData.key.replace(/]/g, '').split('[').concat(_path); + } + + if (this._parentMgr != null) { + _path = this._parentMgr.getPath(_path); + } + + return _path; + } + + /** + * Get array entry is the equivalent to the boetemplate get_array function. + * It returns a reference to the (sub) array with the given key. This also works + * for keys using the ETemplate referencing scheme like a[b][c] + * + * @param _key is the string index, may contain sub-indices like a[b] + * @param _referenceInto if true none-existing sub-arrays/-indices get created + * to be returned as reference, else false is returned. Defaults to false + * @param _skipEmpty returns null if _key is not present in this content array. + * Defaults to false. + */ + getEntry(_key: string, _referenceInto?: boolean, _skipEmpty?: boolean): string | object { + if (typeof _referenceInto == "undefined") { + _referenceInto = false; + } + + if (typeof _skipEmpty == "undefined") { + _skipEmpty = false; + } + + // Parse the given key by removing the "]"-chars and splitting at "[" + const indexes = this.explodeKey(_key); + + let entry = this.data; + for (let i = 0; i < indexes.length; i++) { + // Abort if the current entry is not an object (associative array) and + // we should descend further into it. + const isObject = typeof entry === 'object'; + if (!isObject && !_referenceInto || entry == null || jQuery.isEmptyObject(entry)) { + return null; + } + + // Check whether the entry actually exists + const idx = indexes[i]; + if (_skipEmpty && (!isObject || typeof entry[idx] == "undefined")) { + return null; + } + + entry = entry[idx]; + } + + return entry; + } + + /** + * Equivalent to the boetemplate::expand_name function. + * + * Expands variables inside the given identifier to their values inside the + * content array. + * + * @param {string} _ident Key used to reference into managed array + * @return {*} + */ + expandName(_ident: string): string | object { + // Check whether the identifier refers to an index in the content array + const is_index_in_content = _ident.charAt(0) == '@'; + + // Check whether "$" occurs in the given identifier + const pos_var = _ident.indexOf('$'); + if (pos_var >= 0 && (this.perspectiveData.row != null || !_ident.match(/\$\{?row\}?/))) { + // Get the content array for the current row + const row = typeof this.perspectiveData.row == 'number' ? this.perspectiveData.row : ''; + const row_cont = this.data[row] || {}; + // $cont is NOT root but current name-space in old eTemplate + const cont = this.data;//getRoot().data; + const _cont = this.data;// according to a grep only used in ImportExport just twice + + // Check whether the expression has already been compiled - if not, + // try to compile it first. If an error occurs, the identifier + // function is set to null + if (typeof et2_arrayMgr.compiledExpressions[_ident] == "undefined") { + try { + if (this.perspectiveData.row == null) { + // No row, compile for only top level content + // @ts-ignore + et2_arrayMgr.compiledExpressions[_ident] = et2_compilePHPExpression( + _ident, ["cont", "_cont"]); + } else { + // @ts-ignore + et2_arrayMgr.compiledExpressions[_ident] = et2_compilePHPExpression( + _ident, ["row", "cont", "row_cont", "_cont"]); + } + } catch (e) { + et2_arrayMgr.compiledExpressions[_ident] = null; + egw.debug("error", "Error while compiling PHP->JS ", e); + } + } + + // Execute the previously compiled expression, if it is not "null" + // because compilation failed. The parameters have to be in the same + // order as defined during compilation. + if (et2_arrayMgr.compiledExpressions[_ident]) { + try { + if (this.perspectiveData.row == null) { + // No row, exec with only top level content + _ident = et2_arrayMgr.compiledExpressions[_ident](cont, _cont); + } else { + _ident = et2_arrayMgr.compiledExpressions[_ident](row, cont, row_cont, _cont); + } + } catch (e) { + // only log error, as they are no real errors but missing data + egw.debug("log", typeof e == 'object' ? e.message : e); + _ident = null; + } + } + } + + if (is_index_in_content && _ident) { + // If an additional "@" is specified, this means that we have to return + // the entry from the root element + if (_ident.charAt(1) == '@') { + return this.getRoot().getEntry(_ident.substr(2)); + } else { + return this.getEntry(_ident.substr(1)); + } + } + + return _ident; + } + + parseBoolExpression(_expression: string) { + // If the first char of the expression is a '!' this means, that the value + // is to be negated. + if (_expression.charAt(0) == '!') { + return !this.parseBoolExpression(_expression.substr(1)); + } + + // Split the expression at a possible "=" + const parts = _expression.split('='); + + // Expand the first value + let val = this.expandName(parts[0]); + val = typeof val == "undefined" ? '' : '' + val; + + // If a second expression existed, test that one + if (typeof parts[1] != "undefined") { + // Expand the second value + const checkVal = '' + this.expandName(parts[1]); + + // Values starting with / are treated as regular expression. It is + // checked whether the first value matches the regular expression + if (checkVal.charAt(0) == '/') { + return (new RegExp(checkVal.substr(1, checkVal.length - 2))) + .test(val); + } + + // Otherwise check for simple equality + return val == checkVal; + } + + return et2_evalBool(val); + } + + /** + * ? + * + * @param {object} _owner owner object + * @param {(string|null|object)} _root string with key, null for whole data or object with data + * @param {number?} _row key for into the _root for the desired row + */ + openPerspective(_owner: et2_widget, _root: (string | null | object), _row: number | null): et2_arrayMgr { + // Get the root node + let root = typeof _root == "string" ? this.data[_root] : + (_root == null ? this.data : _root); + if (typeof root == "undefined" && typeof _root == "string") root = this.getEntry(_root); + + // Create a new content array manager with the given root + const constructor = this.readOnly ? et2_readonlysArrayMgr : et2_arrayMgr; + const mgr = new constructor(root, this); + + // Set the owner + mgr.perspectiveData.owner = _owner; + + // Set the root key + if (typeof _root == "string") { + mgr.perspectiveData.key = _root; + } + + // Set _row parameter + if (typeof _row != "undefined") { + mgr.perspectiveData.row = _row; + } + + return mgr; + } + +} + +/** + * @augments et2_arrayMgr + */ +export class et2_readonlysArrayMgr extends et2_arrayMgr { + + readOnly : boolean = true; + + /** + * Find out if the given ID is readonly, according to the array data + * + * @memberOf et2_readonlysArrayMgr + * @param _id + * @param _attr + * @param _parent + * @returns + */ + isReadOnly(_id: string, _attr: string, _parent?: et2_arrayMgr): boolean | string { + let entry = null; + + if (_id != null) { + if (_id.indexOf('$') >= 0 || _id.indexOf('@') >= 0) { + _id = this.expandName(_id); + } + // readonlys was not namespaced in old eTemplate, therefore if we dont find data + // under current namespace, we look into parent + // (if there is anything namespaced, we will NOT look for parent!) + let mgr: et2_arrayMgr = this; + while (mgr.getParentMgr() && jQuery.isEmptyObject(mgr.data)) { + mgr = mgr.getParentMgr(); + } + entry = mgr.getEntry(_id); + } + + // Let the array entry override the read only attribute entry + if (typeof entry != "undefined" && !(typeof entry === 'object')) { + return entry; + } + + // If the attribute is set, return that + if (typeof _attr != "undefined" && _attr !== null) { + // Accept 'editable', but otherwise boolean + return this.expandName(_attr) === 'editable' ? 'editable' : et2_evalBool(_attr); + } + + // Otherwise take into accounf whether the parent is readonly + if (typeof _parent != "undefined" && _parent) { + return true; + } + + // Otherwise return the default value + entry = this.getEntry("__ALL__"); + return entry !== null && (typeof entry != "undefined"); + } + + /** + * Override parent to handle cont and row_cont. + * + * Normally these should refer to the readonlys data, but that's not + * useful, so we use the owner inside perspective data to expand using content. + * + * @param {string} ident Key for searching into the array. + * @returns {*} + */ + expandName(ident: string): any { + return this.perspectiveData.owner.getArrayMgr('content').expandName(ident); + } +} + +/** + * Creates a new set of array managers + * + * @param _owner is the owner object of the array managers - this object (a widget) + * will free the array manager + * @param _mgrs is the original set of array managers, the array managers are + * inside an associative array as recived from et2_widget::getArrayMgrs() + * @param _data is an associative array of new data which will be merged into the + * existing array managers. + * @param _row is the row for which the array managers will be opened. + */ +export function et2_arrayMgrs_expand(_owner: et2_widget, _mgrs: object, _data: object, _row: number) { + // Create a copy of the given _mgrs associative array + let result = {}; + + // Merge the given data associative array into the existing array managers + for (let key in _mgrs) { + result[key] = _mgrs[key]; + + if (typeof _data[key] != "undefined") { + // Open a perspective for the given data row + let rowData = {}; + rowData[_row] = _data[key]; + + result[key] = _mgrs[key].openPerspective(_owner, rowData, _row); + } + } + + // Return the resulting managers object + return result; +} + diff --git a/api/js/etemplate/et2_core_baseWidget.js b/api/js/etemplate/et2_core_baseWidget.js index 017a3e13ba..985c3f70cd 100644 --- a/api/js/etemplate/et2_core_baseWidget.js +++ b/api/js/etemplate/et2_core_baseWidget.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - JS Widget base 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 - /vendor/bower-asset/jquery/dist/jquery.js; - lib/tooltip; - et2_core_DOMWidget; + /vendor/bower-asset/jquery/dist/jquery.js; + lib/tooltip; + et2_core_DOMWidget; */ - +require("./et2_core_interfaces"); +require("./et2_core_common"); +var et2_core_DOMWidget_1 = require("./et2_core_DOMWidget"); +var et2_core_inheritance_1 = require("./et2_core_inheritance"); +var et2_core_widget_1 = require("./et2_core_widget"); /** * Class which manages the DOM node itself. The simpleWidget class is derrived * from et2_DOMWidget and implements the getDOMNode function. A setDOMNode @@ -23,407 +39,328 @@ * * @augments et2_DOMWidget */ -var et2_baseWidget = (function(){ "use strict"; return et2_DOMWidget.extend(et2_IAligned, -{ - attributes: { - "statustext": { - "name": "Tooltip", - "type": "string", - "description": "Tooltip which is shown for this element", - "translate": true - }, - "statustext_html": { - "name": "Tooltip is html", - "type": "boolean", - "description": "Flag to allow html content in tooltip", - "default": false - }, - "align": { - "name": "Align", - "type": "string", - "default": "left", - "description": "Position of this element in the parent hbox" - }, - "onclick": { - "name": "onclick", - "type": "js", - "default": et2_no_init, - "description": "JS code which is executed when the element is clicked." - } - }, - - /** - * Constructor - * - * @memberOf et2BaseWidget - */ - init: function() { - this.align = "left"; - - this._super.apply(this, arguments); - - this.node = null; - this.statustext = ""; - this._messageDiv = null; - this._tooltipElem = null; - }, - - destroy: function() { - this._super.apply(this, arguments); - - this.node = null; - this._messageDiv = null; - }, - - /** - * The setMessage function can be used to attach a small message box to the - * widget. This is e.g. used to display validation errors or success messages - * - * @param _text is the text which should be displayed as a message - * @param _type is an css class which is attached to the message box. - * Currently available are "hint", "success" and "validation_error", defaults - * to "hint" - * @param _floating if true, the object will be in one row with the element, - * defaults to true - * @param _prepend if set, the message is displayed behind the widget node - * instead of before. Defaults to false. - */ - showMessage: function(_text, _type, _floating, _prepend) { - - // Preset the parameters - if (typeof _type == "undefined") - { - _type = "hint"; - } - - if (typeof _floating == "undefined") - { - _floating = true; - } - - if (typeof _prepend == "undefined") - { - _prepend = false; - } - - var surr = this.getSurroundings(); - - // Remove the message div from the surroundings before creating a new - // one - this.hideMessage(false, true); - - // Create the message div and add it to the "surroundings" manager - this._messageDiv = jQuery(document.createElement("div")) - .addClass("message") - .addClass(_type) - .addClass(_floating ? "floating" : "") - .text(_text.valueOf() + ""); - - // Decide whether to prepend or append the div - if (_prepend) - { - surr.prependDOMNode(this._messageDiv[0]); - } - else - { - surr.appendDOMNode(this._messageDiv[0]); - } - - surr.update(); - }, - - /** - * The hideMessage function can be used to hide a previously shown message. - * - * @param _fade if true, the message div will fade out, otherwise the message - * div is removed immediately. Defaults to true. - * @param _noUpdate is used internally to prevent an update of the surroundings - * manager. - */ - hideMessage: function(_fade, _noUpdate) { - if (typeof _fade == "undefined") - { - _fade = true; - } - - if (typeof _noUpdate == "undefined") - { - _noUpdate = false; - } - - // Remove the message from the surroundings manager and remove the - // reference to it - if (this._messageDiv != null) - { - var surr = this.getSurroundings(); - var self = this; - var messageDiv = this._messageDiv; - self._messageDiv = null; - - var _done = function() { - surr.removeDOMNode(messageDiv[0]); - - // Update the surroundings manager - if (!_noUpdate) - { - surr.update(); - } - }; - - // Either fade out or directly call the function which removes the div - if (_fade) - { - messageDiv.fadeOut("fast", _done); - } - else - { - _done(); - } - } - }, - - detachFromDOM: function() { - // Detach this node from the tooltip node - if (this._tooltipElem) - { - this.egw().tooltipUnbind(this._tooltipElem); - this._tooltipElem = null; - } - - // Remove the binding to the click handler - if (this.node) - { - jQuery(this.node).unbind("click.et2_baseWidget"); - } - - this._super.apply(this, arguments); - }, - - attachToDOM: function() { - this._super.apply(this, arguments); - - // Add the binding for the click handler - if (this.node) - { - jQuery(this.node).bind("click.et2_baseWidget", this, function(e) { - return e.data.click.call(e.data, e, this); - }); - if (typeof this.onclick == 'function') jQuery(this.node).addClass('et2_clickable'); - } - - // Update the statustext - this.set_statustext(this.statustext); - }, - - setDOMNode: function(_node) { - if (_node != this.node) - { - // Deatch the old node from the DOM - this.detachFromDOM(); - - // Set the new DOM-Node - this.node = _node; - - // Attatch the DOM-Node to the tree - return this.attachToDOM(); - } - - return false; - }, - - getDOMNode: function() { - return this.node; - }, - - getTooltipElement: function() { - return this.getDOMNode(this); - }, - - /** - * Click handler calling custom handler set via onclick attribute to this.onclick - * - * @param _ev - * @returns - */ - click: function(_ev) { - if(typeof this.onclick == 'function') - { - // Make sure function gets a reference to the widget, splice it in as 2. argument if not - var args = Array.prototype.slice.call(arguments); - if(args.indexOf(this) == -1) args.splice(1, 0, this); - - return this.onclick.apply(this, args); - } - - return true; - }, - - set_statustext: function(_value) { - // Tooltip should not be shown in mobile view - if (egwIsMobile()) return; - // Don't execute the code below, if no tooltip will be attached/detached - if (_value == "" && !this._tooltipElem) - { - return; - } - - // allow statustext to contain multiple translated sub-strings eg: {Firstname}.{Lastname} - if (_value.indexOf('{') !== -1) - { - var egw = this.egw(); - _value = _value.replace(/{([^}]+)}/g, function(str,p1) - { - return egw.lang(p1); - }); - } - - this.statustext = _value; - - //Get the domnode the tooltip should be attached to - var elem = jQuery(this.getTooltipElement()); - - if (elem) - { - //If a tooltip is already attached to the element, remove it first - if (this._tooltipElem) - { - this.egw().tooltipUnbind(this._tooltipElem); - this._tooltipElem = null; - } - - if (_value && _value != '') - { - this.egw().tooltipBind(elem, _value, this.options.statustext_html); - this._tooltipElem = elem; - } - } - }, - - set_align: function(_value) { - this.align = _value; - }, - - get_align: function(_value) { - return this.align; - } - -});}).call(this); - +var et2_baseWidget = /** @class */ (function (_super) { + __extends(et2_baseWidget, _super); + /** + * Constructor + */ + function et2_baseWidget(_parent, _attrs, _child) { + var _this = + // Call the inherited constructor + _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_baseWidget._attributes, _child || {})) || this; + _this.align = 'left'; + _this.node = null; + _this.statustext = ''; + _this._messageDiv = null; + _this._tooltipElem = null; + return _this; + } + et2_baseWidget.prototype.destroy = function () { + _super.prototype.destroy.call(this); + this.node = null; + this._messageDiv = null; + }; + /** + * The setMessage function can be used to attach a small message box to the + * widget. This is e.g. used to display validation errors or success messages + * + * @param _text is the text which should be displayed as a message + * @param _type is an css class which is attached to the message box. + * Currently available are "hint", "success" and "validation_error", defaults + * to "hint" + * @param _floating if true, the object will be in one row with the element, + * defaults to true + * @param _prepend if set, the message is displayed behind the widget node + * instead of before. Defaults to false. + */ + et2_baseWidget.prototype.showMessage = function (_text, _type, _floating, _prepend) { + // Preset the parameters + if (typeof _type == "undefined") { + _type = "hint"; + } + if (typeof _floating == "undefined") { + _floating = true; + } + if (typeof _prepend == "undefined") { + _prepend = false; + } + var surr = this.getSurroundings(); + // Remove the message div from the surroundings before creating a new + // one + this.hideMessage(false, true); + // Create the message div and add it to the "surroundings" manager + this._messageDiv = jQuery(document.createElement("div")) + .addClass("message") + .addClass(_type) + .addClass(_floating ? "floating" : "") + .text(_text.valueOf() + ""); + // Decide whether to prepend or append the div + if (_prepend) { + surr.prependDOMNode(this._messageDiv[0]); + } + else { + surr.appendDOMNode(this._messageDiv[0]); + } + surr.update(); + }; + /** + * The hideMessage function can be used to hide a previously shown message. + * + * @param _fade if true, the message div will fade out, otherwise the message + * div is removed immediately. Defaults to true. + * @param _noUpdate is used internally to prevent an update of the surroundings + * manager. + */ + et2_baseWidget.prototype.hideMessage = function (_fade, _noUpdate) { + if (typeof _fade == "undefined") { + _fade = true; + } + if (typeof _noUpdate == "undefined") { + _noUpdate = false; + } + // Remove the message from the surroundings manager and remove the + // reference to it + if (this._messageDiv != null) { + var surr = this.getSurroundings(); + var self = this; + var messageDiv = this._messageDiv; + self._messageDiv = null; + var _done = function () { + surr.removeDOMNode(messageDiv[0]); + // Update the surroundings manager + if (!_noUpdate) { + surr.update(); + } + }; + // Either fade out or directly call the function which removes the div + if (_fade) { + messageDiv.fadeOut("fast", _done); + } + else { + _done(); + } + } + }; + et2_baseWidget.prototype.detachFromDOM = function () { + // Detach this node from the tooltip node + if (this._tooltipElem) { + this.egw().tooltipUnbind(this._tooltipElem); + this._tooltipElem = null; + } + // Remove the binding to the click handler + if (this.node) { + jQuery(this.node).unbind("click.et2_baseWidget"); + } + return _super.prototype.detachFromDOM.call(this); + }; + et2_baseWidget.prototype.attachToDOM = function () { + var ret = _super.prototype.attachToDOM.call(this); + // Add the binding for the click handler + if (this.node) { + jQuery(this.node).bind("click.et2_baseWidget", this, function (e) { + return e.data.click.call(e.data, e, this); + }); + if (typeof this.onclick == 'function') + jQuery(this.node).addClass('et2_clickable'); + } + // Update the statustext + this.set_statustext(this.statustext); + return ret; + }; + et2_baseWidget.prototype.setDOMNode = function (_node) { + if (_node != this.node) { + // Deatch the old node from the DOM + this.detachFromDOM(); + // Set the new DOM-Node + this.node = _node; + // Attatch the DOM-Node to the tree + return this.attachToDOM(); + } + return false; + }; + et2_baseWidget.prototype.getDOMNode = function (_sender) { + return this.node; + }; + et2_baseWidget.prototype.getTooltipElement = function () { + return this.getDOMNode(this); + }; + /** + * Click handler calling custom handler set via onclick attribute to this.onclick + * + * @param _ev + * @returns + */ + et2_baseWidget.prototype.click = function (_ev) { + if (typeof this.onclick == 'function') { + // Make sure function gets a reference to the widget, splice it in as 2. argument if not + var args = Array.prototype.slice.call(arguments); + if (args.indexOf(this) == -1) + args.splice(1, 0, this); + return this.onclick.apply(this, args); + } + return true; + }; + et2_baseWidget.prototype.set_statustext = function (_value) { + // Tooltip should not be shown in mobile view + if (egwIsMobile()) + return; + // Don't execute the code below, if no tooltip will be attached/detached + if (_value == "" && !this._tooltipElem) { + return; + } + // allow statustext to contain multiple translated sub-strings eg: {Firstname}.{Lastname} + if (_value.indexOf('{') !== -1) { + var egw = this.egw(); + _value = _value.replace(/{([^}]+)}/g, function (str, p1) { + return egw.lang(p1); + }); + } + this.statustext = _value; + //Get the domnode the tooltip should be attached to + var elem = jQuery(this.getTooltipElement()); + if (elem) { + //If a tooltip is already attached to the element, remove it first + if (this._tooltipElem) { + this.egw().tooltipUnbind(this._tooltipElem); + this._tooltipElem = null; + } + if (_value && _value != '') { + this.egw().tooltipBind(elem, _value, this.options.statustext_html); + this._tooltipElem = elem; + } + } + }; + et2_baseWidget.prototype.set_align = function (_value) { + this.align = _value; + }; + et2_baseWidget.prototype.get_align = function () { + return this.align; + }; + et2_baseWidget._attributes = { + "statustext": { + "name": "Tooltip", + "type": "string", + "description": "Tooltip which is shown for this element", + "translate": true + }, + "statustext_html": { + "name": "Tooltip is html", + "type": "boolean", + "description": "Flag to allow html content in tooltip", + "default": false + }, + "align": { + "name": "Align", + "type": "string", + "default": "left", + "description": "Position of this element in the parent hbox" + }, + "onclick": { + "name": "onclick", + "type": "js", + "default": et2_no_init, + "description": "JS code which is executed when the element is clicked." + } + }; + return et2_baseWidget; +}(et2_core_DOMWidget_1.et2_DOMWidget)); +exports.et2_baseWidget = et2_baseWidget; /** * Simple container object - * - * @augments et2_baseWidget */ -var et2_container = (function(){ "use strict"; return et2_baseWidget.extend( -{ - /** - * Constructor - * - * @memberOf et2_container - */ - init: function() { - this._super.apply(this, arguments); - - this.setDOMNode(document.createElement("div")); - }, - - /** - * The destroy function destroys all children of the widget, removes itself - * from the parents children list. - * Overriden to not try to remove self from parent, as that's not possible. - */ - destroy: function() { - // Call the destructor of all children - for (var i = this._children.length - 1; i >= 0; i--) - { - this._children[i].free(); - } - - // 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(); - } - } - } -});}).call(this); - +var et2_container = /** @class */ (function (_super) { + __extends(et2_container, _super); + /** + * Constructor + */ + function et2_container(_parent, _attrs, _child) { + var _this = + // Call the inherited constructor + _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_container._attributes, _child || {})) || this; + _this.setDOMNode(document.createElement("div")); + return _this; + } + /** + * The destroy function destroys all children of the widget, removes itself + * from the parents children list. + * Overriden to not try to remove self from parent, as that's not possible. + */ + et2_container.prototype.destroy = function () { + // Call the destructor of all children + for (var i = this._children.length - 1; i >= 0; i--) { + this._children[i].destroy(); + } + // Free the array managers if they belong to this widget + for (var key in this._mgrs) { + if (this._mgrs[key] && this._mgrs[key].owner == this) { + this._mgrs[key].destroy(); + } + } + }; + return et2_container; +}(et2_baseWidget)); +exports.et2_container = et2_container; +// Register widget for attributes, but not for any xml tags +et2_core_widget_1.et2_register_widget(et2_container, []); /** * Container object for not-yet supported widgets * * @augments et2_baseWidget */ -var et2_placeholder = (function(){ "use strict"; return et2_baseWidget.extend([et2_IDetachedDOM], -{ - /** - * Constructor - * - * @memberOf et2_placeholder - */ - init: function() { - this._super.apply(this, arguments); - - // The attrNodes object will hold the DOM nodes which represent the - // values of this object - this.attrNodes = {}; - - this.visible = false; - - // Create the placeholder div - this.placeDiv = jQuery(document.createElement("span")) - .addClass("et2_placeholder"); - - var headerNode = jQuery(document.createElement("span")) - .text(this._type || "") - .addClass("et2_caption") - .appendTo(this.placeDiv); - - var attrsCntr = jQuery(document.createElement("span")) - .appendTo(this.placeDiv) - .hide(); - - headerNode.click(this, function(e) { - e.data.visible = !e.data.visible; - if (e.data.visible) - { - attrsCntr.show(); - } - else - { - attrsCntr.hide(); - } - }); - - for (var key in this.options) - { - if (typeof this.options[key] != "undefined") - { - if (typeof this.attrNodes[key] == "undefined") - { - this.attrNodes[key] = jQuery(document.createElement("span")) - .addClass("et2_attr"); - attrsCntr.append(this.attrNodes[key]); - } - - this.attrNodes[key].text(key + "=" + this.options[key]); - } - } - - this.setDOMNode(this.placeDiv[0]); - }, - - getDetachedAttributes: function(_attrs) { - _attrs.push("value"); - }, - - getDetachedNodes: function() { - return [this.placeDiv[0]]; - }, - - setDetachedAttributes: function(_nodes, _values) { - this.placeDiv = jQuery(_nodes[0]); - } -});}).call(this); - +var et2_placeholder = /** @class */ (function (_super) { + __extends(et2_placeholder, _super); + /** + * Constructor + */ + function et2_placeholder(_parent, _attrs, _child) { + var _this = + // Call the inherited constructor + _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_placeholder._attributes, _child || {})) || this; + _this.visible = false; + _this.attrNodes = {}; + // Create the placeholder div + _this.placeDiv = jQuery(document.createElement("span")) + .addClass("et2_placeholder"); + var headerNode = jQuery(document.createElement("span")) + .text(_this.getType() || "") + .addClass("et2_caption") + .appendTo(_this.placeDiv); + var attrsCntr = jQuery(document.createElement("span")) + .appendTo(_this.placeDiv) + .hide(); + headerNode.click(_this, function (e) { + e.data.visible = !e.data.visible; + if (e.data.visible) { + attrsCntr.show(); + } + else { + attrsCntr.hide(); + } + }); + for (var key in _this.options) { + if (typeof _this.options[key] != "undefined") { + if (typeof _this.attrNodes[key] == "undefined") { + _this.attrNodes[key] = jQuery(document.createElement("span")) + .addClass("et2_attr"); + attrsCntr.append(_this.attrNodes[key]); + } + _this.attrNodes[key].text(key + "=" + _this.options[key]); + } + } + _this.setDOMNode(_this.placeDiv[0]); + return _this; + } + et2_placeholder.prototype.getDetachedAttributes = function (_attrs) { + _attrs.push("value"); + }; + et2_placeholder.prototype.getDetachedNodes = function () { + return [this.placeDiv[0]]; + }; + et2_placeholder.prototype.setDetachedAttributes = function (_nodes, _values) { + this.placeDiv = jQuery(_nodes[0]); + }; + return et2_placeholder; +}(et2_baseWidget)); +// Register widget, but no tags +et2_core_widget_1.et2_register_widget(et2_placeholder, []); +//# sourceMappingURL=et2_core_baseWidget.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_core_baseWidget.ts b/api/js/etemplate/et2_core_baseWidget.ts new file mode 100644 index 0000000000..4077159733 --- /dev/null +++ b/api/js/etemplate/et2_core_baseWidget.ts @@ -0,0 +1,454 @@ +/** + * 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 + */ + +/*egw:uses + /vendor/bower-asset/jquery/dist/jquery.js; + lib/tooltip; + et2_core_DOMWidget; +*/ + +import './et2_core_interfaces'; +import './et2_core_common'; +import {et2_DOMWidget} from './et2_core_DOMWidget'; +import {ClassWithAttributes} from "./et2_core_inheritance"; +import {et2_register_widget, et2_widget, WidgetConfig} from "./et2_core_widget"; + +/** + * Class which manages the DOM node itself. The simpleWidget class is derrived + * from et2_DOMWidget and implements the getDOMNode function. A setDOMNode + * function is provided, which attatches the given node to the DOM if possible. + * + * @augments et2_DOMWidget + */ +export class et2_baseWidget extends et2_DOMWidget implements et2_IAligned +{ + static readonly _attributes: any = { + "statustext": { + "name": "Tooltip", + "type": "string", + "description": "Tooltip which is shown for this element", + "translate": true + }, + "statustext_html": { + "name": "Tooltip is html", + "type": "boolean", + "description": "Flag to allow html content in tooltip", + "default": false + }, + "align": { + "name": "Align", + "type": "string", + "default": "left", + "description": "Position of this element in the parent hbox" + }, + "onclick": { + "name": "onclick", + "type": "js", + "default": et2_no_init, + "description": "JS code which is executed when the element is clicked." + } + }; + + align: string = 'left'; + node: HTMLElement = null; + statustext: string = ''; + private _messageDiv: JQuery = null; + protected _tooltipElem: JQuery = null; + onclick: any; + + /** + * Constructor + */ + constructor(_parent, _attrs? : WidgetConfig, _child? : object) + { + // Call the inherited constructor + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_baseWidget._attributes, _child || {})); + } + + destroy() + { + super.destroy(); + + this.node = null; + this._messageDiv = null; + } + + /** + * The setMessage function can be used to attach a small message box to the + * widget. This is e.g. used to display validation errors or success messages + * + * @param _text is the text which should be displayed as a message + * @param _type is an css class which is attached to the message box. + * Currently available are "hint", "success" and "validation_error", defaults + * to "hint" + * @param _floating if true, the object will be in one row with the element, + * defaults to true + * @param _prepend if set, the message is displayed behind the widget node + * instead of before. Defaults to false. + */ + showMessage(_text, _type?, _floating?, _prepend?) + { + // Preset the parameters + if (typeof _type == "undefined") + { + _type = "hint"; + } + + if (typeof _floating == "undefined") + { + _floating = true; + } + + if (typeof _prepend == "undefined") + { + _prepend = false; + } + + var surr = this.getSurroundings(); + + // Remove the message div from the surroundings before creating a new + // one + this.hideMessage(false, true); + + // Create the message div and add it to the "surroundings" manager + this._messageDiv = jQuery(document.createElement("div")) + .addClass("message") + .addClass(_type) + .addClass(_floating ? "floating" : "") + .text(_text.valueOf() + ""); + + // Decide whether to prepend or append the div + if (_prepend) + { + surr.prependDOMNode(this._messageDiv[0]); + } + else + { + surr.appendDOMNode(this._messageDiv[0]); + } + + surr.update(); + } + + /** + * The hideMessage function can be used to hide a previously shown message. + * + * @param _fade if true, the message div will fade out, otherwise the message + * div is removed immediately. Defaults to true. + * @param _noUpdate is used internally to prevent an update of the surroundings + * manager. + */ + hideMessage(_fade? : boolean, _noUpdate? : boolean) + { + if (typeof _fade == "undefined") + { + _fade = true; + } + + if (typeof _noUpdate == "undefined") + { + _noUpdate = false; + } + + // Remove the message from the surroundings manager and remove the + // reference to it + if (this._messageDiv != null) + { + var surr = this.getSurroundings(); + var self = this; + var messageDiv = this._messageDiv; + self._messageDiv = null; + + var _done = function() { + surr.removeDOMNode(messageDiv[0]); + + // Update the surroundings manager + if (!_noUpdate) + { + surr.update(); + } + }; + + // Either fade out or directly call the function which removes the div + if (_fade) + { + messageDiv.fadeOut("fast", _done); + } + else + { + _done(); + } + } + } + + detachFromDOM() + { + // Detach this node from the tooltip node + if (this._tooltipElem) + { + this.egw().tooltipUnbind(this._tooltipElem); + this._tooltipElem = null; + } + + // Remove the binding to the click handler + if (this.node) + { + jQuery(this.node).unbind("click.et2_baseWidget"); + } + + return super.detachFromDOM(); + } + + attachToDOM() + { + let ret = super.attachToDOM(); + + // Add the binding for the click handler + if (this.node) + { + jQuery(this.node).bind("click.et2_baseWidget", this, function(e) { + return e.data.click.call(e.data, e, this); + }); + if (typeof this.onclick == 'function') jQuery(this.node).addClass('et2_clickable'); + } + + // Update the statustext + this.set_statustext(this.statustext); + + return ret; + } + + setDOMNode(_node) + { + if (_node != this.node) + { + // Deatch the old node from the DOM + this.detachFromDOM(); + + // Set the new DOM-Node + this.node = _node; + + // Attatch the DOM-Node to the tree + return this.attachToDOM(); + } + + return false; + } + + getDOMNode(_sender?: et2_widget) + { + return this.node; + } + + getTooltipElement() + { + return this.getDOMNode(this); + } + + /** + * Click handler calling custom handler set via onclick attribute to this.onclick + * + * @param _ev + * @returns + */ + click(_ev) + { + if(typeof this.onclick == 'function') + { + // Make sure function gets a reference to the widget, splice it in as 2. argument if not + var args = Array.prototype.slice.call(arguments); + if(args.indexOf(this) == -1) args.splice(1, 0, this); + + return this.onclick.apply(this, args); + } + + return true; + } + + set_statustext(_value) + { + // Tooltip should not be shown in mobile view + if (egwIsMobile()) return; + // Don't execute the code below, if no tooltip will be attached/detached + if (_value == "" && !this._tooltipElem) + { + return; + } + + // allow statustext to contain multiple translated sub-strings eg: {Firstname}.{Lastname} + if (_value.indexOf('{') !== -1) + { + var egw = this.egw(); + _value = _value.replace(/{([^}]+)}/g, function(str,p1) + { + return egw.lang(p1); + }); + } + + this.statustext = _value; + + //Get the domnode the tooltip should be attached to + var elem = jQuery(this.getTooltipElement()); + + if (elem) + { + //If a tooltip is already attached to the element, remove it first + if (this._tooltipElem) + { + this.egw().tooltipUnbind(this._tooltipElem); + this._tooltipElem = null; + } + + if (_value && _value != '') + { + this.egw().tooltipBind(elem, _value, this.options.statustext_html); + this._tooltipElem = elem; + } + } + } + + set_align(_value) + { + this.align = _value; + } + + get_align() + { + return this.align; + } +} + +/** + * Simple container object + */ +export class et2_container extends et2_baseWidget +{ + /** + * Constructor + */ + constructor(_parent, _attrs? : WidgetConfig, _child? : object) + { + // Call the inherited constructor + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_container._attributes, _child || {})); + + this.setDOMNode(document.createElement("div")); + } + + /** + * The destroy function destroys all children of the widget, removes itself + * from the parents children list. + * Overriden to not try to remove self from parent, as that's not possible. + */ + destroy() + { + // Call the destructor of all children + for (var i = this._children.length - 1; i >= 0; i--) + { + this._children[i].destroy(); + } + + // Free the array managers if they belong to this widget + for (var key in this._mgrs) + { + if (this._mgrs[key] && this._mgrs[key].owner == this) + { + this._mgrs[key].destroy(); + } + } + } +} +// Register widget for attributes, but not for any xml tags +et2_register_widget(et2_container, []); + +/** + * Container object for not-yet supported widgets + * + * @augments et2_baseWidget + */ +class et2_placeholder extends et2_baseWidget implements et2_IDetachedDOM +{ + /** + * he attrNodes object will hold the DOM nodes which represent the + * values of this object + */ + attrNodes: {}; + visible: boolean = false; + placeDiv: JQuery; + + /** + * Constructor + */ + constructor(_parent, _attrs? : WidgetConfig, _child? : object) + { + // Call the inherited constructor + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_placeholder._attributes, _child || {})); + + this.attrNodes = {}; + + // Create the placeholder div + this.placeDiv = jQuery(document.createElement("span")) + .addClass("et2_placeholder"); + + var headerNode = jQuery(document.createElement("span")) + .text(this.getType() || "") + .addClass("et2_caption") + .appendTo(this.placeDiv); + + var attrsCntr = jQuery(document.createElement("span")) + .appendTo(this.placeDiv) + .hide(); + + headerNode.click(this, function(e) { + e.data.visible = !e.data.visible; + if (e.data.visible) + { + attrsCntr.show(); + } + else + { + attrsCntr.hide(); + } + }); + + for (var key in this.options) + { + if (typeof this.options[key] != "undefined") + { + if (typeof this.attrNodes[key] == "undefined") + { + this.attrNodes[key] = jQuery(document.createElement("span")) + .addClass("et2_attr"); + attrsCntr.append(this.attrNodes[key]); + } + + this.attrNodes[key].text(key + "=" + this.options[key]); + } + } + + this.setDOMNode(this.placeDiv[0]); + } + + getDetachedAttributes(_attrs) + { + _attrs.push("value"); + } + + getDetachedNodes() + { + return [this.placeDiv[0]]; + } + + setDetachedAttributes(_nodes, _values) + { + this.placeDiv = jQuery(_nodes[0]); + } +} +// Register widget, but no tags +et2_register_widget(et2_placeholder, []); diff --git a/api/js/etemplate/et2_core_common.js b/api/js/etemplate/et2_core_common.js index 878b913ada..445c9d2cba 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,277 @@ 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) { + var _a; + 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 ((_a = s) === null || _a === void 0 ? void 0 : _a.text) { + 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; } +//# sourceMappingURL=et2_core_common.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_core_common.ts b/api/js/etemplate/et2_core_common.ts new file mode 100644 index 0000000000..5114c847b7 --- /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_editableWidget.js b/api/js/etemplate/et2_core_editableWidget.js index 4bd0f27f2d..63028d781a 100644 --- a/api/js/etemplate/et2_core_editableWidget.js +++ b/api/js/etemplate/et2_core_editableWidget.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - JS Widget base class * @@ -5,15 +6,27 @@ * @package etemplate * @subpackage api * @link http://www.egroupware.org - * @author Andreas Stöckel - * @copyright Stylite 2011 - * @version $Id$ + * @author Nathan Gray */ - +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_inputWidget; + et2_core_inputWidget; */ - +var et2_core_inputWidget_1 = require("./et2_core_inputWidget"); +var et2_core_inheritance_1 = require("./et2_core_inheritance"); /** * et2_editableWidget derives from et2_inputWidget and adds the ability to start * readonly, then turn editable on double-click. If we decide to do this with @@ -21,184 +34,161 @@ * * @augments et2_inputWidget */ -var et2_editableWidget = (function(){ "use strict"; return et2_inputWidget.extend(et2_ISubmitListener, -{ - attributes: { - readonly: { - name: "readonly", - type: "string", // | boolean - default: false, - description: "If set to 'editable' will start readonly, double clicking will make it editable and clicking out will save" - }, - toggle_readonly: { - name: "toggle_readonly", - type: "boolean", - default: true, - description: "Double clicking makes widget editable. If off, must be made editable in some other way." - }, - save_callback: { - name: "save_callback", - type: "string", - default: et2_no_init, - description: "Ajax callback to save changed value when readonly is 'editable'. If not provided, a regular submit is done." - }, - save_callback_params: { - name: "readonly", - type: "string", - default: et2_no_init, - description: "Additional parameters passed to save_callback" - }, - editable_height: { - name: "Editable height", - description: "Set height for widget while in edit mode", - type: "string" - } - }, - - /** - * Constructor - * - * @memberOf et2_inputWidget - */ - init: function(_parent, _attrs) { - // 'Editable' really should be boolean for everything else to work - if(_attrs.readonly && typeof _attrs.readonly === 'string') - { - _attrs.readonly = true; - this._toggle_readonly = _attrs.toggle_readonly; - } - this._super.apply(this, arguments); - }, - - destroy: function() { - var node = this.getInputNode(); - if (node) - { - jQuery(node).off('.et2_editableWidget'); - } - - this._super.apply(this, arguments); - }, - - /** - * Load the validation errors from the server - * - * @param {object} _attrs - */ - transformAttributes: function(_attrs) { - this._super.apply(this, arguments); - - }, - - attachToDOM: function() { - this._super.apply(this,arguments); - var node = this.getDOMNode(); - if (node && this._toggle_readonly) - { - jQuery(node) - .off('.et2_editableWidget') - .on("dblclick.et2_editableWidget", this, function(e) { - e.data.dblclick.call(e.data, this); - }) - .addClass('et2_clickable et2_editable'); - } - else - { - jQuery(node).addClass('et2_editable_readonly'); - } - - }, - - detatchFromDOM: function() { - this._super.apply(this,arguments); - }, - - /** - * Handle double click - * - * Turn widget editable - * - * @param {DOMNode} _node - */ - dblclick: function (_node) { - // Turn off readonly - this.set_readonly(false); - - jQuery('body').on("click.et2_editableWidget", this, function(e) { - // Make sure click comes from body, not a popup - if(jQuery.contains(this, e.target) && e.target.type != 'textarea') - { - jQuery(this).off("click.et2_editableWidget"); - e.data.focusout.call(e.data, this); - } - }); - }, - - /** - * User clicked somewhere else, save and turn back to readonly - * - * @param {DOMNode} _node Body node - * @returns {et2_core_editableWidgetet2_editableWidget.et2_core_editableWidgetAnonym$0@call;getInstanceManager@call;submit} - */ - focusout: function (_node) - { - var value = this.get_value(); - var oldValue = this._oldValue; - - // Change back to readonly - this.set_readonly(true); - - // No change, do nothing - if(value == oldValue) return; - - - // Submit - if(this.options.save_callback) - { - var params = [value]; - if(this.options.save_callback_params) - { - params = params.concat(this.options.save_callback_params.split(',')); - } - - egw.json(this.options.save_callback, params, function() { - }, this, true, this).sendRequest(); - } - else - { - this.set_value(value); - return this.getInstanceManager().submit(); - } - }, - - /** - * Called whenever the template gets submitted. - * If we have a save_callback, we call that before the submit (no check on - * the result) - * - * @param _values contains the values which will be sent to the server. - * Listeners may change these values before they get submitted. - */ - submit: function(_values) { - if(this.options.readonly) - { - // Not currently editing, just continue on - return true; - } - // Change back to readonly - this.set_readonly(true); - - var params = [this.get_value()]; - if(this.options.save_callback_params) - { - params = params.concat(this.options.save_callback_params.split(',')); - } - if(this.options.save_callback) - { - egw.json(this.options.save_callback, params, function() { - }, this, true, this).sendRequest(); - } - return true; - } -});}).call(this); - +var et2_editableWidget = /** @class */ (function (_super) { + __extends(et2_editableWidget, _super); + /** + * Constructor + */ + function et2_editableWidget(_parent, _attrs, _child) { + var _this = this; + // 'Editable' really should be boolean for everything else to work + if (_attrs.readonly && typeof _attrs.readonly === 'string') { + _attrs.readonly = true; + var toggle_readonly = _attrs.toggle_readonly; + } + // Call the inherited constructor + _this = _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_editableWidget._attributes, _child || {})) || this; + if (typeof toggle_readonly != 'undefined') + _this._toggle_readonly = toggle_readonly; + return _this; + } + et2_editableWidget.prototype.destroy = function () { + var node = this.getInputNode(); + if (node) { + jQuery(node).off('.et2_editableWidget'); + } + _super.prototype.destroy.call(this); + }; + /** + * Load the validation errors from the server + * + * @param {object} _attrs + */ + et2_editableWidget.prototype.transformAttributes = function (_attrs) { + _super.prototype.transformAttributes.call(this, _attrs); + }; + et2_editableWidget.prototype.attachToDOM = function () { + var res = _super.prototype.attachToDOM.call(this); + var node = this.getDOMNode(); + if (node && this._toggle_readonly) { + jQuery(node) + .off('.et2_editableWidget') + .on("dblclick.et2_editableWidget", this, function (e) { + e.data.dblclick.call(e.data, this); + }) + .addClass('et2_clickable et2_editable'); + } + else { + jQuery(node).addClass('et2_editable_readonly'); + } + return res; + }; + et2_editableWidget.prototype.detatchFromDOM = function () { + _super.prototype.detatchFromDOM.call(this); + }; + /** + * Handle double click + * + * Turn widget editable + * + * @param {DOMNode} _node + */ + et2_editableWidget.prototype.dblclick = function (_node) { + // Turn off readonly + this.set_readonly(false); + jQuery('body').on("click.et2_editableWidget", this, function (e) { + // Make sure click comes from body, not a popup + if (jQuery.contains(this, e.target) && e.target.type != 'textarea') { + jQuery(this).off("click.et2_editableWidget"); + e.data.focusout.call(e.data, this); + } + }); + }; + /** + * User clicked somewhere else, save and turn back to readonly + * + * @param {DOMNode} _node Body node + * @returns {et2_core_editableWidgetet2_editableWidget.et2_core_editableWidgetAnonym$0@call;getInstanceManager@call;submit} + */ + et2_editableWidget.prototype.focusout = function (_node) { + var value = this.get_value(); + var oldValue = this._oldValue; + // Change back to readonly + this.set_readonly(true); + // No change, do nothing + if (value == oldValue) + return; + // Submit + if (this.options.save_callback) { + var params = [value]; + if (this.options.save_callback_params) { + params = params.concat(this.options.save_callback_params.split(',')); + } + egw.json(this.options.save_callback, params, function () { + }, this, true, this).sendRequest(); + } + else { + this.set_value(value); + return this.getInstanceManager().submit(); + } + }; + /** + * Called whenever the template gets submitted. + * If we have a save_callback, we call that before the submit (no check on + * the result) + * + * @param _values contains the values which will be sent to the server. + * Listeners may change these values before they get submitted. + */ + et2_editableWidget.prototype.submit = function (_values) { + if (this.options.readonly) { + // Not currently editing, just continue on + return true; + } + // Change back to readonly + this.set_readonly(true); + var params = [this.get_value()]; + if (this.options.save_callback_params) { + params = params.concat(this.options.save_callback_params.split(',')); + } + if (this.options.save_callback) { + egw.json(this.options.save_callback, params, function () { + }, this, true, this).sendRequest(); + } + return true; + }; + et2_editableWidget._attributes = { + readonly: { + name: "readonly", + type: "string", + default: false, + description: "If set to 'editable' will start readonly, double clicking will make it editable and clicking out will save" + }, + toggle_readonly: { + name: "toggle_readonly", + type: "boolean", + default: true, + description: "Double clicking makes widget editable. If off, must be made editable in some other way." + }, + save_callback: { + name: "save_callback", + type: "string", + default: et2_no_init, + description: "Ajax callback to save changed value when readonly is 'editable'. If not provided, a regular submit is done." + }, + save_callback_params: { + name: "readonly", + type: "string", + default: et2_no_init, + description: "Additional parameters passed to save_callback" + }, + editable_height: { + name: "Editable height", + description: "Set height for widget while in edit mode", + type: "string" + } + }; + return et2_editableWidget; +}(et2_core_inputWidget_1.et2_inputWidget)); +exports.et2_editableWidget = et2_editableWidget; +//# sourceMappingURL=et2_core_editableWidget.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_core_editableWidget.ts b/api/js/etemplate/et2_core_editableWidget.ts new file mode 100644 index 0000000000..c60e58b7e8 --- /dev/null +++ b/api/js/etemplate/et2_core_editableWidget.ts @@ -0,0 +1,213 @@ +/** + * 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 Nathan Gray + */ + +/*egw:uses + et2_core_inputWidget; +*/ + +import {et2_inputWidget} from "./et2_core_inputWidget"; +import {WidgetConfig} from "./et2_core_widget"; +import {ClassWithAttributes} from "./et2_core_inheritance"; + +/** + * et2_editableWidget derives from et2_inputWidget and adds the ability to start + * readonly, then turn editable on double-click. If we decide to do this with + * more widgets, it should just be merged with et2_inputWidget. + * + * @augments et2_inputWidget + */ +export class et2_editableWidget extends et2_inputWidget implements et2_ISubmitListener +{ + static readonly _attributes : any = { + readonly: { + name: "readonly", + type: "string", // | boolean + default: false, + description: "If set to 'editable' will start readonly, double clicking will make it editable and clicking out will save" + }, + toggle_readonly: { + name: "toggle_readonly", + type: "boolean", + default: true, + description: "Double clicking makes widget editable. If off, must be made editable in some other way." + }, + save_callback: { + name: "save_callback", + type: "string", + default: et2_no_init, + description: "Ajax callback to save changed value when readonly is 'editable'. If not provided, a regular submit is done." + }, + save_callback_params: { + name: "readonly", + type: "string", + default: et2_no_init, + description: "Additional parameters passed to save_callback" + }, + editable_height: { + name: "Editable height", + description: "Set height for widget while in edit mode", + type: "string" + } + }; + + private _toggle_readonly : boolean; + + /** + * Constructor + */ + constructor(_parent, _attrs? : WidgetConfig, _child? : object) + { + // 'Editable' really should be boolean for everything else to work + if(_attrs.readonly && typeof _attrs.readonly === 'string') + { + _attrs.readonly = true; + var toggle_readonly = _attrs.toggle_readonly; + } + // Call the inherited constructor + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_editableWidget._attributes, _child || {})); + if (typeof toggle_readonly != 'undefined') this._toggle_readonly = toggle_readonly; + } + + destroy() + { + let node = this.getInputNode(); + if (node) + { + jQuery(node).off('.et2_editableWidget'); + } + super.destroy(); + } + + /** + * Load the validation errors from the server + * + * @param {object} _attrs + */ + transformAttributes(_attrs) + { + super.transformAttributes(_attrs); + } + + attachToDOM() + { + let res = super.attachToDOM(); + let node = this.getDOMNode(); + if (node && this._toggle_readonly) + { + jQuery(node) + .off('.et2_editableWidget') + .on("dblclick.et2_editableWidget", this, function(e) { + e.data.dblclick.call(e.data, this); + }) + .addClass('et2_clickable et2_editable'); + } + else + { + jQuery(node).addClass('et2_editable_readonly'); + } + return res; + } + + detatchFromDOM() + { + super.detatchFromDOM(); + } + + /** + * Handle double click + * + * Turn widget editable + * + * @param {DOMNode} _node + */ + dblclick(_node) + { + // Turn off readonly + this.set_readonly(false); + + jQuery('body').on("click.et2_editableWidget", this, function(e) { + // Make sure click comes from body, not a popup + if(jQuery.contains(this, e.target) && e.target.type != 'textarea') + { + jQuery(this).off("click.et2_editableWidget"); + e.data.focusout.call(e.data, this); + } + }); + } + + /** + * User clicked somewhere else, save and turn back to readonly + * + * @param {DOMNode} _node Body node + * @returns {et2_core_editableWidgetet2_editableWidget.et2_core_editableWidgetAnonym$0@call;getInstanceManager@call;submit} + */ + focusout(_node) + { + var value = this.get_value(); + var oldValue = this._oldValue; + + // Change back to readonly + this.set_readonly(true); + + // No change, do nothing + if(value == oldValue) return; + + + // Submit + if(this.options.save_callback) + { + var params = [value]; + if(this.options.save_callback_params) + { + params = params.concat(this.options.save_callback_params.split(',')); + } + + egw.json(this.options.save_callback, params, function() { + }, this, true, this).sendRequest(); + } + else + { + this.set_value(value); + return this.getInstanceManager().submit(); + } + } + + /** + * Called whenever the template gets submitted. + * If we have a save_callback, we call that before the submit (no check on + * the result) + * + * @param _values contains the values which will be sent to the server. + * Listeners may change these values before they get submitted. + */ + submit(_values) + { + if(this.options.readonly) + { + // Not currently editing, just continue on + return true; + } + // Change back to readonly + this.set_readonly(true); + + var params = [this.get_value()]; + if(this.options.save_callback_params) + { + params = params.concat(this.options.save_callback_params.split(',')); + } + if(this.options.save_callback) + { + egw.json(this.options.save_callback, params, function() { + }, this, true, this).sendRequest(); + } + return true; + } +} + diff --git a/api/js/etemplate/et2_core_inheritance.js b/api/js/etemplate/et2_core_inheritance.js index 37f00022ae..93d0bb3033 100644 --- a/api/js/etemplate/et2_core_inheritance.js +++ b/api/js/etemplate/et2_core_inheritance.js @@ -1,158 +1,206 @@ +"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.generateAttributeSet = function (widget, _attrs) { + // Sanity check and validation + for (var key in _attrs) { + if (typeof widget[key] != "undefined") { + if (!widget[key].ignore) { + _attrs[key] = et2_checkType(_attrs[key], widget[key].type, key, this); + } + } + else { + // Key does not exist - delete it and issue a warning + delete (_attrs[key]); + egw.debug("warn", this, "Attribute '" + key + + "' does not exist in " + _attrs.type + "!"); + } + } + // Include default values or already set values for this attribute + for (var key in widget) { + if (typeof _attrs[key] == "undefined") { + var _default = widget[key]["default"]; + if (_default == et2_no_init) { + _default = undefined; + } + _attrs[key] = _default; + } + } + return _attrs; + }; + /** + * The initAttributes function sets the attributes to their default + * values. The attributes are not overwritten, which means, that the + * default is only set, if either a setter exists or this[propName] does + * not exist yet. + * + * @param {object} _attrs is the associative array containing the attributes. + */ + 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); + } + } + }; + ClassWithAttributes.buildAttributes = function (class_prototype) { + var class_tree = []; + var attributes = {}; + var n = 0; + do { + n++; + class_tree.push(class_prototype); + class_prototype = Object.getPrototypeOf(class_prototype); + } while (class_prototype !== ClassWithAttributes && n < 50); + for (var i = class_tree.length - 1; i >= 0; i--) { + attributes = ClassWithAttributes.extendAttributes(attributes, class_tree[i]._attributes); + } + return attributes; + }; + /** + * Extend current _attributes with the one from the parent class + * + * This gives inheritance from the parent plus the ability to override in the current class. + * + * @param _attributes + * @param _parent + */ + ClassWithAttributes.extendAttributes = function (_parent, _attributes) { + 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; + }; + /** + * 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 + */ + ClassWithAttributes.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 + */ + ClassWithAttributes.prototype.instanceOf = function (_class_or_interfacename) { + if (typeof _class_or_interfacename === 'string') { + return this.implements(_class_or_interfacename); + } + return this instanceof _class_or_interfacename; + }; + return ClassWithAttributes; +}()); +exports.ClassWithAttributes = ClassWithAttributes; +//# sourceMappingURL=et2_core_inheritance.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_core_inheritance.ts b/api/js/etemplate/et2_core_inheritance.ts new file mode 100644 index 0000000000..4948401dd7 --- /dev/null +++ b/api/js/etemplate/et2_core_inheritance.ts @@ -0,0 +1,269 @@ +/** + * 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. + */ + static generateAttributeSet(widget, _attrs) + { + // Sanity check and validation + for (var key in _attrs) + { + if (typeof widget[key] != "undefined") + { + if (!widget[key].ignore) + { + _attrs[key] = et2_checkType(_attrs[key], widget[key].type, + key, this); + } + } + else + { + // Key does not exist - delete it and issue a warning + delete(_attrs[key]); + egw.debug("warn", this, "Attribute '" + key + + "' does not exist in " + _attrs.type+"!"); + } + } + + // Include default values or already set values for this attribute + for (var key in widget) + { + if (typeof _attrs[key] == "undefined") + { + var _default = widget[key]["default"]; + if (_default == et2_no_init) + { + _default = undefined; + } + + _attrs[key] = _default; + } + } + + return _attrs; + } + + /** + * The initAttributes function sets the attributes to their default + * values. The attributes are not overwritten, which means, that the + * default is only set, if either a setter exists or this[propName] does + * not exist yet. + * + * @param {object} _attrs is the associative array containing the attributes. + */ + initAttributes(_attrs) + { + for (var key in _attrs) + { + if (typeof this.attributes[key] != "undefined" && !this.attributes[key].ignore && !(_attrs[key] == undefined)) + { + this.setAttribute(key, _attrs[key], false); + } + } + } + + static buildAttributes(class_prototype: object) + { + let class_tree = []; + let attributes = {}; + let n = 0; + do + { + n++; + class_tree.push(class_prototype); + class_prototype = Object.getPrototypeOf(class_prototype); + } while (class_prototype !== ClassWithAttributes && n < 50); + + for(let i = class_tree.length - 1; i >= 0; i--) + { + attributes = ClassWithAttributes.extendAttributes(attributes, class_tree[i]._attributes); + } + return attributes; + } + /** + * Extend current _attributes with the one from the parent class + * + * This gives inheritance from the parent plus the ability to override in the current class. + * + * @param _attributes + * @param _parent + */ + static extendAttributes(_parent : object, _attributes : object) : object + { + function _copyMerge(_new, _old) + { + 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; + } + + /** + * 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; + } +} \ No newline at end of file diff --git a/api/js/etemplate/et2_core_inputWidget.js b/api/js/etemplate/et2_core_inputWidget.js index 6c9cfdab1b..c5e6a37ba9 100644 --- a/api/js/etemplate/et2_core_inputWidget.js +++ b/api/js/etemplate/et2_core_inputWidget.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - JS Widget base class * @@ -6,317 +7,280 @@ * @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 - /vendor/bower-asset/jquery/dist/jquery.js; - et2_core_interfaces; - et2_core_valueWidget; + /vendor/bower-asset/jquery/dist/jquery.js; + et2_core_interfaces; + et2_core_valueWidget; */ - +require("./et2_core_common"); +var et2_core_inheritance_1 = require("./et2_core_inheritance"); +var et2_core_valueWidget_1 = require("./et2_core_valueWidget"); +require("./et2_types"); /** * et2_inputWidget derrives from et2_simpleWidget and implements the IInput * interface. When derriving from this class, call setDOMNode with an input * DOMNode. - * - * @augments et2_valueWidget */ -var et2_inputWidget = (function(){ "use strict"; return et2_valueWidget.extend([et2_IInput,et2_ISubmitListener], -{ - attributes: { - "needed": { - "name": "Required", - "default": false, - "type": "boolean", - "description": "If required, the user must enter a value before the form can be submitted" - }, - "onchange": { - "name": "onchange", - "type": "js", - "default": et2_no_init, - "description": "JS code which is executed when the value changes." - }, - "onfocus": { - "name": "onfocus", - "type": "js", - "default": et2_no_init, - "description": "JS code which get executed when wiget receives focus." - }, - "validation_error": { - "name": "Validation Error", - "type": "string", - "default": et2_no_init, - "description": "Used internally to store the validation error that came from the server." - }, - "tabindex": { - "name": "Tab index", - "type": "integer", - "default": et2_no_init, - "description": "Specifies the tab order of a widget when the 'tab' button is used for navigating." - }, - readonly: { - name: "readonly", - type: "boolean", - "default": false, - description: "Does NOT allow user to enter data, just displays existing data" - } - }, - - /** - * Constructor - * - * @memberOf et2_inputWidget - */ - init: function() { - this._super.apply(this, arguments); - - // mark value as not initialised, so set_value can determine if it is necessary to trigger change event - this._oldValue = et2_no_init; - this._labelContainer = null; - }, - - destroy: function() { - var node = this.getInputNode(); - if (node) - { - jQuery(node).unbind("change.et2_inputWidget"); - jQuery(node).unbind("focus"); - } - - this._super.apply(this, arguments); - - this._labelContainer = null; - }, - - /** - * Load the validation errors from the server - * - * @param {object} _attrs - */ - transformAttributes: function(_attrs) { - this._super.apply(this, arguments); - - // Check whether an validation error entry exists - if (this.id && this.getArrayMgr("validation_errors")) - { - var val = this.getArrayMgr("validation_errors").getEntry(this.id); - if (val) - { - _attrs["validation_error"] = val; - } - } - }, - - attachToDOM: function() { - var node = this.getInputNode(); - if (node) - { - jQuery(node) - .off('.et2_inputWidget') - .bind("change.et2_inputWidget", this, function(e) { - e.data.change.call(e.data, this); - }) - .bind("focus.et2_inputWidget", this, function(e) { - e.data.focus.call(e.data, this); - }); - } - - this._super.apply(this,arguments); - -// jQuery(this.getInputNode()).attr("novalidate","novalidate"); // Stop browser from getting involved -// jQuery(this.getInputNode()).validator(); - }, - - detatchFromDOM: function() { -// if(this.getInputNode()) { -// jQuery(this.getInputNode()).data("validator").destroy(); -// } - this._super.apply(this,arguments); - }, - - change: function(_node) { - var messages = []; - var valid = this.isValid(messages); - - // Passing false will clear any set messages - this.set_validation_error(valid ? false : messages); - - if (valid && this.onchange) - { - if(typeof this.onchange == 'function') - { - // Make sure function gets a reference to the widget - var args = Array.prototype.slice.call(arguments); - if(args.indexOf(this) == -1) args.push(this); - - return this.onchange.apply(this, args); - } else { - return (et2_compileLegacyJS(this.options.onchange, this, _node))(); - } - } - return valid; - }, - - focus: function(_node) - { - if(typeof this.options.onfocus == 'function') - { - // Make sure function gets a reference to the widget - var args = Array.prototype.slice.call(arguments); - if(args.indexOf(this) == -1) args.push(this); - - return this.options.onfocus.apply(this, args); - } - }, - - /** - * Set value of widget and trigger for real changes a change event - * - * First initialisation (_oldValue === et2_no_init) is NOT considered a change! - * - * @param {string} _value value to set - */ - set_value: function(_value) - { - var node = this.getInputNode(); - if (node) - { - jQuery(node).val(_value); - if(this.isAttached() && this._oldValue !== et2_no_init && this._oldValue !== _value) - { - jQuery(node).change(); - } - } - this._oldValue = _value; - }, - - set_id: function(_value) { - this.id = _value; - this.dom_id = _value && this.getInstanceManager() ? this.getInstanceManager().uniqueId+'_'+this.id : _value; - - // Set the id of the _input_ node (in contrast to the default - // implementation, which sets the base node) - var node = this.getInputNode(); - if (node) - { - // Unique ID to prevent DOM collisions across multiple templates - if (_value != "") - { - node.setAttribute("id", this.dom_id); - node.setAttribute("name", _value); - } - else - { - node.removeAttribute("id"); - node.removeAttribute("name"); - } - } - }, - - set_needed: function(_value) { - var node = this.getInputNode(); - if (node) - { - if(_value && !this.options.readonly) { - jQuery(node).attr("required", "required"); - } else { - node.removeAttribute("required"); - } - } - - }, - - set_validation_error: function(_value) { - var node = this.getInputNode(); - if (node) - { - if (_value === false) - { - this.hideMessage(); - jQuery(node).removeClass("invalid"); - } - else - { - this.showMessage(_value, "validation_error"); - jQuery(node).addClass("invalid"); - - // If on a tab, switch to that tab so user can see it - var widget = this; - while(widget._parent && widget._type != 'tabbox') - { - widget = widget._parent; - } - if (widget._type == 'tabbox') widget.activateTab(this); - } - } - }, - - /** - * Set tab index - * - * @param {number} index - */ - set_tabindex: function(index) { - jQuery(this.getInputNode()).attr("tabindex", index); - }, - - getInputNode: function() { - return this.node; - }, - - get_value: function() { - return this.getValue(); - }, - - getValue: function() { - var node = this.getInputNode(); - if (node) - { - var val = jQuery(node).val(); - - return val; - } - - return this._oldValue; - }, - - isDirty: function() { - return this._oldValue != this.getValue(); - }, - - resetDirty: function() { - this._oldValue = this.getValue(); - }, - - isValid: function(messages) { - var ok = true; - - // Check for required - if (this.options && this.options.needed && !this.options.readonly && !this.disabled && - (this.getValue() == null || this.getValue().valueOf() == '')) - { - messages.push(this.egw().lang('Field must not be empty !!!')); - ok = false; - } - return ok; - }, - - /** - * Called whenever the template gets submitted. We return false if the widget - * is not valid, which cancels the 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) { - var messages = []; - var valid = this.isValid(messages); - - // Passing false will clear any set messages - this.set_validation_error(valid ? false : messages); - return valid; - } -});}).call(this); - +var et2_inputWidget = /** @class */ (function (_super) { + __extends(et2_inputWidget, _super); + /** + * Constructor + */ + function et2_inputWidget(_parent, _attrs, _child) { + var _this = + // Call the inherited constructor + _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_inputWidget._attributes, _child || {})) || this; + // mark value as not initialised, so set_value can determine if it is necessary to trigger change event + _this._oldValue = et2_no_init; + _this._labelContainer = null; + return _this; + } + et2_inputWidget.prototype.destroy = function () { + var node = this.getInputNode(); + if (node) { + jQuery(node).unbind("change.et2_inputWidget"); + jQuery(node).unbind("focus"); + } + _super.prototype.destroy.call(this); + this._labelContainer = null; + }; + /** + * Load the validation errors from the server + * + * @param {object} _attrs + */ + et2_inputWidget.prototype.transformAttributes = function (_attrs) { + _super.prototype.transformAttributes.call(this, _attrs); + // Check whether an validation error entry exists + if (this.id && this.getArrayMgr("validation_errors")) { + var val = this.getArrayMgr("validation_errors").getEntry(this.id); + if (val) { + _attrs["validation_error"] = val; + } + } + }; + et2_inputWidget.prototype.attachToDOM = function () { + var node = this.getInputNode(); + if (node) { + jQuery(node) + .off('.et2_inputWidget') + .bind("change.et2_inputWidget", this, function (e) { + e.data.change.call(e.data, this); + }) + .bind("focus.et2_inputWidget", this, function (e) { + e.data.focus.call(e.data, this); + }); + } + return _super.prototype.attachToDOM.call(this); + // jQuery(this.getInputNode()).attr("novalidate","novalidate"); // Stop browser from getting involved + // jQuery(this.getInputNode()).validator(); + }; + et2_inputWidget.prototype.detatchFromDOM = function () { + // if(this.getInputNode()) { + // jQuery(this.getInputNode()).data("validator").destroy(); + // } + _super.prototype.detachFromDOM.call(this); + }; + et2_inputWidget.prototype.change = function (_node, _widget, _value) { + var messages = []; + var valid = this.isValid(messages); + // Passing false will clear any set messages + this.set_validation_error(valid ? false : messages); + if (valid && this.onchange) { + if (typeof this.onchange == 'function') { + // Make sure function gets a reference to the widget + var args = Array.prototype.slice.call(arguments); + if (args.indexOf(this) == -1) + args.push(this); + return this.onchange.apply(this, args); + } + else { + return (et2_compileLegacyJS(this.options.onchange, this, _node))(); + } + } + return valid; + }; + et2_inputWidget.prototype.focus = function (_node) { + if (typeof this.options.onfocus == 'function') { + // Make sure function gets a reference to the widget + var args = Array.prototype.slice.call(arguments); + if (args.indexOf(this) == -1) + args.push(this); + return this.options.onfocus.apply(this, args); + } + }; + /** + * Set value of widget and trigger for real changes a change event + * + * First initialisation (_oldValue === et2_no_init) is NOT considered a change! + * + * @param {string} _value value to set + */ + et2_inputWidget.prototype.set_value = function (_value) { + var node = this.getInputNode(); + if (node) { + jQuery(node).val(_value); + if (this.isAttached() && this._oldValue !== et2_no_init && this._oldValue !== _value) { + jQuery(node).change(); + } + } + this._oldValue = _value; + }; + et2_inputWidget.prototype.set_id = function (_value) { + this.id = _value; + this.dom_id = _value && this.getInstanceManager() ? this.getInstanceManager().uniqueId + '_' + this.id : _value; + // Set the id of the _input_ node (in contrast to the default + // implementation, which sets the base node) + var node = this.getInputNode(); + if (node) { + // Unique ID to prevent DOM collisions across multiple templates + if (_value != "") { + node.setAttribute("id", this.dom_id); + node.setAttribute("name", _value); + } + else { + node.removeAttribute("id"); + node.removeAttribute("name"); + } + } + }; + et2_inputWidget.prototype.set_needed = function (_value) { + var node = this.getInputNode(); + if (node) { + if (_value && !this.options.readonly) { + jQuery(node).attr("required", "required"); + } + else { + node.removeAttribute("required"); + } + } + }; + et2_inputWidget.prototype.set_validation_error = function (_value) { + var node = this.getInputNode(); + if (node) { + if (_value === false) { + this.hideMessage(); + jQuery(node).removeClass("invalid"); + } + else { + this.showMessage(_value, "validation_error"); + jQuery(node).addClass("invalid"); + // If on a tab, switch to that tab so user can see it + var widget = this; + while (widget.getParent() && widget.getType() != 'tabbox') { + widget = widget.getParent(); + } + if (widget.getType() == 'tabbox') + widget.activateTab(this); + } + } + }; + /** + * Set tab index + * + * @param {number} index + */ + et2_inputWidget.prototype.set_tabindex = function (index) { + jQuery(this.getInputNode()).attr("tabindex", index); + }; + et2_inputWidget.prototype.getInputNode = function () { + return this.node; + }; + et2_inputWidget.prototype.get_value = function () { + return this.getValue(); + }; + et2_inputWidget.prototype.getValue = function () { + var node = this.getInputNode(); + if (node) { + var val = jQuery(node).val(); + return val; + } + return this._oldValue; + }; + et2_inputWidget.prototype.isDirty = function () { + return this._oldValue != this.getValue(); + }; + et2_inputWidget.prototype.resetDirty = function () { + this._oldValue = this.getValue(); + }; + et2_inputWidget.prototype.isValid = function (messages) { + var ok = true; + // Check for required + if (this.options && this.options.needed && !this.options.readonly && !this.disabled && + (this.getValue() == null || this.getValue().valueOf() == '')) { + messages.push(this.egw().lang('Field must not be empty !!!')); + ok = false; + } + return ok; + }; + /** + * Called whenever the template gets submitted. We return false if the widget + * is not valid, which cancels the submission. + * + * @param _values contains the values which will be sent to the server. + * Listeners may change these values before they get submitted. + */ + et2_inputWidget.prototype.submit = function (_values) { + var messages = []; + var valid = this.isValid(messages); + // Passing false will clear any set messages + this.set_validation_error(valid ? false : messages); + return valid; + }; + et2_inputWidget._attributes = { + "needed": { + "name": "Required", + "default": false, + "type": "boolean", + "description": "If required, the user must enter a value before the form can be submitted" + }, + "onchange": { + "name": "onchange", + "type": "js", + "default": et2_no_init, + "description": "JS code which is executed when the value changes." + }, + "onfocus": { + "name": "onfocus", + "type": "js", + "default": et2_no_init, + "description": "JS code which get executed when wiget receives focus." + }, + "validation_error": { + "name": "Validation Error", + "type": "string", + "default": et2_no_init, + "description": "Used internally to store the validation error that came from the server." + }, + "tabindex": { + "name": "Tab index", + "type": "integer", + "default": et2_no_init, + "description": "Specifies the tab order of a widget when the 'tab' button is used for navigating." + }, + readonly: { + name: "readonly", + type: "boolean", + "default": false, + description: "Does NOT allow user to enter data, just displays existing data" + } + }; + return et2_inputWidget; +}(et2_core_valueWidget_1.et2_valueWidget)); +exports.et2_inputWidget = et2_inputWidget; +//# sourceMappingURL=et2_core_inputWidget.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_core_inputWidget.ts b/api/js/etemplate/et2_core_inputWidget.ts new file mode 100644 index 0000000000..134f30093d --- /dev/null +++ b/api/js/etemplate/et2_core_inputWidget.ts @@ -0,0 +1,343 @@ +/** + * 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 + */ + +/*egw:uses + /vendor/bower-asset/jquery/dist/jquery.js; + et2_core_interfaces; + et2_core_valueWidget; +*/ + +import './et2_core_common'; +import { ClassWithAttributes } from "./et2_core_inheritance"; +import { et2_widget, et2_createWidget, et2_register_widget, WidgetConfig } from "./et2_core_widget"; +import { et2_DOMWidget } from './et2_core_DOMWidget' +import { et2_valueWidget } from './et2_core_valueWidget' +import './et2_types'; + +/** + * et2_inputWidget derrives from et2_simpleWidget and implements the IInput + * interface. When derriving from this class, call setDOMNode with an input + * DOMNode. + */ +export class et2_inputWidget extends et2_valueWidget implements et2_IInput, et2_ISubmitListener +{ + static readonly _attributes : any = { + "needed": { + "name": "Required", + "default": false, + "type": "boolean", + "description": "If required, the user must enter a value before the form can be submitted" + }, + "onchange": { + "name": "onchange", + "type": "js", + "default": et2_no_init, + "description": "JS code which is executed when the value changes." + }, + "onfocus": { + "name": "onfocus", + "type": "js", + "default": et2_no_init, + "description": "JS code which get executed when wiget receives focus." + }, + "validation_error": { + "name": "Validation Error", + "type": "string", + "default": et2_no_init, + "description": "Used internally to store the validation error that came from the server." + }, + "tabindex": { + "name": "Tab index", + "type": "integer", + "default": et2_no_init, + "description": "Specifies the tab order of a widget when the 'tab' button is used for navigating." + }, + readonly: { + name: "readonly", + type: "boolean", + "default": false, + description: "Does NOT allow user to enter data, just displays existing data" + } + } + + protected _oldValue: any; + onchange: Function; + + /** + * Constructor + */ + constructor(_parent, _attrs? : WidgetConfig, _child? : object) + { + // Call the inherited constructor + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_inputWidget._attributes, _child || {})); + + // mark value as not initialised, so set_value can determine if it is necessary to trigger change event + this._oldValue = et2_no_init; + this._labelContainer = null; + } + + destroy() + { + var node = this.getInputNode(); + if (node) + { + jQuery(node).unbind("change.et2_inputWidget"); + jQuery(node).unbind("focus"); + } + + super.destroy(); + + this._labelContainer = null; + } + + /** + * Load the validation errors from the server + * + * @param {object} _attrs + */ + transformAttributes(_attrs) + { + super.transformAttributes(_attrs); + + // Check whether an validation error entry exists + if (this.id && this.getArrayMgr("validation_errors")) + { + var val = this.getArrayMgr("validation_errors").getEntry(this.id); + if (val) + { + _attrs["validation_error"] = val; + } + } + } + + attachToDOM() + { + var node = this.getInputNode(); + if (node) + { + jQuery(node) + .off('.et2_inputWidget') + .bind("change.et2_inputWidget", this, function(e) { + e.data.change.call(e.data, this); + }) + .bind("focus.et2_inputWidget", this, function(e) { + e.data.focus.call(e.data, this); + }); + } + + return super.attachToDOM(); + +// jQuery(this.getInputNode()).attr("novalidate","novalidate"); // Stop browser from getting involved +// jQuery(this.getInputNode()).validator(); + } + + detatchFromDOM() + { +// if(this.getInputNode()) { +// jQuery(this.getInputNode()).data("validator").destroy(); +// } + super.detachFromDOM(); + } + + change(_node, _widget?, _value?) + { + var messages = []; + var valid = this.isValid(messages); + + // Passing false will clear any set messages + this.set_validation_error(valid ? false : messages); + + if (valid && this.onchange) + { + if(typeof this.onchange == 'function') + { + // Make sure function gets a reference to the widget + var args = Array.prototype.slice.call(arguments); + if(args.indexOf(this) == -1) args.push(this); + + return this.onchange.apply(this, args); + } else { + return (et2_compileLegacyJS(this.options.onchange, this, _node))(); + } + } + return valid; + } + + focus(_node) + { + if(typeof this.options.onfocus == 'function') + { + // Make sure function gets a reference to the widget + var args = Array.prototype.slice.call(arguments); + if(args.indexOf(this) == -1) args.push(this); + + return this.options.onfocus.apply(this, args); + } + } + + /** + * Set value of widget and trigger for real changes a change event + * + * First initialisation (_oldValue === et2_no_init) is NOT considered a change! + * + * @param {string} _value value to set + */ + set_value(_value : any | null) + { + var node = this.getInputNode(); + if (node) + { + jQuery(node).val(_value); + if(this.isAttached() && this._oldValue !== et2_no_init && this._oldValue !== _value) + { + jQuery(node).change(); + } + } + this._oldValue = _value; + } + + set_id(_value) + { + this.id = _value; + this.dom_id = _value && this.getInstanceManager() ? this.getInstanceManager().uniqueId+'_'+this.id : _value; + + // Set the id of the _input_ node (in contrast to the default + // implementation, which sets the base node) + var node = this.getInputNode(); + if (node) + { + // Unique ID to prevent DOM collisions across multiple templates + if (_value != "") + { + node.setAttribute("id", this.dom_id); + node.setAttribute("name", _value); + } + else + { + node.removeAttribute("id"); + node.removeAttribute("name"); + } + } + } + + set_needed(_value) + { + var node = this.getInputNode(); + if (node) + { + if(_value && !this.options.readonly) { + jQuery(node).attr("required", "required"); + } else { + node.removeAttribute("required"); + } + } + } + + set_validation_error(_value) + { + var node = this.getInputNode(); + if (node) + { + if (_value === false) + { + this.hideMessage(); + jQuery(node).removeClass("invalid"); + } + else + { + this.showMessage(_value, "validation_error"); + jQuery(node).addClass("invalid"); + + // If on a tab, switch to that tab so user can see it + let widget : et2_widget = this; + while(widget.getParent() && widget.getType() != 'tabbox') + { + widget = widget.getParent(); + } + if (widget.getType() == 'tabbox') (widget).activateTab(this); + } + } + } + + /** + * Set tab index + * + * @param {number} index + */ + set_tabindex(index) + { + jQuery(this.getInputNode()).attr("tabindex", index); + } + + getInputNode() + { + return this.node; + } + + get_value() + { + return this.getValue(); + } + + getValue() + { + var node = this.getInputNode(); + if (node) + { + var val = jQuery(node).val(); + + return val; + } + + return this._oldValue; + } + + isDirty() + { + return this._oldValue != this.getValue(); + } + + resetDirty() + { + this._oldValue = this.getValue(); + } + + isValid(messages) + { + var ok = true; + + // Check for required + if (this.options && this.options.needed && !this.options.readonly && !this.disabled && + (this.getValue() == null || this.getValue().valueOf() == '')) + { + messages.push(this.egw().lang('Field must not be empty !!!')); + ok = false; + } + return ok; + } + + /** + * Called whenever the template gets submitted. We return false if the widget + * is not valid, which cancels the submission. + * + * @param _values contains the values which will be sent to the server. + * Listeners may change these values before they get submitted. + */ + submit(_values) + { + var messages = []; + var valid = this.isValid(messages); + + // Passing false will clear any set messages + this.set_validation_error(valid ? false : messages); + return valid; + } +} + diff --git a/api/js/etemplate/et2_core_interfaces.js b/api/js/etemplate/et2_core_interfaces.js index dc87241236..db083a072f 100644 --- a/api/js/etemplate/et2_core_interfaces.js +++ b/api/js/etemplate/et2_core_interfaces.js @@ -6,156 +6,51 @@ * @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; +} +var et2_IDOMNode = "et2_IDOMNode"; +function implements_et2_IDOMNode(obj) { + return implements_methods(obj, ["getDOMNode"]); +} +var et2_IInput = "et2_IInput"; +function implements_et2_IInput(obj) { + return implements_methods(obj, ["getValue", "isDirty", "resetDirty", "isValid"]); +} +var et2_IResizeable = "et2_IResizeable"; +function implements_et2_IResizeable(obj) { + return implements_methods(obj, ["resize"]); +} +var et2_IAligned = "et2_IAligned"; +function implements_et2_IAligned(obj) { + return implements_methods(obj, ["get_align"]); +} +var et2_ISubmitListener = "et2_ISubmitListener"; +function implements_et2_ISubmitListener(obj) { + return implements_methods(obj, ["submit"]); +} +var et2_IDetachedDOM = "et2_IDetachedDOM"; +function implements_et2_IDetachedDOM(obj) { + return implements_methods(obj, ["getDetachedAttributes", "getDetachedNodes", "setDetachedAttributes"]); +} +var et2_IPrint = "et2_IPrint"; +function implements_et2_IPrint(obj) { + return implements_methods(obj, ["beforePrint", "afterPrint"]); +} +var et2_IExposable = "et2_IExposable"; +function implements_et2_IExposable(obj) { + return implements_methods(obj, ["getMedia"]); +} +//# sourceMappingURL=et2_core_interfaces.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_core_interfaces.ts b/api/js/etemplate/et2_core_interfaces.ts new file mode 100644 index 0000000000..52ef4a8a10 --- /dev/null +++ b/api/js/etemplate/et2_core_interfaces.ts @@ -0,0 +1,230 @@ +/** + * 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 +} +var et2_IDOMNode = "et2_IDOMNode"; +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 +} +var et2_IInput = "et2_IInput"; +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(number) : void +} +var et2_IResizeable = "et2_IResizeable"; +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 +} +var et2_IAligned = "et2_IAligned"; +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 +} +var et2_ISubmitListener = "et2_ISubmitListener"; +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, _data?) : void +} +var et2_IDetachedDOM = "et2_IDetachedDOM"; +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 +} +var et2_IPrint = "et2_IPrint"; +function implements_et2_IPrint(obj : et2_widget) +{ + return implements_methods(obj, ["beforePrint", "afterPrint"]); +} + +/** + * Interface all exposed widget must support in order to getMedia for the blueimp Gallery. + */ +interface et2_IExposable +{ + /** + * get media an array of media objects to pass to blueimp Gallery + * @param {array} _attrs + */ + getMedia(_attrs) : void; +} +var et2_IExposable = "et2_IExposable"; +function implements_et2_IExposable(obj : et2_widget) +{ + return implements_methods(obj, ["getMedia"]); +} \ No newline at end of file diff --git a/api/js/etemplate/et2_core_valueWidget.js b/api/js/etemplate/et2_core_valueWidget.js index 404986cd9a..246562d3e2 100644 --- a/api/js/etemplate/et2_core_valueWidget.js +++ b/api/js/etemplate/et2_core_valueWidget.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - JS widget class with value attribute and auto loading * @@ -9,124 +10,125 @@ * @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 - /vendor/bower-asset/jquery/dist/jquery.js; - et2_core_baseWidget; + /vendor/bower-asset/jquery/dist/jquery.js; + et2_core_baseWidget; */ - +var et2_core_baseWidget_1 = require("./et2_core_baseWidget"); +require("./et2_core_common"); +var et2_core_inheritance_1 = require("./et2_core_inheritance"); /** * et2_valueWidget is the base class for et2_inputWidget - valueWidget introduces * the "value" attribute and automatically loads it from the "content" array * after loading from XML. - * - * @augments et2_baseWidget */ -var et2_valueWidget = (function(){ "use strict"; return et2_baseWidget.extend( -{ - attributes: { - "label": { - "name": "Label", - "default": "", - "type": "string", - "description": "The label is displayed by default in front (for radiobuttons behind) each widget (if not empty). If you want to specify a different position, use a '%s' in the label, which gets replaced by the widget itself. Eg. '%s Name' to have the label Name behind a checkbox. The label can contain variables, as descript for name. If the label starts with a '@' it is replaced by the value of the content-array at this index (with the '@'-removed and after expanding the variables).", - "translate": true - }, - "value": { - "name": "Value", - "description": "The value of the widget", - "type": "rawstring", // no html-entity decoding - "default": et2_no_init - } - }, - - /** - * - * - * @memberOf et2_valueWidget - * @param _attrs - */ - transformAttributes: function(_attrs) { - this._super.apply(this, arguments); - - if (this.id) - { - // Set the value for this element - var contentMgr = this.getArrayMgr("content"); - if (contentMgr != null) { - var val = contentMgr.getEntry(this.id,false,true); - if (val !== null) - { - _attrs["value"] = val; - } - } - // Check for already inside namespace - if(this.createNamespace && this.getArrayMgr("content").perspectiveData.owner == this) - { - _attrs["value"] = this.getArrayMgr("content").data; - } - - } - }, - - set_label: function(_value) { - // Abort if ther was no change in the label - if (_value == this.label) - { - return; - } - - if (_value) - { - // Create the label container if it didn't exist yet - if (this._labelContainer == null) - { - this._labelContainer = jQuery(document.createElement("label")) - .addClass("et2_label"); - this.getSurroundings().insertDOMNode(this._labelContainer[0]); - } - - // Clear the label container. - this._labelContainer.empty(); - - // Create the placeholder element and set it - var ph = document.createElement("span"); - this.getSurroundings().setWidgetPlaceholder(ph); - - // Split the label at the "%s" - var parts = et2_csvSplit(_value, 2, "%s"); - - // Update the content of the label container - for (var i = 0; i < parts.length; i++) - { - if (parts[i]) - { - this._labelContainer.append(document.createTextNode(parts[i])); - } - if (i == 0) - { - this._labelContainer.append(ph); - } - } - - // add class if label is empty - this._labelContainer.toggleClass('et2_label_empty', !_value || !parts[0]); - } - else - { - // Delete the labelContainer from the surroundings object - if (this._labelContainer) - { - this.getSurroundings().removeDOMNode(this._labelContainer[0]); - } - this._labelContainer = null; - } - - // Update the surroundings in order to reflect the change in the label - this.getSurroundings().update(); - - // Copy the given value - this.label = _value; - } -});}).call(this); - +var et2_valueWidget = /** @class */ (function (_super) { + __extends(et2_valueWidget, _super); + /** + * Constructor + */ + function et2_valueWidget(_parent, _attrs, _child) { + var _this = + // Call the inherited constructor + _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_valueWidget._attributes, _child || {})) || this; + _this.label = ''; + _this._labelContainer = null; + return _this; + } + /** + * + * @param _attrs + */ + et2_valueWidget.prototype.transformAttributes = function (_attrs) { + _super.prototype.transformAttributes.call(this, _attrs); + if (this.id) { + // Set the value for this element + var contentMgr = this.getArrayMgr("content"); + if (contentMgr != null) { + var val = contentMgr.getEntry(this.id, false, true); + if (val !== null) { + _attrs["value"] = val; + } + } + // Check for already inside namespace + if (this._createNamespace() && this.getArrayMgr("content").perspectiveData.owner == this) { + _attrs["value"] = this.getArrayMgr("content").data; + } + } + }; + et2_valueWidget.prototype.set_label = function (_value) { + // Abort if there was no change in the label + if (_value == this.label) { + return; + } + if (_value) { + // Create the label container if it didn't exist yet + if (this._labelContainer == null) { + this._labelContainer = jQuery(document.createElement("label")) + .addClass("et2_label"); + this.getSurroundings().insertDOMNode(this._labelContainer[0]); + } + // Clear the label container. + this._labelContainer.empty(); + // Create the placeholder element and set it + var ph = document.createElement("span"); + this.getSurroundings().setWidgetPlaceholder(ph); + // Split the label at the "%s" + var parts = et2_csvSplit(_value, 2, "%s"); + // Update the content of the label container + for (var i = 0; i < parts.length; i++) { + if (parts[i]) { + this._labelContainer.append(document.createTextNode(parts[i])); + } + if (i == 0) { + this._labelContainer.append(ph); + } + } + // add class if label is empty + this._labelContainer.toggleClass('et2_label_empty', !_value || !parts[0]); + } + else { + // Delete the labelContainer from the surroundings object + if (this._labelContainer) { + this.getSurroundings().removeDOMNode(this._labelContainer[0]); + } + this._labelContainer = null; + } + // Update the surroundings in order to reflect the change in the label + this.getSurroundings().update(); + // Copy the given value + this.label = _value; + }; + et2_valueWidget._attributes = { + "label": { + "name": "Label", + "default": "", + "type": "string", + "description": "The label is displayed by default in front (for radiobuttons behind) each widget (if not empty). If you want to specify a different position, use a '%s' in the label, which gets replaced by the widget itself. Eg. '%s Name' to have the label Name behind a checkbox. The label can contain variables, as descript for name. If the label starts with a '@' it is replaced by the value of the content-array at this index (with the '@'-removed and after expanding the variables).", + "translate": true + }, + "value": { + "name": "Value", + "description": "The value of the widget", + "type": "rawstring", + "default": et2_no_init + } + }; + return et2_valueWidget; +}(et2_core_baseWidget_1.et2_baseWidget)); +exports.et2_valueWidget = et2_valueWidget; +//# sourceMappingURL=et2_core_valueWidget.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_core_valueWidget.ts b/api/js/etemplate/et2_core_valueWidget.ts new file mode 100644 index 0000000000..cc2f9768ac --- /dev/null +++ b/api/js/etemplate/et2_core_valueWidget.ts @@ -0,0 +1,147 @@ +/** + * EGroupware eTemplate2 - JS widget class with value attribute and auto loading + * + * @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 + * @version $Id$ + */ + +/*egw:uses + /vendor/bower-asset/jquery/dist/jquery.js; + et2_core_baseWidget; +*/ + +import { et2_baseWidget } from './et2_core_baseWidget' +import './et2_core_common'; +import {WidgetConfig} from "./et2_core_widget"; +import {ClassWithAttributes} from "./et2_core_inheritance"; + +/** + * et2_valueWidget is the base class for et2_inputWidget - valueWidget introduces + * the "value" attribute and automatically loads it from the "content" array + * after loading from XML. + */ +export class et2_valueWidget extends et2_baseWidget +{ + static readonly _attributes : any = { + "label": { + "name": "Label", + "default": "", + "type": "string", + "description": "The label is displayed by default in front (for radiobuttons behind) each widget (if not empty). If you want to specify a different position, use a '%s' in the label, which gets replaced by the widget itself. Eg. '%s Name' to have the label Name behind a checkbox. The label can contain variables, as descript for name. If the label starts with a '@' it is replaced by the value of the content-array at this index (with the '@'-removed and after expanding the variables).", + "translate": true + }, + "value": { + "name": "Value", + "description": "The value of the widget", + "type": "rawstring", // no html-entity decoding + "default": et2_no_init + } + }; + + label: string = ''; + protected _labelContainer: JQuery = null; + + /** + * Constructor + */ + constructor(_parent, _attrs? : WidgetConfig, _child? : object) + { + // Call the inherited constructor + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_valueWidget._attributes, _child || {})); + } + + /** + * + * @param _attrs + */ + transformAttributes(_attrs : object) + { + super.transformAttributes(_attrs); + + if (this.id) + { + // Set the value for this element + var contentMgr = this.getArrayMgr("content"); + if (contentMgr != null) { + let val = contentMgr.getEntry(this.id, false, true); + if (val !== null) + { + _attrs["value"] = val; + } + } + // Check for already inside namespace + if(this._createNamespace() && this.getArrayMgr("content").perspectiveData.owner == this) + { + _attrs["value"] = this.getArrayMgr("content").data; + } + + } + } + + set_label(_value : string) + { + // Abort if there was no change in the label + if (_value == this.label) + { + return; + } + + if (_value) + { + // Create the label container if it didn't exist yet + if (this._labelContainer == null) + { + this._labelContainer = jQuery(document.createElement("label")) + .addClass("et2_label"); + this.getSurroundings().insertDOMNode(this._labelContainer[0]); + } + + // Clear the label container. + this._labelContainer.empty(); + + // Create the placeholder element and set it + var ph = document.createElement("span"); + this.getSurroundings().setWidgetPlaceholder(ph); + + // Split the label at the "%s" + var parts = et2_csvSplit(_value, 2, "%s"); + + // Update the content of the label container + for (var i = 0; i < parts.length; i++) + { + if (parts[i]) + { + this._labelContainer.append(document.createTextNode(parts[i])); + } + if (i == 0) + { + this._labelContainer.append(ph); + } + } + + // add class if label is empty + this._labelContainer.toggleClass('et2_label_empty', !_value || !parts[0]); + } + else + { + // Delete the labelContainer from the surroundings object + if (this._labelContainer) + { + this.getSurroundings().removeDOMNode(this._labelContainer[0]); + } + this._labelContainer = null; + } + + // Update the surroundings in order to reflect the change in the label + this.getSurroundings().update(); + + // Copy the given value + this.label = _value; + } +} + diff --git a/api/js/etemplate/et2_core_widget.js b/api/js/etemplate/et2_core_widget.js index fc3c0623ff..5595818e3a 100644 --- a/api/js/etemplate/et2_core_widget.js +++ b/api/js/etemplate/et2_core_widget.js @@ -1,57 +1,64 @@ +"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 = {}; - +var et2_attribute_registry = {}; /** - * Registers the widget class defined by the given constructor and associates it + * Registers the widget class defined by the given constructor, registers all its class attributes, and associates it * with the types in the _types array. * * @param {function} _constructor constructor * @param {array} _types widget types _constructor wants to register for */ -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"; + et2_attribute_registry[_constructor.name] = et2_core_inheritance_1.ClassWithAttributes.buildAttributes(_constructor); + // Iterate over all given types and register those + for (var i = 0; i < _types.length; i++) { + var type = _types[i].toLowerCase(); + // Check whether a widget has already been registered for one of the + // types. + if (et2_registry[type]) { + egw.debug("warn", "Widget class registered for " + type + + " will be overwritten."); + } + et2_registry[type] = _constructor; + } } - +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 +72,835 @@ 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 + et2_core_inheritance_1.ClassWithAttributes.generateAttributeSet(et2_attribute_registry[constructor.name], _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; + // Set the legacyOptions array to the names of the properties the "options" + // attribute defines. + _this.legacyOptions = []; + _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(_attrs); + } + } + if (_this.id) { + //this.id = this.id.replace(/\[/g,'[').replace(/]/g,']'); + } + // Add all attributes hidden in the content arrays to the attributes + // parameter + _this.transformAttributes(_attrs); + // Create a local copy of the options object + _this.options = et2_cloneObject(_attrs); + 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].destroy(); + } + // Remove this element from the parent, if it exists + if (typeof this._parent != "undefined" && this._parent !== null) { + this._parent.removeChild(this); + } + // Free the array managers if they belong to this widget + for (var key in this._mgrs) { + if (this._mgrs[key] && this._mgrs[key].owner == this) { + this._mgrs[key].destroy(); + } + } + }; + et2_widget.prototype.getType = function () { + return this._type; + }; + et2_widget.prototype.setType = function (_type) { + this._type = _type; + }; + /** + * Creates a copy of this widget. The parameters given are passed to the + * constructor of the copied object. If the parameters are omitted, _parent + * is defaulted to null + * + * @param {et2_widget} _parent parent to set for clone, default null + */ + 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); + /* + Comment this out (for now) + if (_node.implements(et2_IDOMNode) && this.implements(et2_IDOMNode) && typeof _node.hasOwnProperty('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.constructor.legacyOptions && _proto.constructor.legacyOptions.length > 0) { + var legacy = _proto.constructor.legacyOptions || []; + var attrs = et2_attribute_registry[Object.getPrototypeOf(_proto).constructor.name] || {}; + // Check for modifications on legacy options here. Normal modifications + // are handled in widget constructor, but it's too late for legacy options then + if (_target.id && this.getArrayMgr("modifications").getEntry(_target.id)) { + var mod = this.getArrayMgr("modifications").getEntry(_target.id); + if (typeof mod.options != "undefined") + attrValue = _attrsObj[i].value = mod.options; + } + // expand legacyOptions with content + if (attrValue.charAt(0) == '@' || attrValue.indexOf('$') != -1) { + attrValue = mgr.expandName(attrValue); + } + // Parse the legacy options (as a string, other types not allowed) + var splitted = et2_csvSplit(attrValue + ""); + for (var j = 0; j < splitted.length && j < legacy.length; j++) { + // Blank = not set, unless there's more legacy options provided after + if (splitted[j].trim().length === 0 && legacy.length >= splitted.length) + continue; + // Check to make sure we don't overwrite a current option with a legacy option + if (typeof _target[legacy[j]] === "undefined") { + attrValue = splitted[j]; + /** + If more legacy options than expected, stuff them all in the last legacy option + Some legacy options take a comma separated list. + */ + if (j == legacy.length - 1 && splitted.length > legacy.length) { + attrValue = splitted.slice(j); + } + var attr = et2_attribute_registry[_proto.constructor.name][legacy[j]] || {}; + // If the attribute is marked as boolean, parse the + // expression as bool expression. + if (attr.type == "boolean") { + attrValue = mgr.parseBoolExpression(attrValue); + } + else if (typeof attrValue != "object") { + attrValue = mgr.expandName(attrValue); + } + _target[legacy[j]] = attrValue; + } + } + } + else if (attrName == "readonly" && typeof _target[attrName] != "undefined") { + // do NOT overwrite already evaluated readonly attribute + } + else { + var attrs = et2_attribute_registry[_proto.constructor.name] || {}; + if (mgr != null && typeof attrs[attrName] != "undefined") { + var attr = attrs[attrName]; + // If the attribute is marked as boolean, parse the + // expression as bool expression. + if (attr.type == "boolean") { + attrValue = mgr.parseBoolExpression(attrValue); + } + else { + attrValue = mgr.expandName(attrValue); + } + } + // Set the attribute + _target[attrName] = attrValue; + } + } + }; + /** + * Apply the "modifications" to the element and translate attributes marked + * with "translate: true" + * + * @param {object} _attrs + */ + 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 + * @param _name XML node name + * + * @return et2_widget + */ + et2_widget.prototype.createElementFromNode = function (_node, _name) { + var attributes = {}; + // Parse the "readonly" and "type" flag for this element here, as they + // determine which constructor is used + var _nodeName = attributes["type"] = _node.getAttribute("type") ? + _node.getAttribute("type") : _node.nodeName.toLowerCase(); + var readonly = attributes["readonly"] = this.getArrayMgr("readonlys") ? + this.getArrayMgr("readonlys").isReadOnly(_node.getAttribute("id"), _node.getAttribute("readonly"), typeof this.readonly !== 'undefined' ? this.readonly : this.options.readonly) : false; + // Check to see if modifications change type + var modifications = this.getArrayMgr("modifications"); + if (modifications && _node.getAttribute("id")) { + var entry = 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 + et2_core_inheritance_1.ClassWithAttributes.generateAttributeSet(et2_attribute_registry[constructor.name], attributes); + // Creates the new widget, passes this widget as an instance and + // passes the widgetType. Then it goes on loading the XML for it. + var widget = new constructor(this, attributes); + // Load the widget itself from XML + widget.loadFromXML(_node); + return widget; + }; + /** + * Loads the widget tree from an XML node + * + * @param _node xml node + */ + 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 {IegwAppLocal} _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} managed_array_type name of array mgr to return + */ + et2_widget.prototype.getArrayMgr = function (managed_array_type) { + if (this._mgrs && typeof this._mgrs[managed_array_type] != "undefined") { + return this._mgrs[managed_array_type]; + } + else if (this._parent) { + return this._parent.getArrayMgr(managed_array_type); + } + return null; + }; + /** + * Checks whether a namespace exists for this element in the content array. + * If yes, an own perspective of the content array is created. If not, the + * parent content manager is used. + * + * Constructor attributes are passed in case a child needs to make decisions + */ + et2_widget.prototype.checkCreateNamespace = function (_attrs) { + // Get the content manager + var mgrs = this.getArrayMgrs(); + for (var key in mgrs) { + var mgr = mgrs[key]; + // Get the original content manager if we have already created a + // perspective for this node + if (typeof this._mgrs[key] != "undefined" && mgr.perspectiveData.owner == this) { + mgr = mgr.parentMgr; + } + // Check whether the manager has a namespace for the id of this object + var entry = mgr.getEntry(this.id); + if (typeof entry === 'object' && entry !== null || this.id) { + // The content manager has an own node for this object, so + // create an own perspective. + this._mgrs[key] = mgr.openPerspective(this, this.id); + } + else { + // The current content manager does not have an own namespace for + // this element, so use the content manager of the parent. + delete (this._mgrs[key]); + } + } + }; + /** + * Widgets that do support a namespace should override and return true. + * + * Since a private attribute doesn't get instanciated properly before it's needed, + * we use a method so we can get what we need while still in the constructor. + * + * @private + */ + et2_widget.prototype._createNamespace = function () { + return false; + }; + /** + * 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; + }; + 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; +//# sourceMappingURL=et2_core_widget.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_core_widget.ts b/api/js/etemplate/et2_core_widget.ts new file mode 100644 index 0000000000..3672b416ed --- /dev/null +++ b/api/js/etemplate/et2_core_widget.ts @@ -0,0 +1,1060 @@ +/** + * 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 = {}; +var et2_attribute_registry = {}; + + +/** + * Registers the widget class defined by the given constructor, registers all its class attributes, and associates it + * with the types in the _types array. + * + * @param {function} _constructor constructor + * @param {array} _types widget types _constructor wants to register for + */ +export function et2_register_widget(_constructor, _types) +{ + "use strict"; + + et2_attribute_registry[_constructor.name] = ClassWithAttributes.buildAttributes(_constructor); + // Iterate over all given types and register those + for (var i = 0; i < _types.length; i++) + { + var type = _types[i].toLowerCase(); + + // Check whether a widget has already been registered for one of the + // types. + if (et2_registry[type]) + { + egw.debug("warn", "Widget class registered for " + type + + " will be overwritten."); + } + + et2_registry[type] = _constructor; + } +} + +/** + * Creates a widget registered for the given tag-name. If "readonly" is listed + * inside the attributes, et2_createWidget will try to use the "_ro" type of the + * widget. + * + * @param _name is the name of the widget with which it is registered. If the + * widget is not found, an et2_placeholder will be created. + * @param _attrs is an associative array with attributes. If not passed, it will + * default to an empty object. + * @param _parent is the parent to which the element will be attached. If _parent + * is not passed, it will default to null. Then you have to attach the element + * to a parent using the addChild or insertChild method. + */ +export function et2_createWidget(_name : string, _attrs : object, _parent? : any) : et2_widget +{ + "use strict"; + + if (typeof _attrs == "undefined") + { + _attrs = {}; + } + + if (typeof _attrs != "object") + { + _attrs = {}; + } + + if (typeof _parent == "undefined") + { + _parent = null; + } + + // Parse the "readonly" and "type" flag for this element here, as they + // determine which constructor is used + var nodeName = _attrs["type"] = _name; + var readonly = _attrs["readonly"] = + typeof _attrs["readonly"] == "undefined" ? false : _attrs["readonly"]; + + // Get the constructor - if the widget is readonly, use the special "_ro" + // constructor if it is available + 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 + ClassWithAttributes.generateAttributeSet(et2_attribute_registry[constructor.name], _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: string[] = []; + + private _type: string; + id: string; + supportedWidgetClasses : any[]; + options: WidgetConfig; + readonly: boolean; + + /** + * Widget constructor + * + * To implement the attributes inheritance and overriding each extending class/widget needs to call: + * + * super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_DOMWidget._attributes, _child || {})); + * + * @param _parent is the parent object from the XML tree which contains this + * object. The default constructor always adds the new instance to the + * children list of the given parent object. _parent may be NULL. + * @param _attrs is an associative array of attributes. + * @param _child attributes object from the child + */ + constructor(_parent?, _attrs? : WidgetConfig, _child? : object) + { + super(); // because we in the top of the widget hierarchy + this.attributes = ClassWithAttributes.extendAttributes(et2_widget._attributes, _child || {}); + + // Check whether all attributes are available + if (typeof _parent == "undefined") { + _parent = null; + } + + if (typeof _attrs == "undefined") { + _attrs = {}; + } + + if (_attrs.attributes) { + jQuery.extend(_attrs, _attrs.attributes); + } + // Initialize all important parameters + this._mgrs = {}; + this._inst = null; + this._children = []; + this._type = _attrs["type"]; + this.id = _attrs["id"]; + + // Add this widget to the given parent widget + if (_parent != null) { + _parent.addChild(this); + } + + // The supported widget classes array defines a whitelist for all widget + // classes or interfaces child widgets have to support. + this.supportedWidgetClasses = [et2_widget]; + + if (_attrs["id"]) { + // Create a namespace for this object + if (this._createNamespace()) { + this.checkCreateNamespace(_attrs); + } + } + + if (this.id) { + //this.id = this.id.replace(/\[/g,'[').replace(/]/g,']'); + } + + // Add all attributes hidden in the content arrays to the attributes + // parameter + this.transformAttributes(_attrs); + + // Create a local copy of the options object + this.options = et2_cloneObject(_attrs); + } + + /** + * The destroy function destroys all children of the widget, removes itself + * from the parents children list. + * In all classes derrived from et2_widget ALWAYS override the destroy + * function and remove ALL references to other objects. Also remember to + * unbind ANY event this widget created and to remove all DOM-Nodes it + * created. + */ + destroy() + { + // Call the destructor of all children + for (var i = this._children.length - 1; i >= 0; i--) { + this._children[i].destroy(); + } + + // Remove this element from the parent, if it exists + if (typeof this._parent != "undefined" && this._parent !== null) { + this._parent.removeChild(this); + } + + // Free the array managers if they belong to this widget + for (var key in this._mgrs) { + if (this._mgrs[key] && this._mgrs[key].owner == this) { + this._mgrs[key].destroy(); + } + } + } + + getType() : string + { + return this._type; + } + + setType(_type : string) + { + this._type = _type; + } + + /** + * Creates a copy of this widget. The parameters given are passed to the + * constructor of the copied object. If the parameters are omitted, _parent + * is defaulted to null + * + * @param {et2_widget} _parent parent to set for clone, default null + */ + clone(_parent) + { + // Default _parent to null + if (typeof _parent == "undefined") { + _parent = null; + } + + // Create the copy + var copy = new (this.constructor)(_parent, this.options); + + // Assign this element to the copy + copy.assign(this); + + return copy; + } + + assign(_obj) + { + if (typeof _obj._children == "undefined") { + this.egw().debug("log", "Foo!"); + } + + // Create a clone of all child elements of the given object + for (var i = 0; i < _obj._children.length; i++) { + _obj._children[i].clone(this); + } + + // Copy a reference to the content array manager + this.setArrayMgrs(_obj.mgrs); + } + + private _parent: et2_widget; + + /** + * Returns the parent widget of this widget + */ + getParent() : et2_widget | null + { + return this._parent; + } + + protected _children = []; + + /** + * Returns the list of children of this widget. + */ + getChildren() : et2_widget[] + { + return this._children; + } + + /** + * Returns the base widget + */ + getRoot() : et2_widget + { + if (this._parent != null) { + return this._parent.getRoot(); + } else { + return this; + } + } + + /** + * Inserts an child at the end of the list. + * + * @param _node is the node which should be added. It has to be an instance + * of et2_widget + */ + addChild(_node : et2_widget) + { + this.insertChild(_node, this._children.length); + } + + /** + * Inserts a child at the given index. + * + * @param _node is the node which should be added. It has to be an instance + * of et2_widget + * @param _idx is the position at which the element should be added. + */ + insertChild(_node : et2_widget, _idx: number) + { + // Check whether the node is one of the supported widget classes. + if (this.isOfSupportedWidgetClass(_node)) { + // Remove the node from its original parent + if (_node._parent) { + _node._parent.removeChild(_node); + } + + _node._parent = this; + this._children.splice(_idx, 0, _node); +/* +Comment this out (for now) + if (_node.implements(et2_IDOMNode) && this.implements(et2_IDOMNode) && typeof _node.hasOwnProperty('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) : et2_widget | null + { + if (this.id == _id) { + return this; + } + if (!this._children) return null; + + for (var i = 0; i < this._children.length; i++) { + var elem = this._children[i].getWidgetById(_id); + + if (elem != null) { + return elem; + } + } + if (this.id && _id.indexOf('[') > -1 && this._children.length) { + var ids = (new et2_arrayMgr()).explodeKey(_id); + var widget : et2_widget = this; + for (var i = 0; i < ids.length && widget !== null; i++) { + widget = widget.getWidgetById(ids[i]); + } + return widget; + } + + return null; + } + + /** + * Function which allows iterating over the complete widget tree. + * + * @param _callback is the function which should be called for each widget + * @param _context is the context in which the function should be executed + * @param _type is an optional parameter which specifies a class/interface + * the elements have to be instanceOf. + */ + iterateOver(_callback, _context, _type?) + { + if (typeof _type == "undefined") { + _type = et2_widget; + } + + if (this.isInTree() && this.instanceOf(_type)) { + _callback.call(_context, this); + } + + for (var i = 0; i < this._children.length; i++) { + this._children[i].iterateOver(_callback, _context, _type); + } + } + + /** + * Returns true if the widget currently resides in the visible part of the + * widget tree. E.g. Templates which have been cloned are not in the visible + * part of the widget tree. + * + * @param _sender + * @param {boolean} _vis can be used by widgets overwriting this function - simply + * write + * return this._super(inTree); + * when calling this function the _vis parameter does not have to be supplied. + */ + isInTree(_sender?, _vis? : boolean) + { + if (typeof _vis == "undefined") { + _vis = true; + } + + if (this._parent) { + return _vis && this._parent.isInTree(this); + } + + return _vis; + } + + isOfSupportedWidgetClass(_obj) + { + for (var i = 0; i < this.supportedWidgetClasses.length; i++) { + if (_obj instanceof this.supportedWidgetClasses[i]) { + return true; + } + } + return false; + } + + /** + * The parseXMLAttrs function takes an XML DOM attributes object + * and adds the given attributes to the _target associative array. This + * function also parses the legacyOptions. + * + * @param _attrsObj is the XML DOM attributes object + * @param {object} _target is the object to which the attributes should be written. + * @param {et2_widget} _proto prototype with attributes and legacyOptions attribute + */ + parseXMLAttrs(_attrsObj, _target, _proto) + { + // Check whether the attributes object is really existing, if not abort + if (typeof _attrsObj == "undefined") { + return; + } + + // Iterate over the given attributes and parse them + var mgr = this.getArrayMgr("content"); + for (var i = 0; i < _attrsObj.length; i++) { + var attrName = _attrsObj[i].name; + var attrValue = _attrsObj[i].value; + + // Special handling for the legacy options + if (attrName == "options" && _proto.constructor.legacyOptions && _proto.constructor.legacyOptions.length > 0) { + let legacy = _proto.constructor.legacyOptions || []; + let attrs = et2_attribute_registry[Object.getPrototypeOf(_proto).constructor.name] || {}; + // Check for modifications on legacy options here. Normal modifications + // are handled in widget constructor, but it's too late for legacy options then + if (_target.id && this.getArrayMgr("modifications").getEntry(_target.id)) { + var mod = this.getArrayMgr("modifications").getEntry(_target.id); + if (typeof mod.options != "undefined") attrValue = _attrsObj[i].value = mod.options; + } + // expand legacyOptions with content + if (attrValue.charAt(0) == '@' || attrValue.indexOf('$') != -1) { + attrValue = mgr.expandName(attrValue); + } + + // Parse the legacy options (as a string, other types not allowed) + var splitted = et2_csvSplit(attrValue + ""); + + for (var j = 0; j < splitted.length && j < legacy.length; j++) { + // Blank = not set, unless there's more legacy options provided after + if (splitted[j].trim().length === 0 && legacy.length >= splitted.length) continue; + + // Check to make sure we don't overwrite a current option with a legacy option + if (typeof _target[legacy[j]] === "undefined") { + attrValue = splitted[j]; + + /** + If more legacy options than expected, stuff them all in the last legacy option + Some legacy options take a comma separated list. + */ + if (j == legacy.length - 1 && splitted.length > legacy.length) { + attrValue = splitted.slice(j); + } + + var attr = et2_attribute_registry[_proto.constructor.name][legacy[j]] || {}; + + // If the attribute is marked as boolean, parse the + // expression as bool expression. + if (attr.type == "boolean") { + attrValue = mgr.parseBoolExpression(attrValue); + } else if (typeof attrValue != "object") { + attrValue = mgr.expandName(attrValue); + } + _target[legacy[j]] = attrValue; + } + } + } else if (attrName == "readonly" && typeof _target[attrName] != "undefined") { + // do NOT overwrite already evaluated readonly attribute + } else { + let attrs = et2_attribute_registry[_proto.constructor.name] || {}; + if (mgr != null && typeof attrs[attrName] != "undefined") { + var attr = attrs[attrName]; + + // If the attribute is marked as boolean, parse the + // expression as bool expression. + if (attr.type == "boolean") { + attrValue = mgr.parseBoolExpression(attrValue); + } else { + attrValue = mgr.expandName(attrValue); + } + } + + // Set the attribute + _target[attrName] = attrValue; + } + } + } + + /** + * Apply the "modifications" to the element and translate attributes marked + * with "translate: true" + * + * @param {object} _attrs + */ + transformAttributes(_attrs) + { + + // Apply the content of the modifications array + if (this.id) { + if (typeof this.id != "string") { + console.log(this.id); + } + + if (this.getArrayMgr("modifications")) { + var data = this.getArrayMgr("modifications").getEntry(this.id); + + // Check for already inside namespace + if (this._createNamespace() && this.getArrayMgr("modifications").perspectiveData.owner == this) { + data = this.getArrayMgr("modifications").data; + } + if (typeof data === 'object') { + for (var key in data) { + _attrs[key] = data[key]; + } + } + } + } + + // Translate the attributes + for (var key in _attrs) { + if (_attrs[key] && typeof this.attributes[key] != "undefined") { + if (this.attributes[key].translate === true || + (this.attributes[key].translate === "!no_lang" && !_attrs["no_lang"])) { + _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 + * @param _name XML node name + * + * @return et2_widget + */ + createElementFromNode(_node, _name?) + { + var attributes = {}; + + // Parse the "readonly" and "type" flag for this element here, as they + // determine which constructor is used + var _nodeName = attributes["type"] = _node.getAttribute("type") ? + _node.getAttribute("type") : _node.nodeName.toLowerCase(); + var readonly = attributes["readonly"] = this.getArrayMgr("readonlys") ? + this.getArrayMgr("readonlys").isReadOnly( + _node.getAttribute("id"), _node.getAttribute("readonly"), + typeof this.readonly !== 'undefined' ? this.readonly : this.options.readonly) : false; + + // Check to see if modifications change type + var modifications = this.getArrayMgr("modifications"); + if (modifications && _node.getAttribute("id")) { + var entry = 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 + ClassWithAttributes.generateAttributeSet(et2_attribute_registry[constructor.name], attributes); + + // Creates the new widget, passes this widget as an instance and + // passes the widgetType. Then it goes on loading the XML for it. + var widget = new constructor(this, attributes); + + // Load the widget itself from XML + widget.loadFromXML(_node); + + return widget; + } + + /** + * Loads the widget tree from an XML node + * + * @param _node xml node + */ + loadFromXML(_node) + { + // Load the child nodes. + for (var i = 0; i < _node.childNodes.length; i++) { + var node = _node.childNodes[i]; + var widgetType = node.nodeName.toLowerCase(); + + if (widgetType == "#comment") { + continue; + } + + if (widgetType == "#text") { + if (node.data.replace(/^\s+|\s+$/g, '')) { + this.loadContent(node.data); + } + continue; + } + + // Create the new element + this.createElementFromNode(node); + } + } + + /** + * Called whenever textNodes are loaded from the XML tree + * + * @param _content + */ + loadContent(_content) + { + } + + /** + * Called when loading the widget (sub-tree) is finished. First when this + * function is called, the DOM-Tree is created. loadingFinished is + * recursively called for all child elements. Do not directly override this + * function but the doLoadingFinished function which is executed before + * descending deeper into the DOM-Tree. + * + * Some widgets (template) do not load immediately because they request + * additional resources via AJAX. They will return a Deferred Promise object. + * If you call loadingFinished(promises) after creating such a widget + * programmatically, you might need to wait for it to fully complete its + * loading before proceeding. In that case use: + * + * 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: IegwAppLocal; + + /** + * The egw function returns the instance of the client side api belonging + * to this widget tree. The api instance can be set in the "container" + * widget using the setApiInstance function. + */ + egw() : IegwAppLocal + { + // The _egw property is not set + if (typeof this._egw === 'undefined') { + if (this._parent != null) { + return this._parent.egw(); + } + + // Get the window this object belongs to + var wnd = null; + if (this.implements(et2_IDOMNode)) { + var node = (this).getDOMNode(); + if (node && node.ownerDocument) { + wnd = node.ownerDocument.parentNode || node.ownerDocument.defaultView; + } + } + + // If we're the root object, return the phpgwapi API instance + return egw('phpgwapi', wnd); + } + + return this._egw; + } + + /** + * Sets the client side api instance. It can be retrieved by the widget tree + * by using the "egw()" function. + * + * @param {IegwAppLocal} _egw egw object to set + */ + setApiInstance(_egw : IegwAppLocal) + { + this._egw = _egw; + } + + protected _mgrs = {}; + + /** + * Sets all array manager objects - this function can be used to set the + * root array managers of the container object. + * + * @param {object} _mgrs + */ + setArrayMgrs(_mgrs) + { + this._mgrs = et2_cloneObject(_mgrs); + } + + /** + * Returns an associative array containing the top-most array managers. + * + * @param _mgrs is used internally and should not be supplied. + */ + getArrayMgrs(_mgrs? : object) + { + if (typeof _mgrs == "undefined") { + _mgrs = {}; + } + + // Add all managers of this object to the result, if they have not already + // been set in the result + for (var key in this._mgrs) { + if (typeof _mgrs[key] == "undefined") { + _mgrs[key] = this._mgrs[key]; + } + } + + // Recursively applies this function to the parent widget + if (this._parent) { + this._parent.getArrayMgrs(_mgrs); + } + + return _mgrs; + } + + /** + * Sets the array manager for the given part + * + * @param {string} _part which array mgr to set + * @param {object} _mgr + */ + setArrayMgr(_part : string, _mgr) + { + this._mgrs[_part] = _mgr; + } + + /** + * Returns the array manager object for the given part + * + * @param {string} managed_array_type name of array mgr to return + */ + getArrayMgr(managed_array_type : string) + { + if (this._mgrs && typeof this._mgrs[managed_array_type] != "undefined") { + return this._mgrs[managed_array_type]; + } else if (this._parent) { + return this._parent.getArrayMgr(managed_array_type); + } + + return null; + } + + /** + * Checks whether a namespace exists for this element in the content array. + * If yes, an own perspective of the content array is created. If not, the + * parent content manager is used. + * + * Constructor attributes are passed in case a child needs to make decisions + */ + checkCreateNamespace(_attrs? : any) + { + // Get the content manager + var mgrs = this.getArrayMgrs(); + + for (var key in mgrs) { + var mgr = mgrs[key]; + + // Get the original content manager if we have already created a + // perspective for this node + if (typeof this._mgrs[key] != "undefined" && mgr.perspectiveData.owner == this) { + mgr = mgr.parentMgr; + } + + // Check whether the manager has a namespace for the id of this object + var entry = mgr.getEntry(this.id); + if (typeof entry === 'object' && entry !== null || this.id) { + // The content manager has an own node for this object, so + // create an own perspective. + this._mgrs[key] = mgr.openPerspective(this, this.id); + } else { + // The current content manager does not have an own namespace for + // this element, so use the content manager of the parent. + delete (this._mgrs[key]); + } + } + } + + /** + * Widgets that do support a namespace should override and return true. + * + * Since a private attribute doesn't get instanciated properly before it's needed, + * we use a method so we can get what we need while still in the constructor. + * + * @private + */ + protected _createNamespace() : boolean + { + return false; + } + + /** + * This is used and therefore it we can not (yet) make it private + * + * @deprecated use this.getInstanceMgr() + */ + _inst = null; + + /** + * Sets the instance manager object (of type etemplate2, see etemplate2.js) + * + * @param {etemplate2} _inst + */ + setInstanceManager(_inst) + { + this._inst = _inst; + } + + /** + * Returns the instance manager + * + * @return {etemplate2} + */ + getInstanceManager() + { + if (this._inst != null) { + return this._inst; + } else if (this._parent) { + return this._parent.getInstanceManager(); + } + + return null; + } + + /** + * Returns the path into the data array. By default, array manager takes care of + * this, but some extensions need to override this + */ + getPath() + { + var path = this.getArrayMgr("content").getPath(); + + // Prevent namespaced widgets with value from going an extra layer deep + if (this.id && this._createNamespace() && path[path.length - 1] == this.id) path.pop(); + + return path; + } +} diff --git a/api/js/etemplate/et2_core_xml.js b/api/js/etemplate/et2_core_xml.js index be836ea850..7ac89a6a61 100644 --- a/api/js/etemplate/et2_core_xml.js +++ b/api/js/etemplate/et2_core_xml.js @@ -6,10 +6,7 @@ * @subpackage api * @link http://www.egroupware.org * @author Andreas Stöckel - * @copyright Stylite 2011 - * @version $Id$ */ - /** * Loads the given URL asynchronously from the server * @@ -21,84 +18,64 @@ * @param {object} _context for _callback * @param {function} _fail_callback function(_xml) */ -function et2_loadXMLFromURL(_url, _callback, _context, _fail_callback) -{ - if (typeof _context == "undefined") - { - _context = null; - } - - // use window object from main window with same algorithm as for the template cache - var win; - try { - if (opener && opener.etemplate2) - { - win = opener; - } - } - catch (e) { - // catch security exception if opener is from a different domain - } - if (typeof win == "undefined") - { - win = top; - } - win.jQuery.ajax({ - // we add the full url (protocol and domain) as sometimes just the path - // gives a CSP error interpreting it as file:///path - // (if there are a enough 404 errors in html content ...) - url: (_url[0]=='/' ? location.protocol+'//'+location.host : '')+_url, - context: _context, - type: 'GET', - dataType: 'xml', - success: function(_data, _status, _xmlhttp){ - _callback.call(_context, _data.documentElement); - }, - error: function(_xmlhttp, _err) { - egw().debug('error', 'Loading eTemplate from '+_url+' failed! '+_xmlhttp.status+' '+_xmlhttp.statusText); - if(typeof _fail_callback !== 'undefined') - { - _fail_callback.call(_context, _err); - } - } - }); +function et2_loadXMLFromURL(_url, _callback, _context, _fail_callback) { + if (typeof _context == "undefined") { + _context = null; + } + // use window object from main window with same algorithm as for the template cache + var win; + try { + if (opener && opener.etemplate2) { + win = opener; + } + } + catch (e) { + // catch security exception if opener is from a different domain + } + if (typeof win == "undefined") { + win = top; + } + win.jQuery.ajax({ + // we add the full url (protocol and domain) as sometimes just the path + // gives a CSP error interpreting it as file:///path + // (if there are a enough 404 errors in html content ...) + url: (_url[0] == '/' ? location.protocol + '//' + location.host : '') + _url, + context: _context, + type: 'GET', + dataType: 'xml', + success: function (_data, _status, _xmlhttp) { + _callback.call(_context, _data.documentElement); + }, + error: function (_xmlhttp, _err) { + egw().debug('error', 'Loading eTemplate from ' + _url + ' failed! ' + _xmlhttp.status + ' ' + _xmlhttp.statusText); + if (typeof _fail_callback !== 'undefined') { + _fail_callback.call(_context, _err); + } + } + }); } - -function et2_directChildrenByTagName(_node, _tagName) -{ - // Normalize the tag name - _tagName = _tagName.toLowerCase(); - - var result = []; - for (var i = 0; i < _node.childNodes.length; i++) - { - if (_tagName == _node.childNodes[i].nodeName.toLowerCase()) - { - result.push(_node.childNodes[i]); - } - } - - return result; +function et2_directChildrenByTagName(_node, _tagName) { + // Normalize the tag name + _tagName = _tagName.toLowerCase(); + var result = []; + for (var i = 0; i < _node.childNodes.length; i++) { + if (_tagName == _node.childNodes[i].nodeName.toLowerCase()) { + result.push(_node.childNodes[i]); + } + } + return result; } - -function et2_filteredNodeIterator(_node, _callback, _context) -{ - for (var i = 0; i < _node.childNodes.length; i++) - { - var node = _node.childNodes[i]; - var nodeName = node.nodeName.toLowerCase(); - if (nodeName.charAt(0) != "#") - { - _callback.call(_context, node, nodeName); - } - } +function et2_filteredNodeIterator(_node, _callback, _context) { + for (var i = 0; i < _node.childNodes.length; i++) { + var node = _node.childNodes[i]; + var nodeName = node.nodeName.toLowerCase(); + if (nodeName.charAt(0) != "#") { + _callback.call(_context, node, nodeName); + } + } } - -function et2_readAttrWithDefault(_node, _name, _default) -{ - var val = _node.getAttribute(_name); - - return (val === null) ? _default : val; +function et2_readAttrWithDefault(_node, _name, _default) { + var val = _node.getAttribute(_name); + return (val === null) ? _default : val; } - - +//# sourceMappingURL=et2_core_xml.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_core_xml.ts b/api/js/etemplate/et2_core_xml.ts new file mode 100644 index 0000000000..e569b1bc32 --- /dev/null +++ b/api/js/etemplate/et2_core_xml.ts @@ -0,0 +1,103 @@ +/** + * EGroupware eTemplate2 - JS XML Code + * + * @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 + */ + +/** + * Loads the given URL asynchronously from the server + * + * We make the Ajax call through main-windows jQuery object, to ensure cached copy + * in main-windows etemplate2 prototype works in IE too! + * + * @param {string} _url + * @param {function} _callback function(_xml) + * @param {object} _context for _callback + * @param {function} _fail_callback function(_xml) + */ +function et2_loadXMLFromURL(_url : string, _callback : Function, _context : object, _fail_callback : Function) +{ + if (typeof _context == "undefined") + { + _context = null; + } + + // use window object from main window with same algorithm as for the template cache + let win; + try { + if (opener && opener.etemplate2) + { + win = opener; + } + } + catch (e) { + // catch security exception if opener is from a different domain + } + if (typeof win == "undefined") + { + win = top; + } + win.jQuery.ajax({ + // we add the full url (protocol and domain) as sometimes just the path + // gives a CSP error interpreting it as file:///path + // (if there are a enough 404 errors in html content ...) + url: (_url[0]=='/' ? location.protocol+'//'+location.host : '')+_url, + context: _context, + type: 'GET', + dataType: 'xml', + success: function(_data, _status, _xmlhttp){ + _callback.call(_context, _data.documentElement); + }, + error: function(_xmlhttp, _err) { + egw().debug('error', 'Loading eTemplate from '+_url+' failed! '+_xmlhttp.status+' '+_xmlhttp.statusText); + if(typeof _fail_callback !== 'undefined') + { + _fail_callback.call(_context, _err); + } + } + }); +} + +function et2_directChildrenByTagName(_node : HTMLElement, _tagName : String) : HTMLElement[] +{ + // Normalize the tag name + _tagName = _tagName.toLowerCase(); + + let result = []; + for (let i = 0; i < _node.childNodes.length; i++) + { + if (_tagName == _node.childNodes[i].nodeName.toLowerCase()) + { + result.push(_node.childNodes[i]); + } + } + + return result; +} + +function et2_filteredNodeIterator(_node : HTMLElement, _callback : Function, _context : object) +{ + for (let i = 0; i < _node.childNodes.length; i++) + { + let node = _node.childNodes[i]; + let nodeName = node.nodeName.toLowerCase(); + if (nodeName.charAt(0) != "#") + { + _callback.call(_context, node, nodeName); + } + } +} + +function et2_readAttrWithDefault(_node : HTMLElement, _name : string, _default : string) : string +{ + let val = _node.getAttribute(_name); + + return (val === null) ? _default : val; +} + + + diff --git a/api/js/etemplate/et2_dataview.js b/api/js/etemplate/et2_dataview.js index 07ceeea7bc..17e0df8fc5 100644 --- a/api/js/etemplate/et2_dataview.js +++ b/api/js/etemplate/et2_dataview.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - dataview code * @@ -8,18 +9,22 @@ * @author Andreas Stöckel * @copyright Stylite 2011-2012 * @version $Id$ - */ + * /*egw:uses - /vendor/bower-asset/jquery/dist/jquery.js; - et2_core_common; + /vendor/bower-asset/jquery/dist/jquery.js; + et2_core_common; - et2_dataview_model_columns; - et2_dataview_view_rowProvider; - et2_dataview_view_grid; - et2_dataview_view_resizeable; + et2_dataview_model_columns; + et2_dataview_view_grid; + et2_dataview_view_rowProvider; + et2_dataview_view_resizeable; */ - +Object.defineProperty(exports, "__esModule", { value: true }); +var et2_dataview_model_columns_1 = require("./et2_dataview_model_columns"); +var et2_dataview_view_resizeable_1 = require("./et2_dataview_view_resizeable"); +var et2_dataview_view_grid_1 = require("./et2_dataview_view_grid"); +var et2_dataview_view_rowProvider_1 = require("./et2_dataview_view_rowProvider"); /** * The et2_dataview class is the main class for displaying a dataview. The * dataview class manages the creation of the outer html nodes (like the table, @@ -29,597 +34,450 @@ * * @augments Class */ -var et2_dataview = (function(){ "use strict"; return Class.extend({ - - /** - * Constant which regulates the column padding. - */ - columnPadding: 2, - - /** - * Some browser dependant variables which will be calculated on creation of - * the first gridContainer object. - */ - scrollbarWidth: false, - headerBorderWidth: false, - columnBorderWidth: false, - - /** - * Hooks to allow parent to keep up to date if things change - */ - onUpdateColumns: false, - selectColumnsClick: false, - - - /** - * Constructor for the grid container - * - * @param {DOMElement} _parentNode is the DOM-Node into which the grid view will be inserted - * @param {egw} _egw - * @memberOf et2_dataview - */ - init: function(_parentNode, _egw) { - - // Copy the arguments - this.parentNode = jQuery(_parentNode); - this.egw = _egw; - - // Initialize some variables - this.columnNodes = []; // Array with the header containers - this.columns = []; - this.columnMgr = null; - this.rowProvider = null; - - this.grid = null; - - this.width = 0; - this.height = 0; - - this.uniqueId = "gridCont_" + this.egw.uid(); - - // Build the base nodes - this._createElements(); - - // Read the browser dependant variables - this._getDepVars(); - }, - - /** - * Destroys the object, removes all dom nodes and clears all references. - */ - destroy: function() { - // Clear the columns - this._clearHeader(); - - // Free the grid - if (this.grid) - { - this.grid.free(); - } - - // Free the row provider - if (this.rowProvider) - { - this.rowProvider.free(); - } - - // Detatch the outer element - this.table.remove(); - }, - - /** - * Clears all data rows and reloads them - */ - clear: function() { - if (this.grid) - { - this.grid.clear(); - } - }, - - /** - * Returns the column container node for the given column index - * - * @param _columnIdx the integer column index - */ - getHeaderContainerNode: function(_columnIdx) { - if (typeof this.columnNodes[_columnIdx] != "undefined") - { - return this.columnNodes[_columnIdx].container[0]; - } - - return null; - }, - - /** - * Sets the column descriptors and creates the column header according to it. - * The inner grid will be emptied if it has already been built. - */ - setColumns: function(_columnData) { - // Free all column objects which have been created till this moment - this._clearHeader(); - - // Copy the given column data - this.columnMgr = new et2_dataview_columns(_columnData); - - // Create the stylesheets - this.updateColumns(); - - // Build the header row - this._buildHeader(); - - // Build the grid - this._buildGrid(); - }, - - /** - * Resizes the grid - */ - resize: function(_w, _h) { - // Not fully initialized yet... - if (!this.columnMgr) return; - - if (this.width != _w) - { - this.width = _w; - - // Take grid border width into account - _w -= (this.table.outerWidth(true) - this.table.innerWidth()); - - // Take grid header border's width into account. eg. category colors may add extra pixel into width - _w = _w - (this.thead.find('tr').outerWidth() - this.thead.find('tr').innerWidth()); - - // Rebuild the column stylesheets - this.columnMgr.setTotalWidth(_w - this.scrollbarWidth); - this._updateColumns(); - } - - if (this.height != _h) - { - this.height = _h; - - // Set the height of the grid. - if (this.grid) - { - this.grid.setScrollHeight(this.height - - this.headTr.outerHeight(true)); - } - } - }, - - /** - * Returns the column manager object. You can use it to set the visibility - * of columns etc. Call "updateHeader" if you did any changes. - */ - getColumnMgr: function() { - return this.columnMgr; - }, - - /** - * Recalculates the stylesheets which determine the column visibility and - * width. - * - * @param setDefault boolean Allow admins to save current settings as default for all users - */ - updateColumns: function(setDefault) { - if (this.columnMgr) - { - this._updateColumns(); - } - - // Ability to notify parent / someone else - if (this.onUpdateColumns) - { - this.onUpdateColumns(setDefault); - } - }, - - - /* --- PRIVATE FUNCTIONS --- */ - - /* --- Code for building the grid container DOM-Tree elements ---- */ - - /** - * Builds the base DOM-Tree elements - */ - _createElements: function() { - /* - Structure: - - - [HEAD] - - - [GRID CONTAINER] - -
- */ - - this.containerTr = jQuery(document.createElement("tr")); - this.headTr = jQuery(document.createElement("tr")); - - this.thead = jQuery(document.createElement("thead")) - .append(this.headTr); - this.tbody = jQuery(document.createElement("tbody")) - .append(this.containerTr); - - this.table = jQuery(document.createElement("table")) - .addClass("egwGridView_outer") - .append(this.thead, this.tbody) - .appendTo(this.parentNode); - }, - - - /* --- Code for building the header row --- */ - - /** - * Clears the header row - */ - _clearHeader: function() { - if (this.columnMgr) - { - this.columnMgr.free(); - this.columnMgr = null; - } - - // Remove dynamic CSS, - for (var i = 0; i < this.columns.length; i++) - { - if(this.columns[i].tdClass) - { - this.egw.css('.'+this.columns[i].tdClass); - } - if(this.columns[i].divClass) - { - this.egw.css('.'+this.columns[i].divClass); - this.egw.css(".egwGridView_outer ." + this.columns[i].divClass); - this.egw.css(".egwGridView_grid ." + this.columns[i].divClass); - } - } - this.egw.css(".egwGridView_grid ." + this.uniqueId + "_div_fullRow"); - this.egw.css(".egwGridView_outer ." + this.uniqueId + "_td_fullRow"); - this.egw.css(".egwGridView_outer ." + this.uniqueId + "_spacer_fullRow"); - - // Reset the headerColumns array and empty the table row - this.columnNodes = []; - this.columns = []; - this.headTr.empty(); - }, - - /** - * Sets the column data which is retrieved by calling egwGridColumns.getColumnData. - * The columns will be updated. - */ - _updateColumns: function() { - // Copy the columns data - this.columns = this.columnMgr.getColumnData(); - - // Count the visible rows - var total_cnt = 0; - for (var i = 0; i < this.columns.length; i++) - { - if (this.columns[i].visible) - { - total_cnt++; - } - } - - // Set the grid column styles - var first = true; - var vis_col = this.visibleColumnCount = 0; - var totalWidth = 0; - for (var i = 0; i < this.columns.length; i++) - { - var col = this.columns[i]; - - col.tdClass = this.uniqueId + "_td_" + col.id; - col.divClass = this.uniqueId + "_div_" + col.id; - - if (col.visible) - { - vis_col++; - this.visibleColumnCount++; - - // Update the visibility of the column - this.egw.css("." + col.tdClass, - "display: table-cell; " + - "!important;"); - - // Ugly browser dependant code - each browser seems to treat the - // right (collapsed) border of the row differently - var subBorder = 0; - var subHBorder = 0; - /* - if (jQuery.browser.mozilla) - { - var maj = jQuery.browser.version.split(".")[0]; - if (maj < 2) { - subBorder = 1; // Versions <= FF 3.6 - } - } - if (jQuery.browser.webkit) - { - if (!first) - { - subBorder = 1; - } - subHBorder = 1; - } - if ((jQuery.browser.msie || jQuery.browser.opera) && first) - { - subBorder = -1; - } - */ - - // Make the last columns one pixel smaller, to prevent a horizontal - // scrollbar from showing up - if (vis_col == total_cnt) - { - subBorder += 1; - } - - // Write the width of the header columns - var headerWidth = Math.max(0, (col.width - this.headerBorderWidth - subHBorder)); - this.egw.css(".egwGridView_outer ." + col.divClass, - "width: " + headerWidth + "px;"); - - // Write the width of the body-columns - var columnWidth = Math.max(0, (col.width - this.columnBorderWidth - subBorder)); - this.egw.css(".egwGridView_grid ." + col.divClass, - "width: " + columnWidth + "px;"); - - totalWidth += col.width; - - first = false; - } - else - { - this.egw.css("." + col.tdClass, "display: none;"); - } - } - - // Add the full row and spacer class - this.egw.css(".egwGridView_grid ." + this.uniqueId + "_div_fullRow", - "width: " + (totalWidth - this.columnBorderWidth - 2) + "px; border-right-width: 0 !important;"); - this.egw.css(".egwGridView_outer ." + this.uniqueId + "_td_fullRow", - "border-right-width: 0 !important;"); - this.egw.css(".egwGridView_outer ." + this.uniqueId + "_spacer_fullRow", - "width: " + (totalWidth - 1) + "px; border-right-width: 0 !important;"); - }, - - /** - * Builds the containers for the header row - */ - _buildHeader: function() { - var self = this; - var handler = function(event) { - }; - for (var i = 0; i < this.columns.length; i++) - { - var col = this.columns[i]; - - // Create the column header and the container element - var cont = jQuery(document.createElement("div")) - .addClass("innerContainer") - .addClass(col.divClass); - - var column = jQuery(document.createElement("th")) - .addClass(col.tdClass) - .attr("align", "left") - .append(cont) - .appendTo(this.headTr); - - if(this.columnMgr && this.columnMgr.columns[i]) - { - column.addClass(this.columnMgr.columns[i].fixedWidth ? 'fixedWidth' : 'relativeWidth'); - if(this.columnMgr.columns[i].visibility === ET2_COL_VISIBILITY_ALWAYS_NOSELECT) - { - column.addClass('noResize'); - } - } - - // make column resizable - var enc_column = self.columnMgr.getColumnById(col.id); - if(enc_column.visibility !== ET2_COL_VISIBILITY_ALWAYS_NOSELECT) - { - et2_dataview_makeResizeable(column, function(_w) { - - // User wants the column to stay where they put it, even for relative - // width columns, so set it explicitly first and adjust other relative - // columns to match. - if(this.relativeWidth) - { - // Set to selected width - this.set_width(_w + "px"); - self.columnMgr.updated = true; - // Just triggers recalculation - self.columnMgr.getColumnWidth(0); - - // Set relative widths to match - var relative = self.columnMgr.totalWidth - self.columnMgr.totalFixed + _w; - this.set_width(_w / relative); - for(var i = 0; i < self.columnMgr.columns.length; i++) - { - var col = self.columnMgr.columns[i]; - if(col == this || col.fixedWidth) continue; - col.set_width(self.columnMgr.columnWidths[i] / relative); - } - // Triggers column change callback, which saves - self.updateColumns(); - } - else - { - this.set_width(this.relativeWidth ? (_w / self.columnMgr.totalWidth) : _w + "px"); - self.columnMgr.updated = true; - self.updateColumns(); - } - - }, enc_column); - } - - // Store both nodes in the columnNodes array - this.columnNodes.push({ - "column": column, - "container": cont - }); - } - - this._buildSelectCol(); - }, - - /** - * Builds the select cols column - */ - _buildSelectCol: function() { - // Build the "select columns" icon - this.selectColIcon = jQuery(document.createElement("span")) - .addClass("selectcols") - .css('display', 'inline-block'); // otherwise jQuery('span.selectcols',this.dataview.headTr).show() set it to "inline" causing it to not show up because 0 height - - // Build the option column - this.selectCol = jQuery(document.createElement("th")) - .addClass("optcol") - .append(this.selectColIcon) - // Toggle display of option popup - .click(this, function(e) {if(e.data.selectColumnsClick) e.data.selectColumnsClick(e);}) - .appendTo(this.headTr); - - this.selectCol.css("width", this.scrollbarWidth - this.selectCol.outerWidth() - + this.selectCol.width() + 1); - }, - - /** - * Builds the inner grid class - */ - _buildGrid: function() { - // Create the collection of column ids - var colIds = new Array(this.columns.length); - for (var i = 0; i < this.columns.length; i++) - { - colIds[i] = this.columns[i].id; - } - - // Create the row provider - if (this.rowProvider) - { - this.rowProvider.free(); - } - - this.rowProvider = new et2_dataview_rowProvider(this.uniqueId, colIds); - - // Create the grid class and pass "19" as the starting average row height - this.grid = new et2_dataview_grid(null, null, this.egw, this.rowProvider, 19); - - // Insert the grid into the DOM-Tree - var tr = jQuery(this.grid._nodes[0]); - this.containerTr.replaceWith(tr); - this.containerTr = tr; - }, - - /* --- Code for calculating the browser/css depending widths --- */ - - /** - * Reads the browser dependant variables - */ - _getDepVars: function() { - if (this.scrollbarWidth === false) - { - // Clone the table and attach it to the outer body tag - var clone = this.table.clone(); - jQuery(window.top.document.getElementsByTagName("body")[0]) - .append(clone); - - // Read the scrollbar width - this.scrollbarWidth = this.constructor.prototype.scrollbarWidth = - this._getScrollbarWidth(clone); - - // Read the header border width - this.headerBorderWidth = this.constructor.prototype.headerBorderWidth = - this._getHeaderBorderWidth(clone); - - // Read the column border width - this.columnBorderWidth = this.constructor.prototype.columnBorderWidth = - this._getColumnBorderWidth(clone); - - // Remove the cloned DOM-Node again from the outer body - clone.remove(); - } - }, - - /** - * Reads the scrollbar width - */ - _getScrollbarWidth: function(_table) { - // Create a temporary td and two divs, which are inserted into the - // DOM-Tree. The outer div has a fixed size and "overflow" set to auto. - // When the second div is inserted, it will be forced to display a scrollbar. - var div_inner = jQuery(document.createElement("div")) - .css("height", "1000px"); - var div_outer = jQuery(document.createElement("div")) - .css("height", "100px") - .css("width", "100px") - .css("overflow", "auto") - .append(div_inner); - var td = jQuery(document.createElement("td")) - .append(div_outer); - - // Store the scrollbar width statically. - jQuery("tbody tr", _table).append(td); - var width = Math.max(10, div_outer.outerWidth() - div_inner.outerWidth()); - - // Remove the elements again - div_outer.remove(); - - return width; - }, - - /** - * Calculates the total width of the header column border - */ - _getHeaderBorderWidth: function(_table) { - // Create a temporary th which is appended to the outer thead row - var cont = jQuery(document.createElement("div")) - .addClass("innerContainer"); - - var th = jQuery(document.createElement("th")) - .append(cont); - - // Insert the th into the document tree - jQuery("thead tr", _table).append(th); - - // Calculate the total border width - var width = th.outerWidth(true) - cont.width(); - - // Remove the appended element again - th.remove(); - - return width; - }, - - /** - * Calculates the total width of the column border - */ - _getColumnBorderWidth : function(_table) { - // Create a temporary th which is appended to the outer thead row - var cont = jQuery(document.createElement("div")) - .addClass("innerContainer"); - - var td = jQuery(document.createElement("td")) - .append(cont); - - // Insert the th into the document tree - jQuery("tbody tr", _table).append(td); - - // Calculate the total border width - _table.addClass("egwGridView_grid"); - var width = td.outerWidth(true) - cont.width(); - - // Remove the appended element again - td.remove(); - - return width; - } - -});}).call(this); - - +var et2_dataview = /** @class */ (function () { + /** + * Constructor for the grid container + * + * @param {DOMElement} _parentNode is the DOM-Node into which the grid view will be inserted + * @param {egw} _egw + * @memberOf et2_dataview + */ + function et2_dataview(_parentNode, _egw) { + // Copy the arguments + this.parentNode = jQuery(_parentNode); + this.egw = _egw; + // Initialize some variables + this.columnNodes = []; // Array with the header containers + this.columns = []; + this.columnMgr = null; + this.rowProvider = null; + this.width = 0; + this.height = 0; + this.uniqueId = "gridCont_" + this.egw.uid(); + // Build the base nodes + this._createElements(); + // Read the browser dependant variables + this._getDepVars(); + } + /** + * Destroys the object, removes all dom nodes and clears all references. + */ + et2_dataview.prototype.destroy = function () { + // Clear the columns + this._clearHeader(); + // Free the grid + if (this.grid) { + this.grid.destroy(); + } + // Free the row provider + if (this.rowProvider) { + this.rowProvider.destroy(); + } + // Detatch the outer element + this.table.remove(); + }; + /** + * Clears all data rows and reloads them + */ + et2_dataview.prototype.clear = function () { + if (this.grid) { + this.grid.clear(); + } + }; + /** + * Returns the column container node for the given column index + * + * @param _columnIdx the integer column index + */ + et2_dataview.prototype.getHeaderContainerNode = function (_columnIdx) { + if (typeof this.columnNodes[_columnIdx] != "undefined") { + return this.columnNodes[_columnIdx].container[0]; + } + return null; + }; + /** + * Sets the column descriptors and creates the column header according to it. + * The inner grid will be emptied if it has already been built. + */ + et2_dataview.prototype.setColumns = function (_columnData) { + // Free all column objects which have been created till this moment + this._clearHeader(); + // Copy the given column data + this.columnMgr = new et2_dataview_model_columns_1.et2_dataview_columns(_columnData); + // Create the stylesheets + this.updateColumns(); + // Build the header row + this._buildHeader(); + // Build the grid + this._buildGrid(); + }; + /** + * Resizes the grid + */ + et2_dataview.prototype.resize = function (_w, _h) { + // Not fully initialized yet... + if (!this.columnMgr) + return; + if (this.width != _w) { + this.width = _w; + // Take grid border width into account + _w -= (this.table.outerWidth(true) - this.table.innerWidth()); + // Take grid header border's width into account. eg. category colors may add extra pixel into width + _w = _w - (this.thead.find('tr').outerWidth() - this.thead.find('tr').innerWidth()); + // Rebuild the column stylesheets + this.columnMgr.setTotalWidth(_w - this.scrollbarWidth); + this._updateColumns(); + } + if (this.height != _h) { + this.height = _h; + // Set the height of the grid. + if (this.grid) { + this.grid.setScrollHeight(this.height - + this.headTr.outerHeight(true)); + } + } + }; + /** + * Returns the column manager object. You can use it to set the visibility + * of columns etc. Call "updateHeader" if you did any changes. + */ + et2_dataview.prototype.getColumnMgr = function () { + return this.columnMgr; + }; + /** + * Recalculates the stylesheets which determine the column visibility and + * width. + * + * @param setDefault boolean Allow admins to save current settings as default for all users + */ + et2_dataview.prototype.updateColumns = function (setDefault) { + if (setDefault === void 0) { setDefault = false; } + if (this.columnMgr) { + this._updateColumns(); + } + // Ability to notify parent / someone else + if (this.onUpdateColumns) { + this.onUpdateColumns(setDefault); + } + }; + /* --- PRIVATE FUNCTIONS --- */ + /* --- Code for building the grid container DOM-Tree elements ---- */ + /** + * Builds the base DOM-Tree elements + */ + et2_dataview.prototype._createElements = function () { + /* + Structure: + + + [HEAD] + + + [GRID CONTAINER] + +
+ */ + this.containerTr = jQuery(document.createElement("tr")); + this.headTr = jQuery(document.createElement("tr")); + this.thead = jQuery(document.createElement("thead")) + .append(this.headTr); + this.tbody = jQuery(document.createElement("tbody")) + .append(this.containerTr); + this.table = jQuery(document.createElement("table")) + .addClass("egwGridView_outer") + .append(this.thead, this.tbody) + .appendTo(this.parentNode); + }; + /* --- Code for building the header row --- */ + /** + * Clears the header row + */ + et2_dataview.prototype._clearHeader = function () { + if (this.columnMgr) { + this.columnMgr.destroy(); + this.columnMgr = null; + } + // Remove dynamic CSS, + for (var i = 0; i < this.columns.length; i++) { + if (this.columns[i].tdClass) { + this.egw.css('.' + this.columns[i].tdClass); + } + if (this.columns[i].divClass) { + this.egw.css('.' + this.columns[i].divClass); + this.egw.css(".egwGridView_outer ." + this.columns[i].divClass); + this.egw.css(".egwGridView_grid ." + this.columns[i].divClass); + } + } + this.egw.css(".egwGridView_grid ." + this.uniqueId + "_div_fullRow"); + this.egw.css(".egwGridView_outer ." + this.uniqueId + "_td_fullRow"); + this.egw.css(".egwGridView_outer ." + this.uniqueId + "_spacer_fullRow"); + // Reset the headerColumns array and empty the table row + this.columnNodes = []; + this.columns = []; + this.headTr.empty(); + }; + /** + * Sets the column data which is retrieved by calling egwGridColumns.getColumnData. + * The columns will be updated. + */ + et2_dataview.prototype._updateColumns = function () { + // Copy the columns data + this.columns = this.columnMgr.getColumnData(); + // Count the visible rows + var total_cnt = 0; + for (var i = 0; i < this.columns.length; i++) { + if (this.columns[i].visible) { + total_cnt++; + } + } + // Set the grid column styles + var first = true; + var vis_col = this.visibleColumnCount = 0; + var totalWidth = 0; + for (var i = 0; i < this.columns.length; i++) { + var col = this.columns[i]; + col.tdClass = this.uniqueId + "_td_" + col.id; + col.divClass = this.uniqueId + "_div_" + col.id; + if (col.visible) { + vis_col++; + this.visibleColumnCount++; + // Update the visibility of the column + this.egw.css("." + col.tdClass, "display: table-cell; " + + "!important;"); + // Ugly browser dependant code - each browser seems to treat the + // right (collapsed) border of the row differently + var subBorder = 0; + var subHBorder = 0; + /* + if (jQuery.browser.mozilla) + { + var maj = jQuery.browser.version.split(".")[0]; + if (maj < 2) { + subBorder = 1; // Versions <= FF 3.6 + } + } + if (jQuery.browser.webkit) + { + if (!first) + { + subBorder = 1; + } + subHBorder = 1; + } + if ((jQuery.browser.msie || jQuery.browser.opera) && first) + { + subBorder = -1; + } + */ + // Make the last columns one pixel smaller, to prevent a horizontal + // scrollbar from showing up + if (vis_col == total_cnt) { + subBorder += 1; + } + // Write the width of the header columns + var headerWidth = Math.max(0, (col.width - this.headerBorderWidth - subHBorder)); + this.egw.css(".egwGridView_outer ." + col.divClass, "width: " + headerWidth + "px;"); + // Write the width of the body-columns + var columnWidth = Math.max(0, (col.width - this.columnBorderWidth - subBorder)); + this.egw.css(".egwGridView_grid ." + col.divClass, "width: " + columnWidth + "px;"); + totalWidth += col.width; + first = false; + } + else { + this.egw.css("." + col.tdClass, "display: none;"); + } + } + // Add the full row and spacer class + this.egw.css(".egwGridView_grid ." + this.uniqueId + "_div_fullRow", "width: " + (totalWidth - this.columnBorderWidth - 2) + "px; border-right-width: 0 !important;"); + this.egw.css(".egwGridView_outer ." + this.uniqueId + "_td_fullRow", "border-right-width: 0 !important;"); + this.egw.css(".egwGridView_outer ." + this.uniqueId + "_spacer_fullRow", "width: " + (totalWidth - 1) + "px; border-right-width: 0 !important;"); + }; + /** + * Builds the containers for the header row + */ + et2_dataview.prototype._buildHeader = function () { + var self = this; + var handler = function (event) { + }; + for (var i = 0; i < this.columns.length; i++) { + var col = this.columns[i]; + // Create the column header and the container element + var cont = jQuery(document.createElement("div")) + .addClass("innerContainer") + .addClass(col.divClass); + var column = jQuery(document.createElement("th")) + .addClass(col.tdClass) + .attr("align", "left") + .append(cont) + .appendTo(this.headTr); + if (this.columnMgr && this.columnMgr.getColumnById(i)) { + column.addClass(this.columnMgr.getColumnById(i).fixedWidth ? 'fixedWidth' : 'relativeWidth'); + if (this.columnMgr.getColumnById(i).visibility === et2_dataview_column.ET2_COL_VISIBILITY_ALWAYS_NOSELECT) { + column.addClass('noResize'); + } + } + // make column resizable + var enc_column = self.columnMgr.getColumnById(col.id); + if (enc_column.visibility !== et2_dataview_column.ET2_COL_VISIBILITY_ALWAYS_NOSELECT) { + et2_dataview_view_resizeable_1.et2_dataview_view_resizable.makeResizeable(column, function (_w) { + // User wants the column to stay where they put it, even for relative + // width columns, so set it explicitly first and adjust other relative + // columns to match. + if (this.relativeWidth) { + // Set to selected width + this.set_width(_w + "px"); + self.columnMgr.updated(); + // Just triggers recalculation + self.columnMgr.getColumnWidth(0); + // Set relative widths to match + var relative = self.columnMgr.totalWidth - self.columnMgr.totalFixed + _w; + this.set_width(_w / relative); + for (var i = 0; i < self.columnMgr.columnCount(); i++) { + var col = self.columnMgr.getColumnById(i); + if (col == this || col.fixedWidth) + continue; + col.set_width(self.columnMgr.getColumnWidth(i) / relative); + } + // Triggers column change callback, which saves + self.updateColumns(); + } + else { + this.set_width(this.relativeWidth ? (_w / self.columnMgr.totalWidth) : _w + "px"); + self.columnMgr.updated(); + self.updateColumns(); + } + }, enc_column); + } + // Store both nodes in the columnNodes array + this.columnNodes.push({ + "column": column, + "container": cont + }); + } + this._buildSelectCol(); + }; + /** + * Builds the select cols column + */ + et2_dataview.prototype._buildSelectCol = function () { + // Build the "select columns" icon + this.selectColIcon = jQuery(document.createElement("span")) + .addClass("selectcols") + .css('display', 'inline-block'); // otherwise jQuery('span.selectcols',this.dataview.headTr).show() set it to "inline" causing it to not show up because 0 height + // Build the option column + this.selectCol = jQuery(document.createElement("th")) + .addClass("optcol") + .append(this.selectColIcon) + // Toggle display of option popup + .click(this, function (e) { if (e.data.selectColumnsClick) + e.data.selectColumnsClick(e); }) + .appendTo(this.headTr); + this.selectCol.css("width", this.scrollbarWidth - this.selectCol.outerWidth() + + this.selectCol.width() + 1); + }; + /** + * Builds the inner grid class + */ + et2_dataview.prototype._buildGrid = function () { + // Create the collection of column ids + var colIds = new Array(this.columns.length); + for (var i = 0; i < this.columns.length; i++) { + colIds[i] = this.columns[i].id; + } + // Create the row provider + if (this.rowProvider) { + this.rowProvider.destroy(); + } + this.rowProvider = new et2_dataview_view_rowProvider_1.et2_dataview_rowProvider(this.uniqueId, colIds); + // Create the grid class and pass "19" as the starting average row height + this.grid = new et2_dataview_view_grid_1.et2_dataview_grid(null, null, this.egw, this.rowProvider, 19); + // Insert the grid into the DOM-Tree + var tr = jQuery(this.grid.getFirstNode()); + this.containerTr.replaceWith(tr); + this.containerTr = tr; + }; + /* --- Code for calculating the browser/css depending widths --- */ + /** + * Reads the browser dependant variables + */ + et2_dataview.prototype._getDepVars = function () { + if (typeof this.scrollbarWidth === 'undefined') { + // Clone the table and attach it to the outer body tag + var clone = this.table.clone(); + jQuery(window.top.document.getElementsByTagName("body")[0]) + .append(clone); + // Read the scrollbar width + this.scrollbarWidth = this.constructor.prototype.scrollbarWidth = + this._getScrollbarWidth(clone); + // Read the header border width + this.headerBorderWidth = this.constructor.prototype.headerBorderWidth = + this._getHeaderBorderWidth(clone); + // Read the column border width + this.columnBorderWidth = this.constructor.prototype.columnBorderWidth = + this._getColumnBorderWidth(clone); + // Remove the cloned DOM-Node again from the outer body + clone.remove(); + } + }; + /** + * Reads the scrollbar width + */ + et2_dataview.prototype._getScrollbarWidth = function (_table) { + // Create a temporary td and two divs, which are inserted into the + // DOM-Tree. The outer div has a fixed size and "overflow" set to auto. + // When the second div is inserted, it will be forced to display a scrollbar. + var div_inner = jQuery(document.createElement("div")) + .css("height", "1000px"); + var div_outer = jQuery(document.createElement("div")) + .css("height", "100px") + .css("width", "100px") + .css("overflow", "auto") + .append(div_inner); + var td = jQuery(document.createElement("td")) + .append(div_outer); + // Store the scrollbar width statically. + jQuery("tbody tr", _table).append(td); + var width = Math.max(10, div_outer.outerWidth() - div_inner.outerWidth()); + // Remove the elements again + div_outer.remove(); + return width; + }; + /** + * Calculates the total width of the header column border + */ + et2_dataview.prototype._getHeaderBorderWidth = function (_table) { + // Create a temporary th which is appended to the outer thead row + var cont = jQuery(document.createElement("div")) + .addClass("innerContainer"); + var th = jQuery(document.createElement("th")) + .append(cont); + // Insert the th into the document tree + jQuery("thead tr", _table).append(th); + // Calculate the total border width + var width = th.outerWidth(true) - cont.width(); + // Remove the appended element again + th.remove(); + return width; + }; + /** + * Calculates the total width of the column border + */ + et2_dataview.prototype._getColumnBorderWidth = function (_table) { + // Create a temporary th which is appended to the outer thead row + var cont = jQuery(document.createElement("div")) + .addClass("innerContainer"); + var td = jQuery(document.createElement("td")) + .append(cont); + // Insert the th into the document tree + jQuery("tbody tr", _table).append(td); + // Calculate the total border width + _table.addClass("egwGridView_grid"); + var width = td.outerWidth(true) - cont.width(); + // Remove the appended element again + td.remove(); + return width; + }; + return et2_dataview; +}()); +exports.et2_dataview = et2_dataview; +//# sourceMappingURL=et2_dataview.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_dataview.ts b/api/js/etemplate/et2_dataview.ts new file mode 100644 index 0000000000..aa22248be2 --- /dev/null +++ b/api/js/etemplate/et2_dataview.ts @@ -0,0 +1,669 @@ +/** + * EGroupware eTemplate2 - dataview code + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage dataview + * @link http://www.egroupware.org + * @author Andreas Stöckel + * @copyright Stylite 2011-2012 + * @version $Id$ + * + +/*egw:uses + /vendor/bower-asset/jquery/dist/jquery.js; + et2_core_common; + + et2_dataview_model_columns; + et2_dataview_view_grid; + et2_dataview_view_rowProvider; + et2_dataview_view_resizeable; +*/ + +import {et2_dataview_columns} from './et2_dataview_model_columns'; +import {et2_dataview_view_resizable} from "./et2_dataview_view_resizeable"; +import {et2_dataview_grid} from "./et2_dataview_view_grid"; +import {et2_dataview_rowProvider} from "./et2_dataview_view_rowProvider" + +/** + * The et2_dataview class is the main class for displaying a dataview. The + * dataview class manages the creation of the outer html nodes (like the table, + * header, etc.) and contains the root container: an instance of + * et2_dataview_view_grid, which can be accessed using the "grid" property of + * this object. + * + * @augments Class + */ +export class et2_dataview +{ + + /** + * Constant which regulates the column padding. + */ + columnPadding: number; + + /** + * Some browser dependant variables which will be calculated on creation of + * the first gridContainer object. + */ + scrollbarWidth: number; + headerBorderWidth: number; + columnBorderWidth: number; + + private width: number; + private height: number; + + private uniqueId: string; + + /** + * Hooks to allow parent to keep up to date if things change + */ + onUpdateColumns: Function; + selectColumnsClick: Function; + + private parentNode: JQuery; + egw: any; + + private columnNodes: any[]; + private columns: any[]; + private columnMgr: et2_dataview_columns; + private rowProvider: et2_dataview_rowProvider; + + private grid: et2_dataview_grid; + + // DOM stuff + private selectColIcon: JQuery; + private headTr: any; + private containerTr: JQuery; + private selectCol: JQuery; + private thead: JQuery; + private tbody: JQuery; + private table: JQuery; + private visibleColumnCount: number; + + + /** + * Constructor for the grid container + * + * @param {DOMElement} _parentNode is the DOM-Node into which the grid view will be inserted + * @param {egw} _egw + * @memberOf et2_dataview + */ + constructor(_parentNode, _egw) { + + // Copy the arguments + this.parentNode = jQuery(_parentNode); + this.egw = _egw; + + // Initialize some variables + this.columnNodes = []; // Array with the header containers + this.columns = []; + this.columnMgr = null; + this.rowProvider = null; + + this.width = 0; + this.height = 0; + + this.uniqueId = "gridCont_" + this.egw.uid(); + + // Build the base nodes + this._createElements(); + + // Read the browser dependant variables + this._getDepVars(); + } + + /** + * Destroys the object, removes all dom nodes and clears all references. + */ + destroy() + { + // Clear the columns + this._clearHeader(); + + // Free the grid + if (this.grid) + { + this.grid.destroy(); + } + + // Free the row provider + if (this.rowProvider) + { + this.rowProvider.destroy(); + } + + // Detatch the outer element + this.table.remove(); + } + + /** + * Clears all data rows and reloads them + */ + clear() + { + if (this.grid) + { + this.grid.clear(); + } + } + + /** + * Returns the column container node for the given column index + * + * @param _columnIdx the integer column index + */ + getHeaderContainerNode(_columnIdx) + { + if (typeof this.columnNodes[_columnIdx] != "undefined") + { + return this.columnNodes[_columnIdx].container[0]; + } + + return null; + } + + /** + * Sets the column descriptors and creates the column header according to it. + * The inner grid will be emptied if it has already been built. + */ + setColumns(_columnData) + { + // Free all column objects which have been created till this moment + this._clearHeader(); + + // Copy the given column data + this.columnMgr = new et2_dataview_columns(_columnData); + + // Create the stylesheets + this.updateColumns(); + + // Build the header row + this._buildHeader(); + + // Build the grid + this._buildGrid(); + } + + /** + * Resizes the grid + */ + resize(_w: number, _h: number) + { + // Not fully initialized yet... + if (!this.columnMgr) return; + + if (this.width != _w) + { + this.width = _w; + + // Take grid border width into account + _w -= (this.table.outerWidth(true) - this.table.innerWidth()); + + // Take grid header border's width into account. eg. category colors may add extra pixel into width + _w = _w - (this.thead.find('tr').outerWidth() - this.thead.find('tr').innerWidth()); + + // Rebuild the column stylesheets + this.columnMgr.setTotalWidth(_w - this.scrollbarWidth); + this._updateColumns(); + } + + if (this.height != _h) + { + this.height = _h; + + // Set the height of the grid. + if (this.grid) + { + this.grid.setScrollHeight(this.height - + this.headTr.outerHeight(true)); + } + } + } + + /** + * Returns the column manager object. You can use it to set the visibility + * of columns etc. Call "updateHeader" if you did any changes. + */ + getColumnMgr() { + return this.columnMgr; + } + + /** + * Recalculates the stylesheets which determine the column visibility and + * width. + * + * @param setDefault boolean Allow admins to save current settings as default for all users + */ + updateColumns(setDefault : boolean = false) + { + if (this.columnMgr) + { + this._updateColumns(); + } + + // Ability to notify parent / someone else + if (this.onUpdateColumns) + { + this.onUpdateColumns(setDefault); + } + } + + + /* --- PRIVATE FUNCTIONS --- */ + + /* --- Code for building the grid container DOM-Tree elements ---- */ + + /** + * Builds the base DOM-Tree elements + */ + private _createElements() + { + /* + Structure: + + + [HEAD] + + + [GRID CONTAINER] + +
+ */ + + this.containerTr = jQuery(document.createElement("tr")); + this.headTr = jQuery(document.createElement("tr")); + + this.thead = jQuery(document.createElement("thead")) + .append(this.headTr); + this.tbody = jQuery(document.createElement("tbody")) + .append(this.containerTr); + + this.table = jQuery(document.createElement("table")) + .addClass("egwGridView_outer") + .append(this.thead, this.tbody) + .appendTo(this.parentNode); + } + + + /* --- Code for building the header row --- */ + + /** + * Clears the header row + */ + private _clearHeader () + { + if (this.columnMgr) + { + this.columnMgr.destroy(); + this.columnMgr = null; + } + + // Remove dynamic CSS, + for (var i = 0; i < this.columns.length; i++) + { + if(this.columns[i].tdClass) + { + this.egw.css('.'+this.columns[i].tdClass); + } + if(this.columns[i].divClass) + { + this.egw.css('.'+this.columns[i].divClass); + this.egw.css(".egwGridView_outer ." + this.columns[i].divClass); + this.egw.css(".egwGridView_grid ." + this.columns[i].divClass); + } + } + this.egw.css(".egwGridView_grid ." + this.uniqueId + "_div_fullRow"); + this.egw.css(".egwGridView_outer ." + this.uniqueId + "_td_fullRow"); + this.egw.css(".egwGridView_outer ." + this.uniqueId + "_spacer_fullRow"); + + // Reset the headerColumns array and empty the table row + this.columnNodes = []; + this.columns = []; + this.headTr.empty(); + } + + /** + * Sets the column data which is retrieved by calling egwGridColumns.getColumnData. + * The columns will be updated. + */ + private _updateColumns() + { + // Copy the columns data + this.columns = this.columnMgr.getColumnData(); + + // Count the visible rows + var total_cnt = 0; + for (var i = 0; i < this.columns.length; i++) + { + if (this.columns[i].visible) + { + total_cnt++; + } + } + + // Set the grid column styles + var first = true; + var vis_col = this.visibleColumnCount = 0; + var totalWidth = 0; + for (var i = 0; i < this.columns.length; i++) + { + var col = this.columns[i]; + + col.tdClass = this.uniqueId + "_td_" + col.id; + col.divClass = this.uniqueId + "_div_" + col.id; + + if (col.visible) + { + vis_col++; + this.visibleColumnCount++; + + // Update the visibility of the column + this.egw.css("." + col.tdClass, + "display: table-cell; " + + "!important;"); + + // Ugly browser dependant code - each browser seems to treat the + // right (collapsed) border of the row differently + var subBorder = 0; + var subHBorder = 0; + /* + if (jQuery.browser.mozilla) + { + var maj = jQuery.browser.version.split(".")[0]; + if (maj < 2) { + subBorder = 1; // Versions <= FF 3.6 + } + } + if (jQuery.browser.webkit) + { + if (!first) + { + subBorder = 1; + } + subHBorder = 1; + } + if ((jQuery.browser.msie || jQuery.browser.opera) && first) + { + subBorder = -1; + } + */ + + // Make the last columns one pixel smaller, to prevent a horizontal + // scrollbar from showing up + if (vis_col == total_cnt) + { + subBorder += 1; + } + + // Write the width of the header columns + var headerWidth = Math.max(0, (col.width - this.headerBorderWidth - subHBorder)); + this.egw.css(".egwGridView_outer ." + col.divClass, + "width: " + headerWidth + "px;"); + + // Write the width of the body-columns + var columnWidth = Math.max(0, (col.width - this.columnBorderWidth - subBorder)); + this.egw.css(".egwGridView_grid ." + col.divClass, + "width: " + columnWidth + "px;"); + + totalWidth += col.width; + + first = false; + } + else + { + this.egw.css("." + col.tdClass, "display: none;"); + } + } + + // Add the full row and spacer class + this.egw.css(".egwGridView_grid ." + this.uniqueId + "_div_fullRow", + "width: " + (totalWidth - this.columnBorderWidth - 2) + "px; border-right-width: 0 !important;"); + this.egw.css(".egwGridView_outer ." + this.uniqueId + "_td_fullRow", + "border-right-width: 0 !important;"); + this.egw.css(".egwGridView_outer ." + this.uniqueId + "_spacer_fullRow", + "width: " + (totalWidth - 1) + "px; border-right-width: 0 !important;"); + } + + /** + * Builds the containers for the header row + */ + private _buildHeader() + { + var self = this; + var handler = function(event) { + }; + for (var i = 0; i < this.columns.length; i++) + { + var col = this.columns[i]; + + // Create the column header and the container element + var cont = jQuery(document.createElement("div")) + .addClass("innerContainer") + .addClass(col.divClass); + + var column = jQuery(document.createElement("th")) + .addClass(col.tdClass) + .attr("align", "left") + .append(cont) + .appendTo(this.headTr); + + if(this.columnMgr && this.columnMgr.getColumnById(i)) + { + column.addClass(this.columnMgr.getColumnById(i).fixedWidth ? 'fixedWidth' : 'relativeWidth'); + if(this.columnMgr.getColumnById(i).visibility === et2_dataview_column.ET2_COL_VISIBILITY_ALWAYS_NOSELECT) + { + column.addClass('noResize'); + } + } + + // make column resizable + var enc_column = self.columnMgr.getColumnById(col.id); + if(enc_column.visibility !== et2_dataview_column.ET2_COL_VISIBILITY_ALWAYS_NOSELECT) + { + et2_dataview_view_resizable.makeResizeable(column, function(_w) { + + // User wants the column to stay where they put it, even for relative + // width columns, so set it explicitly first and adjust other relative + // columns to match. + if(this.relativeWidth) + { + // Set to selected width + this.set_width(_w + "px"); + self.columnMgr.updated(); + // Just triggers recalculation + self.columnMgr.getColumnWidth(0); + + // Set relative widths to match + var relative = self.columnMgr.totalWidth - self.columnMgr.totalFixed + _w; + this.set_width(_w / relative); + for(var i = 0; i < self.columnMgr.columnCount(); i++) + { + var col = self.columnMgr.getColumnById(i); + if(col == this || col.fixedWidth) continue; + col.set_width(self.columnMgr.getColumnWidth(i) / relative); + } + // Triggers column change callback, which saves + self.updateColumns(); + } + else + { + this.set_width(this.relativeWidth ? (_w / self.columnMgr.totalWidth) : _w + "px"); + self.columnMgr.updated(); + self.updateColumns(); + } + + }, enc_column); + } + + // Store both nodes in the columnNodes array + this.columnNodes.push({ + "column": column, + "container": cont + }); + } + + this._buildSelectCol(); + } + + /** + * Builds the select cols column + */ + private _buildSelectCol() + { + // Build the "select columns" icon + this.selectColIcon = jQuery(document.createElement("span")) + .addClass("selectcols") + .css('display', 'inline-block'); // otherwise jQuery('span.selectcols',this.dataview.headTr).show() set it to "inline" causing it to not show up because 0 height + + // Build the option column + this.selectCol = jQuery(document.createElement("th")) + .addClass("optcol") + .append(this.selectColIcon) + // Toggle display of option popup + .click(this, function(e) {if(e.data.selectColumnsClick) e.data.selectColumnsClick(e);}) + .appendTo(this.headTr); + + this.selectCol.css("width", this.scrollbarWidth - this.selectCol.outerWidth() + + this.selectCol.width() + 1); + } + + /** + * Builds the inner grid class + */ + private _buildGrid() + { + // Create the collection of column ids + var colIds = new Array(this.columns.length); + for (var i = 0; i < this.columns.length; i++) + { + colIds[i] = this.columns[i].id; + } + + // Create the row provider + if (this.rowProvider) + { + this.rowProvider.destroy(); + } + + this.rowProvider = new et2_dataview_rowProvider(this.uniqueId, colIds); + + // Create the grid class and pass "19" as the starting average row height + this.grid = new et2_dataview_grid(null, null, this.egw, this.rowProvider, 19); + + // Insert the grid into the DOM-Tree + var tr = jQuery(this.grid.getFirstNode()); + this.containerTr.replaceWith(tr); + this.containerTr = tr; + } + + /* --- Code for calculating the browser/css depending widths --- */ + + /** + * Reads the browser dependant variables + */ + private _getDepVars() + { + if (typeof this.scrollbarWidth === 'undefined') + { + // Clone the table and attach it to the outer body tag + var clone = this.table.clone(); + jQuery(window.top.document.getElementsByTagName("body")[0]) + .append(clone); + + // Read the scrollbar width + this.scrollbarWidth = this.constructor.prototype.scrollbarWidth = + this._getScrollbarWidth(clone); + + // Read the header border width + this.headerBorderWidth = this.constructor.prototype.headerBorderWidth = + this._getHeaderBorderWidth(clone); + + // Read the column border width + this.columnBorderWidth = this.constructor.prototype.columnBorderWidth = + this._getColumnBorderWidth(clone); + + // Remove the cloned DOM-Node again from the outer body + clone.remove(); + } + } + + /** + * Reads the scrollbar width + */ + private _getScrollbarWidth(_table: JQuery) + { + // Create a temporary td and two divs, which are inserted into the + // DOM-Tree. The outer div has a fixed size and "overflow" set to auto. + // When the second div is inserted, it will be forced to display a scrollbar. + var div_inner = jQuery(document.createElement("div")) + .css("height", "1000px"); + var div_outer = jQuery(document.createElement("div")) + .css("height", "100px") + .css("width", "100px") + .css("overflow", "auto") + .append(div_inner); + var td = jQuery(document.createElement("td")) + .append(div_outer); + + // Store the scrollbar width statically. + jQuery("tbody tr", _table).append(td); + var width = Math.max(10, div_outer.outerWidth() - div_inner.outerWidth()); + + // Remove the elements again + div_outer.remove(); + + return width; + } + + /** + * Calculates the total width of the header column border + */ + private _getHeaderBorderWidth(_table: JQuery) + { + // Create a temporary th which is appended to the outer thead row + var cont = jQuery(document.createElement("div")) + .addClass("innerContainer"); + + var th = jQuery(document.createElement("th")) + .append(cont); + + // Insert the th into the document tree + jQuery("thead tr", _table).append(th); + + // Calculate the total border width + var width = th.outerWidth(true) - cont.width(); + + // Remove the appended element again + th.remove(); + + return width; + } + + /** + * Calculates the total width of the column border + */ + private _getColumnBorderWidth(_table: JQuery) + { + // Create a temporary th which is appended to the outer thead row + var cont = jQuery(document.createElement("div")) + .addClass("innerContainer"); + + var td = jQuery(document.createElement("td")) + .append(cont); + + // Insert the th into the document tree + jQuery("tbody tr", _table).append(td); + + // Calculate the total border width + _table.addClass("egwGridView_grid"); + var width = td.outerWidth(true) - cont.width(); + + // Remove the appended element again + td.remove(); + + return width; + } + +} + diff --git a/api/js/etemplate/et2_dataview_controller.js b/api/js/etemplate/et2_dataview_controller.js index 4587ccc3f9..87abcd7ce7 100644 --- a/api/js/etemplate/et2_dataview_controller.js +++ b/api/js/etemplate/et2_dataview_controller.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 * @@ -7,1052 +8,808 @@ * @link http://www.egroupware.org * @author Andreas Stöckel * @copyright Stylite 2011-2012 - * @version $Id$ - */ /*egw:uses - et2_core_common; - et2_core_inheritance; + et2_core_common; + et2_core_inheritance; - et2_dataview_interfaces; - et2_dataview_controller_selection; - et2_dataview_view_row; + et2_dataview_interfaces; + et2_dataview_controller_selection; + et2_dataview_view_row; + et2_dataview_view_tile; - egw_action.egw_action; + egw_action.egw_action; */ - +Object.defineProperty(exports, "__esModule", { value: true }); +var et2_dataview_controller_selection_1 = require("./et2_dataview_controller_selection"); +var et2_dataview_view_row_1 = require("./et2_dataview_view_row"); /** * The fetch timeout specifies the time during which the controller tries to * consolidate requests for rows. */ var ET2_DATAVIEW_FETCH_TIMEOUT = 50; - var ET2_DATAVIEW_STEPSIZE = 50; - /** * The et2_dataview_controller class is the intermediate layer between a grid * instance and the corresponding data source. It manages updating the grid, * as well as inserting and deleting rows. */ -var et2_dataview_controller = (function(){ "use strict"; return Class.extend({ - - // Maximum concurrent data requests. Additional ones are held in the queue. - CONCURRENT_REQUESTS: 5, - - /** - * Constructor of the et2_dataview_controller, connects to the grid - * callback. - * - * @param _grid is the grid the controller should controll. - * @param _dataProvider is an object implementing the et2_IDataProvider - * interface. - * @param _rowCallback is the callback function that gets called when a row - * is requested. - * @param _linkCallback is the callback function that gets called for - * requesting action links for a row. The row data, the index of the row and - * the uid are passed as parameters to the function. - * uid is passed to the function. - * @param _context is the context in which the _rowCallback and the - * _linkCallback are called. - * @param _actionObjectManager is the object that manages the action - * objects. - */ - init: function (_parentController, _grid, _dataProvider, _rowCallback, - _linkCallback, _context, _actionObjectManager) - { - // Copy the given arguments - this._parentController = _parentController; - this._grid = _grid; - this._dataProvider = _dataProvider; - this._rowCallback = _rowCallback; - this._linkCallback = _linkCallback; - this._context = _context; - - // Initialize list of child controllers - this._children = []; - - // Initialize the "index map" which contains all currently displayed - // containers hashed by the "index" - this._indexMap = {}; - - // Timer used for queing fetch requests - this._queueTimer = null; - - // Array which contains all currently queued row indices in the form of - // an associative array - this._queue = {}; - - // Current concurrent requests we have - this._request_queue = []; - - // Register the dataFetch callback - this._grid.setDataCallback(this._gridCallback, this); - - // Create the selection manager - this._selectionMgr = new et2_dataview_selectionManager( - this._parentController ? this._parentController._selectionMgr : null, - this._indexMap, - _actionObjectManager, - this._selectionFetchRange, - this._makeIndexVisible, - this - ); - - // Record the child - if(this._parentController != null) - { - this._parentController._children.push(this); - } - }, - - destroy: function () { - - // Destroy the selection manager - this._selectionMgr.free(); - - // Clear the selection timeout - this._clearTimer(); - - // Remove the child from the child list - if(this._parentController != null) - { - var idx = this._parentController._children.indexOf(this); - - if (idx >= 0) - { - // This element is no longer parent of the child - this._parentController._children.splice(idx, 1); - this._parentController = null; - } - } - }, - - /** - * The update function queries the server for changes in the currently - * managed index range -- those changes are then merged into the current - * view without a complete rebuild of every row. - * - * @param {boolean} clear Skip the fancy stuff, dump everything and start again. - * Completely clears the grid and selection. - */ - update: function (clear) { - - // --------- - - // TODO: Actually stuff here should be done if the server responds that - // there at all were some changes (needs implementation of "refresh") - - // Tell the grid not to try and update itself while we do this - this._grid.doInvalidate = false; - - if(clear) - { - // Scroll to top - this._grid.makeIndexVisible(0); - this._grid.clear(); - - // Free selection manager - this._selectionMgr.clear(); - - // Clear object manager - this._objectManager.clear(); - - // Clear the map - this._indexMap = {} - // Update selection manager, it uses this by reference - this._selectionMgr.setIndexMap(this._indexMap); - - // Clear the queue - this._queue = {}; - - // Invalidate the change detection, re-fetches any known rows - this._lastModification = 0; - } - // Remove all rows which are outside the view range - this._grid.cleanup(); - - // Get the currently visible range from the grid - var range = this._grid.getIndexRange(); - - // Force range.top and range.bottom to contain an integer - if (range.top === false) - { - range.top = range.bottom = 0; - } - this._request_queue = []; - - // Require that range from the server - this._queueFetch(et2_bounds(range.top, clear ? 0 : range.bottom + 1), 0, true); - }, - - /** - * Rebuilds the complete grid. - */ - reset: function () { - // Throw away all internal mappings and reset the timestamp - this._indexMap = {}; - // Update selection manager, it uses this by reference - this._selectionMgr.setIndexMap(this._indexMap); - - // Clear the grid - this._grid.clear(); - - // Clear the row queue - this._queue = {}; - - // Reset the request queue - this._request_queue = []; - - // Update the data - this.update(); - }, - - /** - * Loads the initial order. Do not call multiple times. - */ - loadInitialOrder: function (order) { - for (var i = 0; i < order.length; i++) - { - this._getIndexEntry(i).uid = order[i]; - } - }, - - /** - * Load initial data - * - * @param {string} uid_key Name of the unique row identifier field - * @param {Object} data Key / Value mapping of initial data. - */ - loadInitialData: function (uid_prefix, uid_key, data) { - var idx = 0; - for(var key in data) - { - // Skip any extra keys - if(typeof data[key] != "object" || data[key] == null || typeof data[key][uid_key] == "undefined") continue; - - // Add to row / uid map - var entry = this._getIndexEntry(idx++); - entry.uid = data[key][uid_key]+""; - if(entry.uid.indexOf(uid_prefix) < 0) - { - entry.uid = uid_prefix + "::" + entry.uid; - } - - // Add to data cache so grid will find it - egw.dataStoreUID(entry.uid, data[key]) - - // Don't try to insert the rows, grid will do that automatically - } - if(idx == 0) - { - // No rows, start with an empty - this._selectionMgr.clear(); - this._emptyRow(this._grid._total == 0); - } - }, - - /** - * Returns the depth of the controller instance. - */ - getDepth: function () { - - if (this._parentController) - { - return this._parentController.getDepth() + 1; - } - - return 0; - }, - - /** - * Set the data cache prefix - * The default is to use appname, but if you need to set it explicitly to - * something else to avoid conflicts. Use the same prefix everywhere for - * each type of data. eg. infolog for infolog entries, even if accessed via addressbook - */ - setPrefix: function(prefix) { - this.dataStorePrefix = prefix; - }, - - /** - * Returns the row information of the passed node, or null if not available - * - * @param {DOMNode} node - * @return {string|false} UID, or false if not found - */ - getRowByNode: function(node) { - // Whatever the node, find a TR - var row_node = jQuery(node).closest('tr'); - var row = false - - // Check index map - simple case - var indexed = this._getIndexEntry(row_node.index()); - if(indexed && indexed.row && indexed.row.getDOMNode() == row_node[0]) - { - row = indexed; - } - else - { - // Check whole index map - for(var index in this._indexMap) - { - indexed = this._indexMap[index]; - if( indexed && indexed.row && indexed.row.getDOMNode() == row_node[0]) - { - row = indexed; - break; - } - } - } - - // Check children - for(var i = 0; !row && i < this._children.length; i++) - { - var child_row = this._children[i].getRowByNode(node); - if(child_row !== false) row = child_row; - } - if(row && !row.controller) - { - row.controller = this; - } - return row; - }, - - /* -- PRIVATE FUNCTIONS -- */ - - - _getIndexEntry: function (_idx) { - // Create an entry in the index map if it does not exist yet - if (typeof this._indexMap[_idx] === "undefined") - { - this._indexMap[_idx] = { - "row": null, - "uid": null - }; - } - - // Always update the index of the entries before returning them. This is - // neccessary, as when we remove the uid from an entry without row, its - // index does not get updated any further - this._indexMap[_idx]["idx"] = _idx; - - return this._indexMap[_idx]; - }, - - /** - * Inserts a new data row into the grid. index and uid are derived from the - * given management entry. If the data for the given uid does not exist yet, - * a "loading" placeholder will be shown instead. The function will do - * nothing if there already is a row associated to the entry. This function - * will not re-insert a row if the entry already had a row. - * - * @param _entry is the management entry for the index the row will be - * displayed at. - * @param _update specifies whether the row should be updated if _entry.row - * already exists. - * @return true, if all data for the row has been available, false - * otherwise. - */ - _insertDataRow: function (_entry, _update) { - // Abort if the entry already has a row but the _insert flag is not set - if (_entry.row && !_update) - { - return true; - } - - // Context used for the callback functions - var ctx = {"self": this, "entry": _entry}; - - // Create a new row instance, if it does not exist yet - var createdRow = false; - if (!_entry.row) - { - createdRow = true; - _entry.row = this._createRow(ctx); - _entry.row.setDestroyCallback(this._destroyCallback, ctx); - } - - // Load the row data if we have a uid for the entry - this.hasData = false; // Gets updated by the _dataCallback - if (_entry.uid) - { - // Register the callback / immediately load the data - this._dataProvider.dataRegisterUID(_entry.uid, this._dataCallback, - ctx); - } - - // Display the loading "row prototype" if we don't have data for the row - if (!this.hasData) - { - // Get the average height, the "-5" derives from the td padding - var avg = Math.round(this._grid.getAverageHeight() - 5) + "px"; - var prototype = this._grid.getRowProvider().getPrototype("loading"); - jQuery("div", prototype).css("height", avg); - var node = _entry.row.getJNode(); - node.empty(); - node.append(prototype.children()); - } - - // Insert the row into the table -- the same row must never be inserted - // twice into the grid, so this function only executes the following - // code only if it is a newly created row. - if (createdRow && _entry.row) - { - this._grid.insertRow(_entry.idx, _entry.row); - } - - return this.hasData; - }, - - - /** - * Create a new row. - * - * @param {type} ctx - * @returns {et2_dataview_container} - */ - _createRow: function(ctx) { - return new et2_dataview_row(this._grid); - }, - - /** - * Function which gets called by the grid when data is requested. - * - * @param _idxStart is the index of the first row for which data is - * requested. - * @param _idxEnd is the index of the last requested row. - */ - _gridCallback: function (_idxStart, _idxEnd) { - - var needsData = false; - - // Iterate over all elements the dataview requested and create a row - // which indicates that we are currently loading data - for (var i = _idxStart; i <= _idxEnd; i++) - { - var entry = this._getIndexEntry(i); - - // Insert the row for the entry -- do not update rows which are - // already existing, as we do not have new data for those. - if (!this._insertDataRow(entry, false) && needsData === false) - { - needsData = i; - } - } - - // Queue fetching that data range - if (needsData !== false) - { - this._queueFetch(et2_bounds(needsData, _idxEnd + 1), needsData == _idxStart ? 0 : needsData > _idxStart ? 1 : -1, false); - } - }, - - /** - * The _queueFetch function is used to queue a fetch request. - * TODO: Refresh is currently not used - */ - _queueFetch: function (_range, _direction, _isUpdate) { - - // Force immediate to be false - _isUpdate = _isUpdate ? _isUpdate : false; - - // Push the requests onto the request queue - var start = Math.max(0, _range.top); - var end = Math.min(this._grid.getTotalCount(), _range.bottom); - for (var i = start; i < end; i++) - { - if (typeof this._queue[i] === "undefined") - { - this._queue[i] = _direction; // Stage 1 - queue for after current, -1 -- queue for before current - } - } - - // Start the queue timer, if this has not already been done - if (this._queueTimer === null && !_isUpdate) - { - var self = this; - egw.debug('log', 'Dataview queue: ', _range); - this._queueTimer = window.setTimeout(function () { - self._flushQueue(false); - }, ET2_DATAVIEW_FETCH_TIMEOUT); - } - - if (_isUpdate) - { - this._flushQueue(true); - } - }, - - /** - * Flushes the queue. - */ - _flushQueue: function (_isUpdate) { - - // Clear any still existing timer - this._clearTimer(); - - // Mark all elements in a radius of ET2_DATAVIEW_STEPSIZE - var marked = {}; - var r = _isUpdate ? 0 : Math.floor(ET2_DATAVIEW_STEPSIZE / 2); - var total = this._grid.getTotalCount(); - for (var key in this._queue) - { - if (this._queue[key] > 1) - continue; - - key = parseInt(key); - - var b = Math.max(0, key - r + (r * this._queue[key])); - var t = Math.min(key + r + (r * this._queue[key]), total - 1); - var c = 0; - for (var i = b; i <= t && c < ET2_DATAVIEW_STEPSIZE; i ++) - { - if (typeof this._queue[i] == "undefined" - || this._queue[i] <= 1) - { - this._queue[i] = 2; // Stage 2 -- pending or available - marked[i] = true; - c++; - } - } - } - - // Create a list with start indices and counts - var fetchList = []; - var entry = null; - var last = 0; - - // Get the int keys and sort the array numeric - var arr = et2_arrayIntKeys(marked).sort( - function(a,b){return a > b ? 1 : (a == b ? 0 : -1)}); - - for (var i = 0; i < arr.length; i++) - { - if (i == 0 || arr[i] - last > 1) - { - if (entry) - { - fetchList.push(entry); - } - entry = { - "start": arr[i], - "count": 1 - }; - } - else - { - entry.count++; - } - - last = arr[i]; - } - - if (entry) - { - fetchList.push(entry); - } - - // Special case: If there are no entries in the fetch list and this is - // an update, create an dummy entry, so that we'll get the current count - if (fetchList.length === 0 && _isUpdate) - { - fetchList.push({ - "start": 0, "count": 0 - }); - - // Disable grid invalidate, or it might request again before we're done - this._grid.doInvalidate = false; - } - - egw.debug("log", "Dataview flush", fetchList); - // Execute all queries - for (var i = 0; i < fetchList.length; i++) - { - // Build the query - var query = { - "start": fetchList[i].start, - "num_rows": fetchList[i].count, - "refresh": false - }; - - // Context used in the callback function - var ctx = { - "self": this, - "start": query.start, - "count": query.num_rows, - "lastModification": this._lastModification - }; - if(this.dataStorePrefix) - { - ctx.prefix = this.dataStorePrefix; - } - - this._queueRequest(query, ctx); - } - }, - - /** - * Queue a request for data - * @param {Object} query - * @param {Object} ctx - */ - _queueRequest: function _queueRequest(query, ctx) - { - this._request_queue.push({ - query: query, - context: ctx, - // Start pending, set to 1 when request sent - status: 0 - }); - - this._fetchQueuedRequest(); - }, - - /** - * Fetch data for a queued request, subject to rate limit - */ - _fetchQueuedRequest: function _fetchQueuedRequest() - { - // Check to see if there's room - var count = 0; - for (var i = 0; i < this._request_queue.length; i++) - { - if(this._request_queue[i].status > 0) count++; - } - // Too many requests, will try again after response is received - if(count >= this.CONCURRENT_REQUESTS || this._request_queue.length === 0) - { - return; - } - - // Keep at least 1 previous pending - var keep = 1; - - // The most recent is the one the user's most interested in - var request = null; - for(var i = this._request_queue.length - 1; i >= 0; i--) - { - // Only interested in pending requests (status 0) - if(this._request_queue[i].status != 0) - { - continue; - } - if(request == null) - { - request = this._request_queue[i]; - } - else if (keep > 0) - { - keep--; - } - else if (keep <= 0) - { - // Cancel pending, they've probably scrolled past. - this._request_queue.splice(i,1); - } - } - if(request == null) return; - - // Request being sent - request.status = 1; - - // Call the callback - this._dataProvider.dataFetch(request.query, this._fetchCallback, request.context); - }, - - _clearTimer: function () { - - // Reset the queue timer upon destruction - if (this._queueTimer) - { - window.clearTimeout(this._queueTimer); - this._queueTimer = null; - } - - }, - - /** - * Called by the data source when the data changes - * - * @param _data Object|null New data, or null. Null will remove the row. - */ - _dataCallback: function (_data) { - // Set the "hasData" flag - this.self.hasData = true; - - // Call the row callback with the new data -- the row callback then - // generates the row DOM nodes that will be inserted into the grid - if (this.self._rowCallback) - { - // Remove everything from the current row - this.entry.row.clear(); - - // If there's no data, stop - if(typeof _data == "undefined" || _data == null) - { - this.self._destroyCallback.call( - this, - this.entry.row - ); - return; - } - - // Fill the row DOM Node with data - this.self._rowCallback.call( - this.self._context, - _data, - this.entry.row, - this.entry.idx, - this.entry - ); - - // Attach the "subgrid" tag to the row, if the depth of this - // controller is larger than zero - var tr = this.entry.row.getDOMNode(); - var d = this.self.getDepth(); - if (d > 0) - { - jQuery(tr).addClass("subentry"); - jQuery("td:first",tr).children("div").last().addClass("level_" + d + " indentation"); - - if(this.entry.idx == 0) - { - // Set the CSS for the level - required so columns line up - var indent = jQuery("").appendTo('body'); - egw.css(".subentry td div.innerContainer.level_"+d, - "margin-right:" + (parseInt(indent.css("margin-right")) * d) + "px" - ); - indent.remove(); - } - } - - var links = null; - - // Look for a flag in the row to avoid actions. Use for sums or extra header rows. - if(!_data.no_actions) - { - // Get the action links if the links callback is set - if (this.self._linkCallback) - { - links = this.self._linkCallback.call( - this.self._context, - _data, - this.entry.idx, - this.entry.uid - ); - } - - // Register the row in the selection manager - this.self._selectionMgr.registerRow(this.entry.uid, this.entry.idx, - tr, links); - } - else - { - // Remember that - this.entry.no_actions = true; - } - - // Invalidate the current row entry - this.entry.row.invalidate(); - } - }, - - /** - * - */ - _destroyCallback: function (_row) { - - // Unregister the row from the selection manager, if not selected - // If it is selected, leave it there - allows selecting rows and scrolling - var selection = this.self._selectionMgr._getRegisteredRowsEntry(this.entry.uid); - if (this.entry.row && selection && !egwBitIsSet(selection.state, EGW_AO_STATE_SELECTED)) - { - var tr = this.entry.row.getDOMNode(); - this.self._selectionMgr._updateState(this.entry.uid, EGW_AO_STATE_NORMAL) - this.self._selectionMgr.unregisterRow(this.entry.uid, tr); - } - - // There is no further row connected to the entry - this.entry.row = null; - - // Unregister the data callback - this.self._dataProvider.dataUnregisterUID(this.entry.uid, - this.self._dataCallback, this); - }, - - /** - * Returns an array containing "_count" index mapping entries starting from - * the index given in "_start". - */ - _getIndexMapping: function (_start, _count) { - var result = []; - - for (var i = _start; i < _start + _count; i++) - { - result.push(this._getIndexEntry(i)); - } - - return result; - }, - - /** - * Updates the grid according to the new order. The function simply does the - * following: It iterates along the new order (given in _order) and the old - * order given in _idxMap. Iteration variables used are - * a) i -- points to the current entry in _order - * b) idx -- points to the current grid row that will be effected by - * this operation. - * c) mapIdx -- points to the current entry in _indexMap - * The following cases may occur: - * a) The current entry in the old order has no uid or no row -- in that - * case the row at the current position is simply updated, - * the old pointer will be incremented. - * b) The two uids differ -- insert a new row with the new uid, do not - * increment the old pointer. - * c) The two uids are the same -- increment the old pointer. - * In a last step all rows that are left in the old order are deleted. All - * newly created index entries are returned. This function does not update - * the internal mapping in _idxMap. - */ - _updateOrder: function (_start, _count, _idxMap, _order) { - // The result contains the newly created index map entries which have to - // be merged with the result - var result = []; - - // Iterate over the new order - var mapIdx = 0; - var idx = _start; - for (var i = 0; i < _order.length; i++, idx++) - { - var current = _idxMap[mapIdx]; - - if (!current.row || !current.uid) - { - // If there is no row yet at the current position or the uid - // of that entry is unknown, simply update the entry. - current.uid = _order[i]; - current.idx = idx; - - // Only update the row, if it is displayed (e.g. has a "loading" - // row displayed) -- this is needed for prefetching - if (current.row) - { - this._insertDataRow(current, true); - } - - mapIdx++; - } - else if (current.uid !== _order[i]) - { - // Insert a new row at the new position - var entry = { - "idx": idx, - "uid": _order[i], - "row": null - }; - - this._insertDataRow(entry, true); - - // Remember the new entry - result.push(entry); - } - else - { - // Do nothing, the uids do not differ, just update the index of - // the element - current.idx = idx; - mapIdx++; - } - } - - // Delete as many rows as we have left, invalidate the corresponding - // index entry - for (var i = mapIdx; i < _idxMap.length; i++) - { - if(typeof _idxMap[i] != 'undefined') - { - _idxMap[i].uid = null; - } - } - - return result; - }, - - _mergeResult: function (_newEntries, _invalidStartIdx, _diff, _total) { - - if (_newEntries.length > 0 || _diff > 0) - { - // Create a new index map - var newMap = {}; - - // Insert all new entries into the new index map - for (var i = 0; i < _newEntries.length; i++) - { - newMap[_newEntries[i].idx] = _newEntries[i]; - } - - // Merge the old map with all old entries - for (var key in this._indexMap) - { - // Get the corresponding index entry - var entry = this._indexMap[key]; - - // Calculate the new index -- if rows were deleted, we'll - // have to adjust the index - var newIdx = entry.idx >= _invalidStartIdx - ? entry.idx - _diff : entry.idx; - if (newIdx >= 0 && newIdx < _total - && typeof newMap[newIdx] === "undefined") - { - entry.idx = newIdx; - newMap[newIdx] = entry; - } - else - { - // Make sure the old entry gets invalidated - entry.idx = null; - entry.row = null; - } - } - - // Make the new index map the current index map - this._indexMap = newMap; - this._selectionMgr.setIndexMap(newMap); - } - - }, - - _fetchCallback: function (_response) { - // Remove answered request from queue - var request = null; - for(var i = 0; i < this.self._request_queue.length; i++) - { - if(this.self._request_queue[i].context == this) - { - request = this.self._request_queue[i]; - this.self._request_queue.splice(i,1); - break; - } - } - - this.self._lastModification = _response.lastModification; - - // Do nothing if _response.order evaluates to false - if (!_response.order) - { - return; - } - - // Make sure _response.order.length is not longer than the requested - // count, if a specific count was requested - var order = this.count != 0 ? _response.order.splice(0, this.count) : _response.order; - - // Remove from queue, or it will not be fetched again - if(_response.total < this.count) - { - // Less rows than we expected - // Clear the queue, or the remnants will never be loaded again - this.self._queue = {}; - } - else - { - for(var i = this.start; i < this.start + order.length; i++) - delete this.self._queue[i]; - } - - // Get the current index map for the updated region - var idxMap = this.self._getIndexMapping(this.start, order.length); - - // Update the grid using the new order. The _updateOrder function does - // not update the internal mapping while inserting and deleting rows, as - // this would move us to another asymptotic runtime level. - var res = this.self._updateOrder(this.start, this.count, idxMap, order); - - // Merge the new indices, update all indices with rows that were not - // affected and invalidate all indices if there were changes - this.self._mergeResult(res, this.start + order.length, - idxMap.length - order.length, _response.total); - - if(_response.total == 0) - { - this.self._emptyRow(true); - } - else - { - var row = jQuery(".egwGridView_empty",this.self._grid.innerTbody).remove(); - this.self._selectionMgr.unregisterRow("",0,row.get(0)); - } - - // Now it's OK to invalidate, if it wasn't before - this.self._grid.doInvalidate = true; - - // Update the total element count in the grid - this.self._grid.setTotalCount(_response.total); - this.self._selectionMgr.setTotalCount(_response.total); - - // Schedule an invalidate, in case total is the same - this.self._grid.invalidate(); - - // Check if requests are waiting - this.self._fetchQueuedRequest(); - }, - - /** - * Insert an empty / placeholder row when there is no data to display - */ - _emptyRow: function(_noRows) - { - var noRows = !_noRows ? false : true; - jQuery(".egwGridView_empty",this._grid.innerTbody).remove(); - if(typeof this._grid._rowProvider != "undefined" && this._grid._rowProvider.getPrototype("empty")) - { - var placeholder = this._grid._rowProvider.getPrototype("empty"); - if(jQuery("td",placeholder).length == 1) - { - jQuery("td",placeholder).css("width",this._grid.outerCell.width() + "px") - } - placeholder.appendTo(this._grid.innerTbody); - - // Register placeholder action only if no rows - if (noRows) - { - // Get the action links if the links callback is set - var links = null; - if (this._linkCallback) - { - links = this._linkCallback.call( - this._context, - {}, - 0, - "" - ); - } - this._selectionMgr.registerRow("",0,placeholder.get(0), links); - } - } - }, - - /** - * Callback function used by the selection manager to translate the selected - * range to uids. - */ - _selectionFetchRange: function (_range, _callback, _context) { - this._dataProvider.dataFetch( - { "start": _range.top, "num_rows": _range.bottom - _range.top + 1, - "no_data": true }, - function (_response) { - _callback.call(_context, _response.order); - }, - _context - ); - }, - - /** - * Tells the grid to make the given index visible. - */ - _makeIndexVisible: function (_idx) { - this._grid.makeIndexVisible(_idx); - } - -});}).call(this); - +var et2_dataview_controller = /** @class */ (function () { + /** + * Constructor of the et2_dataview_controller, connects to the grid + * callback. + * + * @param _grid is the grid the controller should controll. + * @param _rowCallback is the callback function that gets called when a row + * is requested. + * @param _linkCallback is the callback function that gets called for + * requesting action links for a row. The row data, the index of the row and + * the uid are passed as parameters to the function. + * uid is passed to the function. + * @param _actionObjectManager is the object that manages the action + * objects. + */ + function et2_dataview_controller(_parentController, _grid) { + this._indexMap = {}; + // Copy the given arguments + this._parentController = _parentController; + this._grid = _grid; + // Initialize list of child controllers + this._children = []; + // Initialize the "index map" which contains all currently displayed + // containers hashed by the "index" + this._indexMap = {}; + // Timer used for queing fetch requests + this._queueTimer = null; + // Array which contains all currently queued row indices in the form of + // an associative array + this._queue = {}; + // Current concurrent requests we have + this._request_queue = []; + // Register the dataFetch callback + this._grid.setDataCallback(this._gridCallback, this); + // Record the child + if (this._parentController != null) { + this._parentController._children.push(this); + } + } + et2_dataview_controller.prototype.destroy = function () { + // Destroy the selection manager + this._selectionMgr.destroy(); + // Clear the selection timeout + this._clearTimer(); + // Remove the child from the child list + if (this._parentController != null) { + var idx = this._parentController._children.indexOf(this); + if (idx >= 0) { + // This element is no longer parent of the child + this._parentController._children.splice(idx, 1); + this._parentController = null; + } + } + }; + /** + * @param value is an object implementing the et2_IDataProvider + * interface + */ + et2_dataview_controller.prototype.setDataProvider = function (value) { + this._dataProvider = value; + }; + et2_dataview_controller.prototype.setRowCallback = function (value) { + this._rowCallback = value; + }; + et2_dataview_controller.prototype.setLinkCallback = function (value) { + this._linkCallback = value; + }; + /** + * @param value is the context in which the _rowCallback and the + * _linkCallback are called. + */ + et2_dataview_controller.prototype.setContext = function (value) { + this._context = value; + }; + et2_dataview_controller.prototype.setActionObjectManager = function (_actionObjectManager) { + if (this._selectionMgr) { + this._selectionMgr.destroy(); + } + // Create the selection manager + this._selectionMgr = new et2_dataview_controller_selection_1.et2_dataview_selectionManager(this._parentController ? this._parentController._selectionMgr : null, this._indexMap, _actionObjectManager, this._selectionFetchRange, this._makeIndexVisible, this); + }; + /** + * The update function queries the server for changes in the currently + * managed index range -- those changes are then merged into the current + * view without a complete rebuild of every row. + * + * @param {boolean} clear Skip the fancy stuff, dump everything and start again. + * Completely clears the grid and selection. + */ + et2_dataview_controller.prototype.update = function (clear) { + // --------- + // TODO: Actually stuff here should be done if the server responds that + // there at all were some changes (needs implementation of "refresh") + // Tell the grid not to try and update itself while we do this + this._grid.doInvalidate = false; + if (clear) { + // Scroll to top + this._grid.makeIndexVisible(0); + this._grid.clear(); + // Free selection manager + this._selectionMgr.clear(); + // Clear object manager + this._objectManager.clear(); + // Clear the map + this._indexMap = {}; + // Update selection manager, it uses this by reference + this._selectionMgr.setIndexMap(this._indexMap); + // Clear the queue + this._queue = {}; + // Invalidate the change detection, re-fetches any known rows + this._lastModification = 0; + } + // Remove all rows which are outside the view range + this._grid.cleanup(); + // Get the currently visible range from the grid + var range = this._grid.getIndexRange(); + // Force range.top and range.bottom to contain an integer + if (range.top === false) { + range.top = range.bottom = 0; + } + this._request_queue = []; + // Require that range from the server + this._queueFetch(et2_bounds(range.top, clear ? 0 : range.bottom + 1), 0, true); + }; + /** + * Rebuilds the complete grid. + */ + et2_dataview_controller.prototype.reset = function () { + // Throw away all internal mappings and reset the timestamp + this._indexMap = {}; + // Update selection manager, it uses this by reference + this._selectionMgr.setIndexMap(this._indexMap); + // Clear the grid + this._grid.clear(); + // Clear the row queue + this._queue = {}; + // Reset the request queue + this._request_queue = []; + // Update the data + this.update(); + }; + /** + * Loads the initial order. Do not call multiple times. + */ + et2_dataview_controller.prototype.loadInitialOrder = function (order) { + for (var i = 0; i < order.length; i++) { + this._getIndexEntry(i).uid = order[i]; + } + }; + /** + * Load initial data + * + * @param {string} uid_key Name of the unique row identifier field + * @param {Object} data Key / Value mapping of initial data. + */ + et2_dataview_controller.prototype.loadInitialData = function (uid_prefix, uid_key, data) { + var idx = 0; + for (var key in data) { + // Skip any extra keys + if (typeof data[key] != "object" || data[key] == null || typeof data[key][uid_key] == "undefined") + continue; + // Add to row / uid map + var entry = this._getIndexEntry(idx++); + entry.uid = data[key][uid_key] + ""; + if (entry.uid.indexOf(uid_prefix) < 0) { + entry.uid = uid_prefix + "::" + entry.uid; + } + // Add to data cache so grid will find it + egw.dataStoreUID(entry.uid, data[key]); + // Don't try to insert the rows, grid will do that automatically + } + if (idx == 0) { + // No rows, start with an empty + this._selectionMgr.clear(); + this._emptyRow(this._grid._total == 0); + } + }; + /** + * Returns the depth of the controller instance. + */ + et2_dataview_controller.prototype.getDepth = function () { + if (this._parentController) { + return this._parentController.getDepth() + 1; + } + return 0; + }; + /** + * Set the data cache prefix + * The default is to use appname, but if you need to set it explicitly to + * something else to avoid conflicts. Use the same prefix everywhere for + * each type of data. eg. infolog for infolog entries, even if accessed via addressbook + */ + et2_dataview_controller.prototype.setPrefix = function (prefix) { + this.dataStorePrefix = prefix; + }; + /** + * Returns the row information of the passed node, or null if not available + * + * @param {DOMNode} node + * @return {string|false} UID, or false if not found + */ + et2_dataview_controller.prototype.getRowByNode = function (node) { + // Whatever the node, find a TR + var row_node = jQuery(node).closest('tr'); + var row = null; + // Check index map - simple case + var indexed = this._getIndexEntry(row_node.index()); + if (indexed && indexed.row && indexed.row.getDOMNode() == row_node[0]) { + row = indexed; + } + else { + // Check whole index map + for (var index in this._indexMap) { + indexed = this._indexMap[index]; + if (indexed && indexed.row && indexed.row.getDOMNode() == row_node[0]) { + row = indexed; + break; + } + } + } + // Check children + for (var i = 0; !row && i < this._children.length; i++) { + var child_row = this._children[i].getRowByNode(node); + if (child_row !== false) + row = child_row; + } + if (row && !row.controller) { + row.controller = this; + } + return row; + }; + /* -- PRIVATE FUNCTIONS -- */ + et2_dataview_controller.prototype._getIndexEntry = function (_idx) { + // Create an entry in the index map if it does not exist yet + if (typeof this._indexMap[_idx] === "undefined") { + this._indexMap[_idx] = { + "row": null, + "uid": null + }; + } + // Always update the index of the entries before returning them. This is + // neccessary, as when we remove the uid from an entry without row, its + // index does not get updated any further + this._indexMap[_idx]["idx"] = _idx; + return this._indexMap[_idx]; + }; + /** + * Inserts a new data row into the grid. index and uid are derived from the + * given management entry. If the data for the given uid does not exist yet, + * a "loading" placeholder will be shown instead. The function will do + * nothing if there already is a row associated to the entry. This function + * will not re-insert a row if the entry already had a row. + * + * @param _entry is the management entry for the index the row will be + * displayed at. + * @param _update specifies whether the row should be updated if _entry.row + * already exists. + * @return true, if all data for the row has been available, false + * otherwise. + */ + et2_dataview_controller.prototype._insertDataRow = function (_entry, _update) { + // Abort if the entry already has a row but the _insert flag is not set + if (_entry.row && !_update) { + return true; + } + // Context used for the callback functions + var ctx = { "self": this, "entry": _entry }; + // Create a new row instance, if it does not exist yet + var createdRow = false; + if (!_entry.row) { + createdRow = true; + _entry.row = this._createRow(ctx); + _entry.row.setDestroyCallback(this._destroyCallback, ctx); + } + // Load the row data if we have a uid for the entry + this.hasData = false; // Gets updated by the _dataCallback + if (_entry.uid) { + // Register the callback / immediately load the data + this._dataProvider.dataRegisterUID(_entry.uid, this._dataCallback, ctx); + } + // Display the loading "row prototype" if we don't have data for the row + if (!this.hasData) { + // Get the average height, the "-5" derives from the td padding + var avg = Math.round(this._grid.getAverageHeight() - 5) + "px"; + var prototype = this._grid.getRowProvider().getPrototype("loading"); + jQuery("div", prototype).css("height", avg); + var node = _entry.row.getJNode(); + node.empty(); + node.append(prototype.children()); + } + // Insert the row into the table -- the same row must never be inserted + // twice into the grid, so this function only executes the following + // code only if it is a newly created row. + if (createdRow && _entry.row) { + this._grid.insertRow(_entry.idx, _entry.row); + } + return this.hasData; + }; + /** + * Create a new row. + * + * @param {type} ctx + * @returns {et2_dataview_container} + */ + et2_dataview_controller.prototype._createRow = function (ctx) { + return new et2_dataview_view_row_1.et2_dataview_row(this._grid); + }; + /** + * Function which gets called by the grid when data is requested. + * + * @param _idxStart is the index of the first row for which data is + * requested. + * @param _idxEnd is the index of the last requested row. + */ + et2_dataview_controller.prototype._gridCallback = function (_idxStart, _idxEnd) { + var needsData = false; + // Iterate over all elements the dataview requested and create a row + // which indicates that we are currently loading data + for (var i = _idxStart; i <= _idxEnd; i++) { + var entry = this._getIndexEntry(i); + // Insert the row for the entry -- do not update rows which are + // already existing, as we do not have new data for those. + if (!this._insertDataRow(entry, false) && needsData === false) { + needsData = i; + } + } + // Queue fetching that data range + if (needsData !== false) { + this._queueFetch(et2_bounds(needsData, _idxEnd + 1), needsData == _idxStart ? 0 : needsData > _idxStart ? 1 : -1, false); + } + }; + /** + * The _queueFetch function is used to queue a fetch request. + * TODO: Refresh is currently not used + */ + et2_dataview_controller.prototype._queueFetch = function (_range, _direction, _isUpdate) { + // Force immediate to be false + _isUpdate = _isUpdate ? _isUpdate : false; + // Push the requests onto the request queue + var start = Math.max(0, _range.top); + var end = Math.min(this._grid.getTotalCount(), _range.bottom); + for (var i = start; i < end; i++) { + if (typeof this._queue[i] === "undefined") { + this._queue[i] = _direction; // Stage 1 - queue for after current, -1 -- queue for before current + } + } + // Start the queue timer, if this has not already been done + if (this._queueTimer === null && !_isUpdate) { + var self = this; + egw.debug('log', 'Dataview queue: ', _range); + this._queueTimer = window.setTimeout(function () { + self._flushQueue(false); + }, ET2_DATAVIEW_FETCH_TIMEOUT); + } + if (_isUpdate) { + this._flushQueue(true); + } + }; + /** + * Flushes the queue. + */ + et2_dataview_controller.prototype._flushQueue = function (_isUpdate) { + // Clear any still existing timer + this._clearTimer(); + // Mark all elements in a radius of ET2_DATAVIEW_STEPSIZE + var marked = {}; + var r = _isUpdate ? 0 : Math.floor(ET2_DATAVIEW_STEPSIZE / 2); + var total = this._grid.getTotalCount(); + for (var key in this._queue) { + if (this._queue[key] > 1) + continue; + key = parseInt(key); + var b = Math.max(0, key - r + (r * this._queue[key])); + var t = Math.min(key + r + (r * this._queue[key]), total - 1); + var c = 0; + for (var i = b; i <= t && c < ET2_DATAVIEW_STEPSIZE; i++) { + if (typeof this._queue[i] == "undefined" + || this._queue[i] <= 1) { + this._queue[i] = 2; // Stage 2 -- pending or available + marked[i] = true; + c++; + } + } + } + // Create a list with start indices and counts + var fetchList = []; + var entry = null; + var last = 0; + // Get the int keys and sort the array numeric + var arr = et2_arrayIntKeys(marked).sort(function (a, b) { return a > b ? 1 : (a == b ? 0 : -1); }); + for (var i = 0; i < arr.length; i++) { + if (i == 0 || arr[i] - last > 1) { + if (entry) { + fetchList.push(entry); + } + entry = { + "start": arr[i], + "count": 1 + }; + } + else { + entry.count++; + } + last = arr[i]; + } + if (entry) { + fetchList.push(entry); + } + // Special case: If there are no entries in the fetch list and this is + // an update, create an dummy entry, so that we'll get the current count + if (fetchList.length === 0 && _isUpdate) { + fetchList.push({ + "start": 0, "count": 0 + }); + // Disable grid invalidate, or it might request again before we're done + this._grid.doInvalidate = false; + } + egw.debug("log", "Dataview flush", fetchList); + // Execute all queries + for (var i = 0; i < fetchList.length; i++) { + // Build the query + var query = { + "start": fetchList[i].start, + "num_rows": fetchList[i].count, + "refresh": false + }; + // Context used in the callback function + var ctx = { + "self": this, + "start": query.start, + "count": query.num_rows, + "lastModification": this._lastModification + }; + if (this.dataStorePrefix) { + ctx.prefix = this.dataStorePrefix; + } + this._queueRequest(query, ctx); + } + }; + /** + * Queue a request for data + * @param {Object} query + * @param {Object} ctx + */ + et2_dataview_controller.prototype._queueRequest = function (query, ctx) { + this._request_queue.push({ + query: query, + context: ctx, + // Start pending, set to 1 when request sent + status: 0 + }); + this._fetchQueuedRequest(); + }; + /** + * Fetch data for a queued request, subject to rate limit + */ + et2_dataview_controller.prototype._fetchQueuedRequest = function () { + // Check to see if there's room + var count = 0; + for (var i = 0; i < this._request_queue.length; i++) { + if (this._request_queue[i].status > 0) + count++; + } + // Too many requests, will try again after response is received + if (count >= et2_dataview_controller.CONCURRENT_REQUESTS || this._request_queue.length === 0) { + return; + } + // Keep at least 1 previous pending + var keep = 1; + // The most recent is the one the user's most interested in + var request = null; + for (var i = this._request_queue.length - 1; i >= 0; i--) { + // Only interested in pending requests (status 0) + if (this._request_queue[i].status != 0) { + continue; + } + if (request == null) { + request = this._request_queue[i]; + } + else if (keep > 0) { + keep--; + } + else if (keep <= 0) { + // Cancel pending, they've probably scrolled past. + this._request_queue.splice(i, 1); + } + } + if (request == null) + return; + // Request being sent + request.status = 1; + // Call the callback + this._dataProvider.dataFetch(request.query, this._fetchCallback, request.context); + }; + et2_dataview_controller.prototype._clearTimer = function () { + // Reset the queue timer upon destruction + if (this._queueTimer) { + window.clearTimeout(this._queueTimer); + this._queueTimer = null; + } + }; + /** + * Called by the data source when the data changes + * + * @param _data Object|null New data, or null. Null will remove the row. + */ + et2_dataview_controller.prototype._dataCallback = function (_data) { + // Set the "hasData" flag + this.self.hasData = true; + // Call the row callback with the new data -- the row callback then + // generates the row DOM nodes that will be inserted into the grid + if (this.self._rowCallback) { + // Remove everything from the current row + this.entry.row.clear(); + // If there's no data, stop + if (typeof _data == "undefined" || _data == null) { + this.self._destroyCallback.call(this, this.entry.row); + return; + } + // Fill the row DOM Node with data + this.self._rowCallback.call(this.self._context, _data, this.entry.row, this.entry.idx, this.entry); + // Attach the "subgrid" tag to the row, if the depth of this + // controller is larger than zero + var tr = this.entry.row.getDOMNode(); + var d = this.self.getDepth(); + if (d > 0) { + jQuery(tr).addClass("subentry"); + jQuery("td:first", tr).children("div").last().addClass("level_" + d + " indentation"); + if (this.entry.idx == 0) { + // Set the CSS for the level - required so columns line up + var indent = jQuery("").appendTo('body'); + egw.css(".subentry td div.innerContainer.level_" + d, "margin-right:" + (parseInt(indent.css("margin-right")) * d) + "px"); + indent.remove(); + } + } + var links = null; + // Look for a flag in the row to avoid actions. Use for sums or extra header rows. + if (!_data.no_actions) { + // Get the action links if the links callback is set + if (this.self._linkCallback) { + links = this.self._linkCallback.call(this.self._context, _data, this.entry.idx, this.entry.uid); + } + // Register the row in the selection manager + this.self._selectionMgr.registerRow(this.entry.uid, this.entry.idx, tr, links); + } + else { + // Remember that + this.entry.no_actions = true; + } + // Invalidate the current row entry + this.entry.row.invalidate(); + } + }; + /** + * + */ + et2_dataview_controller.prototype._destroyCallback = function (_row) { + // Unregister the row from the selection manager, if not selected + // If it is selected, leave it there - allows selecting rows and scrolling + var selection = this.self._selectionMgr._getRegisteredRowsEntry(this.entry.uid); + if (this.entry.row && selection && !egwBitIsSet(selection.state, EGW_AO_STATE_SELECTED)) { + var tr = this.entry.row.getDOMNode(); + this.self._selectionMgr._updateState(this.entry.uid, EGW_AO_STATE_NORMAL); + this.self._selectionMgr.unregisterRow(this.entry.uid, tr); + } + // There is no further row connected to the entry + this.entry.row = null; + // Unregister the data callback + this.self._dataProvider.dataUnregisterUID(this.entry.uid, this.self._dataCallback, this); + }; + /** + * Returns an array containing "_count" index mapping entries starting from + * the index given in "_start". + */ + et2_dataview_controller.prototype._getIndexMapping = function (_start, _count) { + var result = []; + for (var i = _start; i < _start + _count; i++) { + result.push(this._getIndexEntry(i)); + } + return result; + }; + /** + * Updates the grid according to the new order. The function simply does the + * following: It iterates along the new order (given in _order) and the old + * order given in _idxMap. Iteration variables used are + * a) i -- points to the current entry in _order + * b) idx -- points to the current grid row that will be effected by + * this operation. + * c) mapIdx -- points to the current entry in _indexMap + * The following cases may occur: + * a) The current entry in the old order has no uid or no row -- in that + * case the row at the current position is simply updated, + * the old pointer will be incremented. + * b) The two uids differ -- insert a new row with the new uid, do not + * increment the old pointer. + * c) The two uids are the same -- increment the old pointer. + * In a last step all rows that are left in the old order are deleted. All + * newly created index entries are returned. This function does not update + * the internal mapping in _idxMap. + */ + et2_dataview_controller.prototype._updateOrder = function (_start, _count, _idxMap, _order) { + // The result contains the newly created index map entries which have to + // be merged with the result + var result = []; + // Iterate over the new order + var mapIdx = 0; + var idx = _start; + for (var i = 0; i < _order.length; i++, idx++) { + var current = _idxMap[mapIdx]; + if (!current.row || !current.uid) { + // If there is no row yet at the current position or the uid + // of that entry is unknown, simply update the entry. + current.uid = _order[i]; + current.idx = idx; + // Only update the row, if it is displayed (e.g. has a "loading" + // row displayed) -- this is needed for prefetching + if (current.row) { + this._insertDataRow(current, true); + } + mapIdx++; + } + else if (current.uid !== _order[i]) { + // Insert a new row at the new position + var entry = { + "idx": idx, + "uid": _order[i], + "row": null + }; + this._insertDataRow(entry, true); + // Remember the new entry + result.push(entry); + } + else { + // Do nothing, the uids do not differ, just update the index of + // the element + current.idx = idx; + mapIdx++; + } + } + // Delete as many rows as we have left, invalidate the corresponding + // index entry + for (var i = mapIdx; i < _idxMap.length; i++) { + if (typeof _idxMap[i] != 'undefined') { + _idxMap[i].uid = null; + } + } + return result; + }; + et2_dataview_controller.prototype._mergeResult = function (_newEntries, _invalidStartIdx, _diff, _total) { + if (_newEntries.length > 0 || _diff > 0) { + // Create a new index map + var newMap = {}; + // Insert all new entries into the new index map + for (var i = 0; i < _newEntries.length; i++) { + newMap[_newEntries[i].idx] = _newEntries[i]; + } + // Merge the old map with all old entries + for (var key in this._indexMap) { + // Get the corresponding index entry + var entry = this._indexMap[key]; + // Calculate the new index -- if rows were deleted, we'll + // have to adjust the index + var newIdx = entry.idx >= _invalidStartIdx + ? entry.idx - _diff : entry.idx; + if (newIdx >= 0 && newIdx < _total + && typeof newMap[newIdx] === "undefined") { + entry.idx = newIdx; + newMap[newIdx] = entry; + } + else { + // Make sure the old entry gets invalidated + entry.idx = null; + entry.row = null; + } + } + // Make the new index map the current index map + this._indexMap = newMap; + this._selectionMgr.setIndexMap(newMap); + } + }; + et2_dataview_controller.prototype._fetchCallback = function (_response) { + // Remove answered request from queue + var request = null; + for (var i = 0; i < this.self._request_queue.length; i++) { + if (this.self._request_queue[i].context == this) { + request = this.self._request_queue[i]; + this.self._request_queue.splice(i, 1); + break; + } + } + this.self._lastModification = _response.lastModification; + // Do nothing if _response.order evaluates to false + if (!_response.order) { + return; + } + // Make sure _response.order.length is not longer than the requested + // count, if a specific count was requested + var order = this.count != 0 ? _response.order.splice(0, this.count) : _response.order; + // Remove from queue, or it will not be fetched again + if (_response.total < this.count) { + // Less rows than we expected + // Clear the queue, or the remnants will never be loaded again + this.self._queue = {}; + } + else { + for (var i = this.start; i < this.start + order.length; i++) + delete this.self._queue[i]; + } + // Get the current index map for the updated region + var idxMap = this.self._getIndexMapping(this.start, order.length); + // Update the grid using the new order. The _updateOrder function does + // not update the internal mapping while inserting and deleting rows, as + // this would move us to another asymptotic runtime level. + var res = this.self._updateOrder(this.start, this.count, idxMap, order); + // Merge the new indices, update all indices with rows that were not + // affected and invalidate all indices if there were changes + this.self._mergeResult(res, this.start + order.length, idxMap.length - order.length, _response.total); + if (_response.total == 0) { + this.self._emptyRow(true); + } + else { + var row = jQuery(".egwGridView_empty", this.self._grid.innerTbody).remove(); + this.self._selectionMgr.unregisterRow("", 0, row.get(0)); + } + // Now it's OK to invalidate, if it wasn't before + this.self._grid.doInvalidate = true; + // Update the total element count in the grid + this.self._grid.setTotalCount(_response.total); + this.self._selectionMgr.setTotalCount(_response.total); + // Schedule an invalidate, in case total is the same + this.self._grid.invalidate(); + // Check if requests are waiting + this.self._fetchQueuedRequest(); + }; + /** + * Insert an empty / placeholder row when there is no data to display + */ + et2_dataview_controller.prototype._emptyRow = function (_noRows) { + var noRows = !_noRows ? false : true; + jQuery(".egwGridView_empty", this._grid.innerTbody).remove(); + if (typeof this._grid._rowProvider != "undefined" && this._grid._rowProvider.getPrototype("empty")) { + var placeholder = this._grid._rowProvider.getPrototype("empty"); + if (jQuery("td", placeholder).length == 1) { + jQuery("td", placeholder).css("width", this._grid.outerCell.width() + "px"); + } + placeholder.appendTo(this._grid.innerTbody); + // Register placeholder action only if no rows + if (noRows) { + // Get the action links if the links callback is set + var links = null; + if (this._linkCallback) { + links = this._linkCallback.call(this._context, {}, 0, ""); + } + this._selectionMgr.registerRow("", 0, placeholder.get(0), links); + } + } + }; + /** + * Callback function used by the selection manager to translate the selected + * range to uids. + */ + et2_dataview_controller.prototype._selectionFetchRange = function (_range, _callback, _context) { + this._dataProvider.dataFetch({ "start": _range.top, "num_rows": _range.bottom - _range.top + 1, + "no_data": true }, function (_response) { + _callback.call(_context, _response.order); + }, _context); + }; + /** + * Tells the grid to make the given index visible. + */ + et2_dataview_controller.prototype._makeIndexVisible = function (_idx) { + this._grid.makeIndexVisible(_idx); + }; + // Maximum concurrent data requests. Additional ones are held in the queue. + et2_dataview_controller.CONCURRENT_REQUESTS = 5; + return et2_dataview_controller; +}()); +exports.et2_dataview_controller = et2_dataview_controller; +//# sourceMappingURL=et2_dataview_controller.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_dataview_controller.ts b/api/js/etemplate/et2_dataview_controller.ts new file mode 100644 index 0000000000..7de0a670c1 --- /dev/null +++ b/api/js/etemplate/et2_dataview_controller.ts @@ -0,0 +1,1128 @@ +/** + * EGroupware eTemplate2 + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage dataview + * @link http://www.egroupware.org + * @author Andreas Stöckel + * @copyright Stylite 2011-2012 + +/*egw:uses + et2_core_common; + et2_core_inheritance; + + et2_dataview_interfaces; + et2_dataview_controller_selection; + et2_dataview_view_row; + et2_dataview_view_tile; + + egw_action.egw_action; +*/ + +import {et2_IDataProvider} from "./et2_dataview_interfaces"; +import {et2_dataview_selectionManager} from "./et2_dataview_controller_selection"; +import {et2_dataview_row} from "./et2_dataview_view_row"; + +/** + * The fetch timeout specifies the time during which the controller tries to + * consolidate requests for rows. + */ +var ET2_DATAVIEW_FETCH_TIMEOUT = 50; + +var ET2_DATAVIEW_STEPSIZE = 50; + +/** + * The et2_dataview_controller class is the intermediate layer between a grid + * instance and the corresponding data source. It manages updating the grid, + * as well as inserting and deleting rows. + */ +export class et2_dataview_controller +{ + // Maximum concurrent data requests. Additional ones are held in the queue. + public static readonly CONCURRENT_REQUESTS = 5; + + private _parentController: any; + private _grid: any; + private dataStorePrefix: any; + private _dataProvider: any; + private _rowCallback: any; + private _linkCallback: any; + private _context: any; + + private _children: any[]; + private _indexMap: any = {}; + + private _queueTimer: number; + private _lastModification: number; + private _queue: {}; + private _request_queue: any[]; + private _selectionMgr: et2_dataview_selectionManager; + + private _objectManager: any; + + /** + * Constructor of the et2_dataview_controller, connects to the grid + * callback. + * + * @param _grid is the grid the controller should controll. + * @param _rowCallback is the callback function that gets called when a row + * is requested. + * @param _linkCallback is the callback function that gets called for + * requesting action links for a row. The row data, the index of the row and + * the uid are passed as parameters to the function. + * uid is passed to the function. + * @param _actionObjectManager is the object that manages the action + * objects. + */ + constructor (_parentController, _grid) + { + // Copy the given arguments + this._parentController = _parentController; + this._grid = _grid; + + // Initialize list of child controllers + this._children = []; + + // Initialize the "index map" which contains all currently displayed + // containers hashed by the "index" + this._indexMap = {}; + + // Timer used for queing fetch requests + this._queueTimer = null; + + // Array which contains all currently queued row indices in the form of + // an associative array + this._queue = {}; + + // Current concurrent requests we have + this._request_queue = []; + + // Register the dataFetch callback + this._grid.setDataCallback(this._gridCallback, this); + + + // Record the child + if(this._parentController != null) + { + this._parentController._children.push(this); + } + } + + destroy( ) + { + + // Destroy the selection manager + this._selectionMgr.destroy(); + + // Clear the selection timeout + this._clearTimer(); + + // Remove the child from the child list + if(this._parentController != null) + { + var idx = this._parentController._children.indexOf(this); + + if (idx >= 0) + { + // This element is no longer parent of the child + this._parentController._children.splice(idx, 1); + this._parentController = null; + } + } + } + + /** + * @param value is an object implementing the et2_IDataProvider + * interface + */ + setDataProvider(value: et2_IDataProvider) + { + this._dataProvider = value; + } + + setRowCallback(value: any) { + this._rowCallback = value; + } + + setLinkCallback(value: any) { + this._linkCallback = value; + } + + /** + * @param value is the context in which the _rowCallback and the + * _linkCallback are called. + */ + setContext(value: any) { + this._context = value; + } + + setActionObjectManager(_actionObjectManager: any) + { + if(this._selectionMgr) + { + this._selectionMgr.destroy(); + } + + // Create the selection manager + this._selectionMgr = new et2_dataview_selectionManager( + this._parentController ? this._parentController._selectionMgr : null, + this._indexMap, + _actionObjectManager, + this._selectionFetchRange, + this._makeIndexVisible, + this + ); + } + + /** + * The update function queries the server for changes in the currently + * managed index range -- those changes are then merged into the current + * view without a complete rebuild of every row. + * + * @param {boolean} clear Skip the fancy stuff, dump everything and start again. + * Completely clears the grid and selection. + */ + update( clear? : boolean) + { + + // --------- + + // TODO: Actually stuff here should be done if the server responds that + // there at all were some changes (needs implementation of "refresh") + + // Tell the grid not to try and update itself while we do this + this._grid.doInvalidate = false; + + if(clear) + { + // Scroll to top + this._grid.makeIndexVisible(0); + this._grid.clear(); + + // Free selection manager + this._selectionMgr.clear(); + + // Clear object manager + this._objectManager.clear(); + + // Clear the map + this._indexMap = {}; + // Update selection manager, it uses this by reference + this._selectionMgr.setIndexMap(this._indexMap); + + // Clear the queue + this._queue = {}; + + // Invalidate the change detection, re-fetches any known rows + this._lastModification = 0; + } + // Remove all rows which are outside the view range + this._grid.cleanup(); + + // Get the currently visible range from the grid + var range = this._grid.getIndexRange(); + + // Force range.top and range.bottom to contain an integer + if (range.top === false) + { + range.top = range.bottom = 0; + } + this._request_queue = []; + + // Require that range from the server + this._queueFetch(et2_bounds(range.top, clear ? 0 : range.bottom + 1), 0, true); + } + + /** + * Rebuilds the complete grid. + */ + reset( ) + { + // Throw away all internal mappings and reset the timestamp + this._indexMap = {}; + // Update selection manager, it uses this by reference + this._selectionMgr.setIndexMap(this._indexMap); + + // Clear the grid + this._grid.clear(); + + // Clear the row queue + this._queue = {}; + + // Reset the request queue + this._request_queue = []; + + // Update the data + this.update(); + } + + /** + * Loads the initial order. Do not call multiple times. + */ + loadInitialOrder( order) + { + for (var i = 0; i < order.length; i++) + { + this._getIndexEntry(i).uid = order[i]; + } + } + + /** + * Load initial data + * + * @param {string} uid_key Name of the unique row identifier field + * @param {Object} data Key / Value mapping of initial data. + */ + loadInitialData( uid_prefix, uid_key, data) + { + var idx = 0; + for(var key in data) + { + // Skip any extra keys + if(typeof data[key] != "object" || data[key] == null || typeof data[key][uid_key] == "undefined") continue; + + // Add to row / uid map + var entry = this._getIndexEntry(idx++); + entry.uid = data[key][uid_key]+""; + if(entry.uid.indexOf(uid_prefix) < 0) + { + entry.uid = uid_prefix + "::" + entry.uid; + } + + // Add to data cache so grid will find it + egw.dataStoreUID(entry.uid, data[key]) + + // Don't try to insert the rows, grid will do that automatically + } + if(idx == 0) + { + // No rows, start with an empty + this._selectionMgr.clear(); + this._emptyRow(this._grid._total == 0); + } + } + + /** + * Returns the depth of the controller instance. + */ + getDepth( ) + { + + if (this._parentController) + { + return this._parentController.getDepth() + 1; + } + + return 0; + } + + /** + * Set the data cache prefix + * The default is to use appname, but if you need to set it explicitly to + * something else to avoid conflicts. Use the same prefix everywhere for + * each type of data. eg. infolog for infolog entries, even if accessed via addressbook + */ + setPrefix( prefix) + { + this.dataStorePrefix = prefix; + } + + /** + * Returns the row information of the passed node, or null if not available + * + * @param {DOMNode} node + * @return {string|false} UID, or false if not found + */ + getRowByNode( node) + { + // Whatever the node, find a TR + var row_node = jQuery(node).closest('tr'); + var row = null; + + // Check index map - simple case + var indexed = this._getIndexEntry(row_node.index()); + if(indexed && indexed.row && indexed.row.getDOMNode() == row_node[0]) + { + row = indexed; + } + else + { + // Check whole index map + for(var index in this._indexMap) + { + indexed = this._indexMap[index]; + if( indexed && indexed.row && indexed.row.getDOMNode() == row_node[0]) + { + row = indexed; + break; + } + } + } + + // Check children + for(var i = 0; !row && i < this._children.length; i++) + { + var child_row = this._children[i].getRowByNode(node); + if(child_row !== false) row = child_row; + } + if(row && !row.controller) + { + row.controller = this; + } + return row; + } + + /* -- PRIVATE FUNCTIONS -- */ + + + _getIndexEntry( _idx) + { + // Create an entry in the index map if it does not exist yet + if (typeof this._indexMap[_idx] === "undefined") + { + this._indexMap[_idx] = { + "row": null, + "uid": null + }; + } + + // Always update the index of the entries before returning them. This is + // neccessary, as when we remove the uid from an entry without row, its + // index does not get updated any further + this._indexMap[_idx]["idx"] = _idx; + + return this._indexMap[_idx]; + } + + /** + * Inserts a new data row into the grid. index and uid are derived from the + * given management entry. If the data for the given uid does not exist yet, + * a "loading" placeholder will be shown instead. The function will do + * nothing if there already is a row associated to the entry. This function + * will not re-insert a row if the entry already had a row. + * + * @param _entry is the management entry for the index the row will be + * displayed at. + * @param _update specifies whether the row should be updated if _entry.row + * already exists. + * @return true, if all data for the row has been available, false + * otherwise. + */ + _insertDataRow( _entry, _update) + { + // Abort if the entry already has a row but the _insert flag is not set + if (_entry.row && !_update) + { + return true; + } + + // Context used for the callback functions + var ctx = {"self": this, "entry": _entry}; + + // Create a new row instance, if it does not exist yet + var createdRow = false; + if (!_entry.row) + { + createdRow = true; + _entry.row = this._createRow(ctx); + _entry.row.setDestroyCallback(this._destroyCallback, ctx); + } + + // Load the row data if we have a uid for the entry + this.hasData = false; // Gets updated by the _dataCallback + if (_entry.uid) + { + // Register the callback / immediately load the data + this._dataProvider.dataRegisterUID(_entry.uid, this._dataCallback, + ctx); + } + + // Display the loading "row prototype" if we don't have data for the row + if (!this.hasData) + { + // Get the average height, the "-5" derives from the td padding + var avg = Math.round(this._grid.getAverageHeight() - 5) + "px"; + var prototype = this._grid.getRowProvider().getPrototype("loading"); + jQuery("div", prototype).css("height", avg); + var node = _entry.row.getJNode(); + node.empty(); + node.append(prototype.children()); + } + + // Insert the row into the table -- the same row must never be inserted + // twice into the grid, so this function only executes the following + // code only if it is a newly created row. + if (createdRow && _entry.row) + { + this._grid.insertRow(_entry.idx, _entry.row); + } + + return this.hasData; + } + + + /** + * Create a new row. + * + * @param {type} ctx + * @returns {et2_dataview_container} + */ + _createRow( ctx) + { + return new et2_dataview_row(this._grid); + } + + /** + * Function which gets called by the grid when data is requested. + * + * @param _idxStart is the index of the first row for which data is + * requested. + * @param _idxEnd is the index of the last requested row. + */ + _gridCallback( _idxStart, _idxEnd) + { + + var needsData = false; + + // Iterate over all elements the dataview requested and create a row + // which indicates that we are currently loading data + for (var i = _idxStart; i <= _idxEnd; i++) + { + var entry = this._getIndexEntry(i); + + // Insert the row for the entry -- do not update rows which are + // already existing, as we do not have new data for those. + if (!this._insertDataRow(entry, false) && needsData === false) + { + needsData = i; + } + } + + // Queue fetching that data range + if (needsData !== false) + { + this._queueFetch(et2_bounds(needsData, _idxEnd + 1), needsData == _idxStart ? 0 : needsData > _idxStart ? 1 : -1, false); + } + } + + /** + * The _queueFetch function is used to queue a fetch request. + * TODO: Refresh is currently not used + */ + _queueFetch( _range, _direction, _isUpdate) + { + + // Force immediate to be false + _isUpdate = _isUpdate ? _isUpdate : false; + + // Push the requests onto the request queue + var start = Math.max(0, _range.top); + var end = Math.min(this._grid.getTotalCount(), _range.bottom); + for (var i = start; i < end; i++) + { + if (typeof this._queue[i] === "undefined") + { + this._queue[i] = _direction; // Stage 1 - queue for after current, -1 -- queue for before current + } + } + + // Start the queue timer, if this has not already been done + if (this._queueTimer === null && !_isUpdate) + { + var self = this; + egw.debug('log', 'Dataview queue: ', _range); + this._queueTimer = window.setTimeout(function () { + self._flushQueue(false); + }, ET2_DATAVIEW_FETCH_TIMEOUT); + } + + if (_isUpdate) + { + this._flushQueue(true); + } + } + + /** + * Flushes the queue. + */ + _flushQueue( _isUpdate) + { + + // Clear any still existing timer + this._clearTimer(); + + // Mark all elements in a radius of ET2_DATAVIEW_STEPSIZE + var marked = {}; + var r = _isUpdate ? 0 : Math.floor(ET2_DATAVIEW_STEPSIZE / 2); + var total = this._grid.getTotalCount(); + for (var key in this._queue) + { + if (this._queue[key] > 1) + continue; + + key = parseInt(key); + + var b = Math.max(0, key - r + (r * this._queue[key])); + var t = Math.min(key + r + (r * this._queue[key]), total - 1); + var c = 0; + for (var i = b; i <= t && c < ET2_DATAVIEW_STEPSIZE; i ++) + { + if (typeof this._queue[i] == "undefined" + || this._queue[i] <= 1) + { + this._queue[i] = 2; // Stage 2 -- pending or available + marked[i] = true; + c++; + } + } + } + + // Create a list with start indices and counts + var fetchList = []; + var entry = null; + var last = 0; + + // Get the int keys and sort the array numeric + var arr = et2_arrayIntKeys(marked).sort( + function(a,b){return a > b ? 1 : (a == b ? 0 : -1)}); + + for (var i = 0; i < arr.length; i++) + { + if (i == 0 || arr[i] - last > 1) + { + if (entry) + { + fetchList.push(entry); + } + entry = { + "start": arr[i], + "count": 1 + }; + } + else + { + entry.count++; + } + + last = arr[i]; + } + + if (entry) + { + fetchList.push(entry); + } + + // Special case: If there are no entries in the fetch list and this is + // an update, create an dummy entry, so that we'll get the current count + if (fetchList.length === 0 && _isUpdate) + { + fetchList.push({ + "start": 0, "count": 0 + }); + + // Disable grid invalidate, or it might request again before we're done + this._grid.doInvalidate = false; + } + + egw.debug("log", "Dataview flush", fetchList); + // Execute all queries + for (var i = 0; i < fetchList.length; i++) + { + // Build the query + var query = { + "start": fetchList[i].start, + "num_rows": fetchList[i].count, + "refresh": false + }; + + // Context used in the callback function + var ctx = { + "self": this, + "start": query.start, + "count": query.num_rows, + "lastModification": this._lastModification + }; + if(this.dataStorePrefix) + { + ctx.prefix = this.dataStorePrefix; + } + + this._queueRequest(query, ctx); + } + } + + /** + * Queue a request for data + * @param {Object} query + * @param {Object} ctx + */ + _queueRequest(query, ctx) + { + this._request_queue.push({ + query: query, + context: ctx, + // Start pending, set to 1 when request sent + status: 0 + }); + + this._fetchQueuedRequest(); + } + + /** + * Fetch data for a queued request, subject to rate limit + */ + _fetchQueuedRequest() + { + // Check to see if there's room + var count = 0; + for (var i = 0; i < this._request_queue.length; i++) + { + if(this._request_queue[i].status > 0) count++; + } + // Too many requests, will try again after response is received + if(count >= et2_dataview_controller.CONCURRENT_REQUESTS || this._request_queue.length === 0) + { + return; + } + + // Keep at least 1 previous pending + var keep = 1; + + // The most recent is the one the user's most interested in + var request = null; + for(var i = this._request_queue.length - 1; i >= 0; i--) + { + // Only interested in pending requests (status 0) + if(this._request_queue[i].status != 0) + { + continue; + } + if(request == null) + { + request = this._request_queue[i]; + } + else if (keep > 0) + { + keep--; + } + else if (keep <= 0) + { + // Cancel pending, they've probably scrolled past. + this._request_queue.splice(i,1); + } + } + if(request == null) return; + + // Request being sent + request.status = 1; + + // Call the callback + this._dataProvider.dataFetch(request.query, this._fetchCallback, request.context); + } + + _clearTimer( ) + { + + // Reset the queue timer upon destruction + if (this._queueTimer) + { + window.clearTimeout(this._queueTimer); + this._queueTimer = null; + } + + } + + /** + * Called by the data source when the data changes + * + * @param _data Object|null New data, or null. Null will remove the row. + */ + _dataCallback( _data) + { + // Set the "hasData" flag + this.self.hasData = true; + + // Call the row callback with the new data -- the row callback then + // generates the row DOM nodes that will be inserted into the grid + if (this.self._rowCallback) + { + // Remove everything from the current row + this.entry.row.clear(); + + // If there's no data, stop + if(typeof _data == "undefined" || _data == null) + { + this.self._destroyCallback.call( + this, + this.entry.row + ); + return; + } + + // Fill the row DOM Node with data + this.self._rowCallback.call( + this.self._context, + _data, + this.entry.row, + this.entry.idx, + this.entry + ); + + // Attach the "subgrid" tag to the row, if the depth of this + // controller is larger than zero + var tr = this.entry.row.getDOMNode(); + var d = this.self.getDepth(); + if (d > 0) + { + jQuery(tr).addClass("subentry"); + jQuery("td:first",tr).children("div").last().addClass("level_" + d + " indentation"); + + if(this.entry.idx == 0) + { + // Set the CSS for the level - required so columns line up + var indent = jQuery("").appendTo('body'); + egw.css(".subentry td div.innerContainer.level_"+d, + "margin-right:" + (parseInt(indent.css("margin-right")) * d) + "px" + ); + indent.remove(); + } + } + + var links = null; + + // Look for a flag in the row to avoid actions. Use for sums or extra header rows. + if(!_data.no_actions) + { + // Get the action links if the links callback is set + if (this.self._linkCallback) + { + links = this.self._linkCallback.call( + this.self._context, + _data, + this.entry.idx, + this.entry.uid + ); + } + + // Register the row in the selection manager + this.self._selectionMgr.registerRow(this.entry.uid, this.entry.idx, + tr, links); + } + else + { + // Remember that + this.entry.no_actions = true; + } + + // Invalidate the current row entry + this.entry.row.invalidate(); + } + } + + /** + * + */ + _destroyCallback( _row) + { + + // Unregister the row from the selection manager, if not selected + // If it is selected, leave it there - allows selecting rows and scrolling + var selection = this.self._selectionMgr._getRegisteredRowsEntry(this.entry.uid); + if (this.entry.row && selection && !egwBitIsSet(selection.state, EGW_AO_STATE_SELECTED)) + { + var tr = this.entry.row.getDOMNode(); + this.self._selectionMgr._updateState(this.entry.uid, EGW_AO_STATE_NORMAL); + this.self._selectionMgr.unregisterRow(this.entry.uid, tr); + } + + // There is no further row connected to the entry + this.entry.row = null; + + // Unregister the data callback + this.self._dataProvider.dataUnregisterUID(this.entry.uid, + this.self._dataCallback, this); + } + + /** + * Returns an array containing "_count" index mapping entries starting from + * the index given in "_start". + */ + _getIndexMapping( _start, _count) + { + var result = []; + + for (var i = _start; i < _start + _count; i++) + { + result.push(this._getIndexEntry(i)); + } + + return result; + } + + /** + * Updates the grid according to the new order. The function simply does the + * following: It iterates along the new order (given in _order) and the old + * order given in _idxMap. Iteration variables used are + * a) i -- points to the current entry in _order + * b) idx -- points to the current grid row that will be effected by + * this operation. + * c) mapIdx -- points to the current entry in _indexMap + * The following cases may occur: + * a) The current entry in the old order has no uid or no row -- in that + * case the row at the current position is simply updated, + * the old pointer will be incremented. + * b) The two uids differ -- insert a new row with the new uid, do not + * increment the old pointer. + * c) The two uids are the same -- increment the old pointer. + * In a last step all rows that are left in the old order are deleted. All + * newly created index entries are returned. This function does not update + * the internal mapping in _idxMap. + */ + _updateOrder( _start, _count, _idxMap, _order) + { + // The result contains the newly created index map entries which have to + // be merged with the result + var result = []; + + // Iterate over the new order + var mapIdx = 0; + var idx = _start; + for (var i = 0; i < _order.length; i++, idx++) + { + var current = _idxMap[mapIdx]; + + if (!current.row || !current.uid) + { + // If there is no row yet at the current position or the uid + // of that entry is unknown, simply update the entry. + current.uid = _order[i]; + current.idx = idx; + + // Only update the row, if it is displayed (e.g. has a "loading" + // row displayed) -- this is needed for prefetching + if (current.row) + { + this._insertDataRow(current, true); + } + + mapIdx++; + } + else if (current.uid !== _order[i]) + { + // Insert a new row at the new position + var entry = { + "idx": idx, + "uid": _order[i], + "row": null + }; + + this._insertDataRow(entry, true); + + // Remember the new entry + result.push(entry); + } + else + { + // Do nothing, the uids do not differ, just update the index of + // the element + current.idx = idx; + mapIdx++; + } + } + + // Delete as many rows as we have left, invalidate the corresponding + // index entry + for (var i = mapIdx; i < _idxMap.length; i++) + { + if(typeof _idxMap[i] != 'undefined') + { + _idxMap[i].uid = null; + } + } + + return result; + } + + _mergeResult( _newEntries, _invalidStartIdx, _diff, _total) + { + + if (_newEntries.length > 0 || _diff > 0) + { + // Create a new index map + var newMap = {}; + + // Insert all new entries into the new index map + for (var i = 0; i < _newEntries.length; i++) + { + newMap[_newEntries[i].idx] = _newEntries[i]; + } + + // Merge the old map with all old entries + for (var key in this._indexMap) + { + // Get the corresponding index entry + var entry = this._indexMap[key]; + + // Calculate the new index -- if rows were deleted, we'll + // have to adjust the index + var newIdx = entry.idx >= _invalidStartIdx + ? entry.idx - _diff : entry.idx; + if (newIdx >= 0 && newIdx < _total + && typeof newMap[newIdx] === "undefined") + { + entry.idx = newIdx; + newMap[newIdx] = entry; + } + else + { + // Make sure the old entry gets invalidated + entry.idx = null; + entry.row = null; + } + } + + // Make the new index map the current index map + this._indexMap = newMap; + this._selectionMgr.setIndexMap(newMap); + } + + } + + _fetchCallback( _response) + { + // Remove answered request from queue + var request = null; + for(var i = 0; i < this.self._request_queue.length; i++) + { + if(this.self._request_queue[i].context == this) + { + request = this.self._request_queue[i]; + this.self._request_queue.splice(i,1); + break; + } + } + + this.self._lastModification = _response.lastModification; + + // Do nothing if _response.order evaluates to false + if (!_response.order) + { + return; + } + + // Make sure _response.order.length is not longer than the requested + // count, if a specific count was requested + var order = this.count != 0 ? _response.order.splice(0, this.count) : _response.order; + + // Remove from queue, or it will not be fetched again + if(_response.total < this.count) + { + // Less rows than we expected + // Clear the queue, or the remnants will never be loaded again + this.self._queue = {}; + } + else + { + for(var i = this.start; i < this.start + order.length; i++) + delete this.self._queue[i]; + } + + // Get the current index map for the updated region + var idxMap = this.self._getIndexMapping(this.start, order.length); + + // Update the grid using the new order. The _updateOrder function does + // not update the internal mapping while inserting and deleting rows, as + // this would move us to another asymptotic runtime level. + var res = this.self._updateOrder(this.start, this.count, idxMap, order); + + // Merge the new indices, update all indices with rows that were not + // affected and invalidate all indices if there were changes + this.self._mergeResult(res, this.start + order.length, + idxMap.length - order.length, _response.total); + + if(_response.total == 0) + { + this.self._emptyRow(true); + } + else + { + var row = jQuery(".egwGridView_empty",this.self._grid.innerTbody).remove(); + this.self._selectionMgr.unregisterRow("",0,row.get(0)); + } + + // Now it's OK to invalidate, if it wasn't before + this.self._grid.doInvalidate = true; + + // Update the total element count in the grid + this.self._grid.setTotalCount(_response.total); + this.self._selectionMgr.setTotalCount(_response.total); + + // Schedule an invalidate, in case total is the same + this.self._grid.invalidate(); + + // Check if requests are waiting + this.self._fetchQueuedRequest(); + } + + /** + * Insert an empty / placeholder row when there is no data to display + */ + _emptyRow(_noRows) + { + var noRows = !_noRows ? false : true; + jQuery(".egwGridView_empty",this._grid.innerTbody).remove(); + if(typeof this._grid._rowProvider != "undefined" && this._grid._rowProvider.getPrototype("empty")) + { + var placeholder = this._grid._rowProvider.getPrototype("empty"); + if(jQuery("td",placeholder).length == 1) + { + jQuery("td",placeholder).css("width",this._grid.outerCell.width() + "px") + } + placeholder.appendTo(this._grid.innerTbody); + + // Register placeholder action only if no rows + if (noRows) + { + // Get the action links if the links callback is set + var links = null; + if (this._linkCallback) + { + links = this._linkCallback.call( + this._context, + {}, + 0, + "" + ); + } + this._selectionMgr.registerRow("",0,placeholder.get(0), links); + } + } + } + + /** + * Callback function used by the selection manager to translate the selected + * range to uids. + */ + _selectionFetchRange( _range, _callback, _context) + { + this._dataProvider.dataFetch( + { "start": _range.top, "num_rows": _range.bottom - _range.top + 1, + "no_data": true }, + function (_response) { + _callback.call(_context, _response.order); + }, + _context + ); + } + + /** + * Tells the grid to make the given index visible. + */ + _makeIndexVisible( _idx) + { + this._grid.makeIndexVisible(_idx); + } + +} + diff --git a/api/js/etemplate/et2_dataview_controller_selection.js b/api/js/etemplate/et2_dataview_controller_selection.js index 0ada4b5b67..0fd2fd7639 100644 --- a/api/js/etemplate/et2_dataview_controller_selection.js +++ b/api/js/etemplate/et2_dataview_controller_selection.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 * @@ -7,15 +8,14 @@ * @link http://www.egroupware.org * @author Andreas Stöckel * @copyright Stylite 2011-2012 - * @version $Id$ - */ + * /*egw:uses - et2_dataview_view_aoi; + et2_dataview_view_aoi; - egw_action.egw_keymanager; + egw_action.egw_keymanager; */ - +Object.defineProperty(exports, "__esModule", { value: true }); /** * The selectioManager is internally used by the et2_dataview_controller class * to manage the row selection. @@ -27,659 +27,491 @@ * * @augments Class */ -var et2_dataview_selectionManager = (function(){ "use strict"; return Class.extend( -{ - - // Maximum number of rows we can safely fetch for selection - // Actual selection may have more rows if we already have some - MAX_SELECTION: 1000, - - /** - * Constructor - * - * @param _parent - * @param _indexMap - * @param _actionObjectManager - * @param _queryRangeCallback - * @param _makeVisibleCallback - * @param _context - * @memberOf et2_dataview_selectionManager - */ - init: function (_parent, _indexMap, _actionObjectManager, - _queryRangeCallback, _makeVisibleCallback, _context) { - - // Copy the arguments - this._parent = _parent; - this._indexMap = _indexMap; - this._actionObjectManager = _actionObjectManager; - this._queryRangeCallback = _queryRangeCallback; - this._makeVisibleCallback = _makeVisibleCallback; - this._context = _context; - - // Attach this manager to the parent manager if one is given - if (this._parent) - { - this._parent._children.push(this); - } - - // Use our selection instead of object manager's to handle not-loaded rows - if(_actionObjectManager) - { - this._actionObjectManager.getAllSelected = jQuery.proxy( - this.getAllSelected, this - ); - } - - // Internal map which contains all curently selected uids and their - // state - this._registeredRows = {}; - this._focusedEntry = null; - this._invertSelection = false; - this._selectAll = false; - this._inUpdate = false; - this._total = 0; - this._children = []; - - // Callback for when the selection changes - this.select_callback = null; - }, - - destroy: function () { - - // If we have a parent, unregister from that - if (this._parent) - { - var idx = this._parent._children.indexOf(this); - this._parent._children.splice(idx, 1); - } - - // Destroy all children - for (var i = this._children.length - 1; i >= 0; i--) - { - this._children[i].free(); - } - - // Delete all still registered rows - for (var key in this._registeredRows) - { - this.unregisterRow(key, this._registeredRows[key].tr); - } - this.select_callback = null; - }, - - clear: function() { - for (var key in this._registeredRows) - { - this.unregisterRow(key, this._registeredRows[key].tr); - delete this._registeredRows[key]; - } - if(this._actionObjectManager) - { - this._actionObjectManager.clear(); - } - for (key in this._indexMap) { - delete this._indexMap[key]; - } - this._total = 0; - this._focusedEntry = null; - this._invertSelection = false; - this._selectAll = false; - this._inUpdate = false; - }, - - setIndexMap: function (_indexMap) { - this._indexMap = _indexMap; - }, - - setTotalCount: function (_total) { - this._total = _total; - }, - - registerRow: function (_uid, _idx, _tr, _links) { - - // Get the corresponding entry from the registered rows array - var entry = this._getRegisteredRowsEntry(_uid); - - // If the row has changed unregister the old one and do not delete - // entry from the entry map - if (entry.tr && entry.tr !== _tr) { - this.unregisterRow(_uid, entry.tr, true); - } - - // Create the AOI for the tr - if (!entry.tr && _links) - { - this._attachActionObjectInterface(entry, _tr, _uid); - this._attachActionObject(entry, _tr, _uid, _links, _idx); - } - - // Update the entry - if(entry.ao) entry.ao._index; - entry.idx = _idx; - entry.tr = _tr; - - // Update the visible state of the _tr - this._updateEntryState(entry, entry.state); - }, - - unregisterRow: function (_uid, _tr, _noDelete) { - - // _noDelete defaults to false - _noDelete = _noDelete ? true : false; - - if (typeof this._registeredRows[_uid] !== "undefined" - && this._registeredRows[_uid].tr === _tr) - { - this._inUpdate = true; - - this._registeredRows[_uid].tr = null; - this._registeredRows[_uid].aoi = null; - - // Remove the action object from its container - if (this._registeredRows[_uid].ao) - { - this._registeredRows[_uid].ao.remove(); - this._registeredRows[_uid].ao = null; - } - - if (!_noDelete - && this._registeredRows[_uid].state === EGW_AO_STATE_NORMAL) - { - delete this._registeredRows[_uid]; - } - - this._inUpdate = false; - } - }, - - resetSelection: function () { - this._invertSelection = false; - this._selectAll = false; - this._actionObjectManager.setAllSelected(false); - - for (var key in this._registeredRows) - { - this.setSelected(key, false); - } - for(var i = 0; i < this._children.length; i++) - { - this._children[i].resetSelection(); - } - }, - - setSelected: function (_uid, _selected) { - this._selectAll = false; - var entry = this._getRegisteredRowsEntry(_uid); - this._updateEntryState(entry, - egwSetBit(entry.state, EGW_AO_STATE_SELECTED, _selected)); - }, - - getAllSelected: function() - { - var selected = this.getSelected(); - return selected.all || (selected.ids.length === this._total); - }, - - setFocused: function (_uid, _focused) { - // Reset the state of the currently focused entry - if (this._focusedEntry) - { - this._updateEntryState(this._focusedEntry, - egwSetBit(this._focusedEntry.state, EGW_AO_STATE_FOCUSED, - false)); - this._focusedEntry = null; - } - // Mark the new given uid as focused - if (_focused) - { - //console.log('et2_dataview_controller_selection::setFocused -> UID:'+_uid+' is focused by:'+this._actionObjectManager.name); - var entry = this._focusedEntry = this._getRegisteredRowsEntry(_uid); - this._updateEntryState(entry, - egwSetBit(entry.state, EGW_AO_STATE_FOCUSED, true)); - } - }, - - selectAll: function () { - // Reset the selection - this.resetSelection(); - - this._selectAll = true; - - // Run as a range if there's less then the max - if(egw.dataKnownUIDs(this._context._dataProvider.dataStorePrefix).length !== this._total && - this._total <= this.MAX_SELECTION - ) - { - this._selectRange(0, this._total); - } - // Tell action manager to do all - this._actionObjectManager.setAllSelected(true); - - // Update the selection - for (var key in this._registeredRows) - { - var entry = this._registeredRows[key]; - this._updateEntryState(entry, entry.state); - } - - this._selectAll = true; - }, - - getSelected: function () { - // Collect all currently selected ids - var ids = []; - for (var key in this._registeredRows) - { - if (egwBitIsSet(this._registeredRows[key].state, EGW_AO_STATE_SELECTED)) - { - ids.push(key); - } - } - - // Push all events of the child managers onto the list - for (var i = 0; i < this._children.length; i++) - { - ids = ids.concat(this._children[i].getSelected().ids); - } - - // Return an array containing those ids - // RB: we are currently NOT using "inverted" - return { - //"inverted": this._invertSelection, - "all": this._selectAll, - "ids": ids - }; - }, - - - /** -- PRIVATE FUNCTIONS -- **/ - - - _attachActionObjectInterface: function (_entry, _tr, _uid) { - // Create the AOI which is used internally in the selection manager - // this AOI is not connected to the AO, as the selection manager - // cares about selection etc. - _entry.aoi = new et2_dataview_rowAOI(_tr); - _entry.aoi.setStateChangeCallback( - function (_newState, _changedBit, _shiftState) { - if (_changedBit === EGW_AO_STATE_SELECTED) - { - // Call the select handler - this._handleSelect( - _uid, - _entry, - egwBitIsSet(_shiftState, EGW_AO_SHIFT_STATE_BLOCK), - egwBitIsSet(_shiftState, EGW_AO_SHIFT_STATE_MULTI) - ); - } - }, this); - }, - - _getDummyAOI: function (_entry, _tr, _uid, _idx) { - // Create AOI - var dummyAOI = new egwActionObjectInterface(); - var self = this; - - // Handling for Action Implementations updating the state - dummyAOI.doSetState = function (_state) { - if (!self._inUpdate) - { - // Update the "focused" flag - self.setFocused(_uid, egwBitIsSet(_state, EGW_AO_STATE_FOCUSED)); - - // Generally update the state - self._updateState(_uid, _state); - } - }; - - // Handle the "make visible" event, pass the request to the parent - // controller - dummyAOI.doMakeVisible = function () { - self._makeVisibleCallback.call(self._context, _idx); - }; - - // Connect the the two AOIs - dummyAOI.doTriggerEvent = _entry.aoi.doTriggerEvent; - - // Implementation of the getDOMNode function, so that the event - // handlers can be properly bound - dummyAOI.getDOMNode = function () { return _tr; }; - - return dummyAOI; - }, - - _attachActionObject: function (_entry, _tr, _uid, _links, _idx) { - - // Get the dummyAOI which connects the action object to the tr but - // does no selection handling - var dummyAOI = this._getDummyAOI(_entry, _tr, _uid, _idx); - - // Create an action object for the tr and connect it to a dummy AOI - if(this._actionObjectManager) - { - if(this._actionObjectManager.getObjectById(_uid)) - { - var state = _entry.state; - this._actionObjectManager.getObjectById(_uid).remove(); - _entry.state = state; - } - _entry.ao = this._actionObjectManager.addObject(_uid, dummyAOI); - } - - // Force context (actual widget) in here, it's the last place it's available - _entry.ao._context = this._context; - _entry.ao.updateActionLinks(_links); - _entry.ao._index = _idx; - - // Overwrite some functions like "traversePath", "getNext" and - // "getPrevious" - var self = this; - - function getIndexAO (_idx) { - // Check whether the index is in the index map - if (typeof self._indexMap[_idx] !== "undefined" - && self._indexMap[_idx].uid) - { - return self._getRegisteredRowsEntry(self._indexMap[_idx].uid).ao; - } - - return null; - } - - function getElementRelatively (_step) { - var total = self._total || Object.keys(self._indexMap).length; - var max_index = Math.max.apply(Math,Object.keys(self._indexMap)); - // Get a reasonable number of iterations - not all - var count = Math.max(1,Math.min(self._total,50)); - var element = null; - var idx = _entry.idx; - while(element == null && count > 0 && max_index > 0) - { - count--; - element = getIndexAO(Math.max(0, - Math.min(max_index, idx += _step))); - } - return element; - }; - - _entry.ao.getPrevious = function (_step) { - return getElementRelatively(-_step); - }; - - _entry.ao.getNext = function (_step) { - return getElementRelatively(_step); - }; - - _entry.ao.traversePath = function (_obj) { - // Get the start and the stop index - var s = Math.min(this._index, _obj._index); - var e = Math.max(this._index, _obj._index); - - var result = []; - - for (var i = s; i < e; i++) - { - var ao = getIndexAO(i); - if (ao) - { - result.push(ao); - } - } - - return result; - }; - }, - - _updateState: function (_uid, _state) { - var entry = this._getRegisteredRowsEntry(_uid); - - this._updateEntryState(entry, _state); - - return entry; - }, - - _updateEntryState: function (_entry, _state) { - - if (this._selectAll) - { - _state |= EGW_AO_STATE_SELECTED; - } - else if (this._invertSelection) - { - _state ^= EGW_AO_STATE_SELECTED; - } - - // Attach ao if not there, happens for rows loaded for selection, but - // not displayed yet - if(!_entry.ao && _entry.uid && this._actionObjectManager) - { - var _links = []; - for (var key in this._registeredRows) - { - if(this._registeredRows[key].ao && this._registeredRows[key].ao.actionLinks) - { - _links = this._registeredRows[key].ao.actionLinks; - break; - } - } - if(_links) - { - this._attachActionObjectInterface(_entry, null, _entry.uid); - this._attachActionObject(_entry, null, _entry.uid, _links, _entry.idx); - } - } - - // Update the state if it has changed - if ((_entry.aoi && _entry.aoi.getState() !== _state) || _entry.state != _state) - { - this._inUpdate = true; // Recursion prevention - - // Update the state of the action object - if (_entry.ao) - { - _entry.ao.setSelected(egwBitIsSet(_state, EGW_AO_STATE_SELECTED)); - _entry.ao.setFocused(egwBitIsSet(_state, EGW_AO_STATE_FOCUSED)); - } - - this._inUpdate = false; - - // Delete the element if state was set to "NORMAL" and there is - // no tr - if (_state === EGW_AO_STATE_NORMAL && !_entry.tr) - { - delete this._registeredRows[_entry.uid]; - } - } - - // Update the visual state - if (_entry.aoi && _entry.aoi.doSetState) - { - _entry.aoi.doSetState(_state); - } - - // Update the state of the entry - _entry.state = _state; - }, - - _getRegisteredRowsEntry: function (_uid) { - if (typeof this._registeredRows[_uid] === "undefined") - { - this._registeredRows[_uid] = { - "uid": _uid, - "idx": null, - "state": EGW_AO_STATE_NORMAL, - "tr": null, - "aoi": null, - "ao": null - }; - } - - return this._registeredRows[_uid]; - }, - - _handleSelect: function (_uid, _entry, _shift, _ctrl) { - // If not "_ctrl" is set, reset the selection - if (!_ctrl) - { - var top = this; - while(top._parent !== null) - { - top = top._parent; - } - top.resetSelection(); - this._actionObjectManager.setAllSelected(false); // needed for hirachical stuff - } - - // Mark the element that was clicked as selected - var entry = this._getRegisteredRowsEntry(_uid); - this.setSelected(_uid, - !_ctrl || !egwBitIsSet(entry.state, EGW_AO_STATE_SELECTED)); - - // Focus the element if shift is not pressed - if (!_shift) - { - this.setFocused(_uid, true); - } - else if (this._focusedEntry) - { - this._selectRange(this._focusedEntry.idx, _entry.idx); - } - - if(this.select_callback && typeof this.select_callback == "function") - { - this.select_callback.apply(this._context, arguments); - } - }, - - _selectRange: function (_start, _stop) { - // Contains ranges that are not currently in the index map and that have - // to be queried - var queryRanges = []; - - // Iterate over the given range and select the elements in the range - // from _start to _stop - var naStart = false; - var s = Math.min(_start, _stop); - var e = Math.max(_stop, _start); - var RANGE_MAX = 50; - var range_break = s + RANGE_MAX; - for (var i = s; i <= e; i++) - { - if (typeof this._indexMap[i] !== "undefined" && - this._indexMap[i].uid && egw.dataGetUIDdata(this._indexMap[i].uid)) - { - // Add the range to the "queryRanges" - if (naStart !== false) - { - queryRanges.push(et2_bounds(naStart, i - 1)); - naStart = false; - range_break += RANGE_MAX; - } - - // Select the element, unless flagged for exclusion - // Check for no_actions flag via data - var data = egw.dataGetUIDdata(this._indexMap[i].uid); - if(data && data.data && !data.data.no_actions) - { - this.setSelected(this._indexMap[i].uid, true); - } - } - else if (naStart === false) - { - naStart = i; - range_break = naStart + RANGE_MAX; - } - else if(i >= range_break) - { - queryRanges.push(et2_bounds(naStart ? naStart : s, i - 1)); - naStart = i; - range_break += RANGE_MAX; - } - } - - // Add the last range to the "queryRanges" - if (naStart !== false) - { - queryRanges.push(et2_bounds(naStart, i - 1)); - naStart = false; - } - - // Query all unknown ranges from the server - if(queryRanges.length > 0) - { - this._query_ranges(queryRanges); - } - }, - - _query_ranges: function _query_ranges(queryRanges) - { - var that = this; - var record_count = 0; - var range_index = 0; - var range_count = queryRanges.length; - var cont = true; - var fetchPromise = new Promise(function (resolve) { - resolve(); - }); - // Found after dialog loads - var progressbar; - - var parent = et2_dialog._create_parent(); - var dialog = et2_createWidget("dialog", { - callback: - // Abort the long task if they canceled the data load - function() {cont = false}, - template: egw.webserverUrl+'/api/templates/default/long_task.xet', - message: egw.lang('Loading'), - title: egw.lang('please wait...'), - buttons: [{"button_id": et2_dialog.CANCEL_BUTTON,"text": egw.lang('cancel'),id: 'dialog[cancel]',image: 'cancel'}], - width: 300 - }, parent); - jQuery(dialog.template.DOMContainer).on('load', function() { - // Get access to template widgets - progressbar = dialog.template.widgetContainer.getWidgetById('progressbar'); - }); - - for (var i = 0; i < queryRanges.length; i++) - { - if(record_count + (queryRanges[i].bottom - queryRanges[i].top+1) > that.MAX_SELECTION) - { - egw.message(egw.lang('Too many rows selected.
Select all, or less than %1 rows', that.MAX_SELECTION)); - break; - } - else - { - record_count += (queryRanges[i].bottom - queryRanges[i].top+1); - fetchPromise = fetchPromise.then((function () - { - // Check for abort - if(!cont) return; - - return new Promise(function(resolve) { - that._queryRangeCallback.call(that._context, this, - function (_order) { - for (var j = 0; j < _order.length; j++) - { - // Check for no_actions flag via data since entry isn't there/available - var data = egw.dataGetUIDdata(_order[j]); - if(!data || data && data.data && !data.data.no_actions) - { - var entry = this._getRegisteredRowsEntry(_order[j]); - this._updateEntryState(entry, - egwSetBit(entry.state, EGW_AO_STATE_SELECTED, true)); - } - } - progressbar.set_value(100*(++range_index/range_count)); - resolve(); - }, that); - }.bind(this)); - }).bind(queryRanges[i])); - } - } - fetchPromise.finally(function() { - dialog.destroy(); - }); - } - -});}).call(this); - +var et2_dataview_selectionManager = /** @class */ (function () { + /** + * Constructor + * + * @param _parent + * @param _indexMap + * @param _actionObjectManager + * @param _queryRangeCallback + * @param _makeVisibleCallback + * @param _context + * @memberOf et2_dataview_selectionManager + */ + function et2_dataview_selectionManager(_parent, _indexMap, _actionObjectManager, _queryRangeCallback, _makeVisibleCallback, _context) { + // Copy the arguments + this._parent = _parent; + this._indexMap = _indexMap; + this._actionObjectManager = _actionObjectManager; + this._queryRangeCallback = _queryRangeCallback; + this._makeVisibleCallback = _makeVisibleCallback; + this._context = _context; + // Attach this manager to the parent manager if one is given + if (this._parent) { + this._parent._children.push(this); + } + // Use our selection instead of object manager's to handle not-loaded rows + if (_actionObjectManager) { + this._actionObjectManager.getAllSelected = jQuery.proxy(this.getAllSelected, this); + } + // Internal map which contains all curently selected uids and their + // state + this._registeredRows = {}; + this._focusedEntry = null; + this._invertSelection = false; + this._selectAll = false; + this._inUpdate = false; + this._total = 0; + this._children = []; + // Callback for when the selection changes + this.select_callback = null; + } + et2_dataview_selectionManager.prototype.destroy = function () { + // If we have a parent, unregister from that + if (this._parent) { + var idx = this._parent._children.indexOf(this); + this._parent._children.splice(idx, 1); + } + // Destroy all children + for (var i = this._children.length - 1; i >= 0; i--) { + this._children[i].destroy(); + } + // Delete all still registered rows + for (var key in this._registeredRows) { + this.unregisterRow(key, this._registeredRows[key].tr); + } + this.select_callback = null; + }; + et2_dataview_selectionManager.prototype.clear = function () { + for (var key in this._registeredRows) { + this.unregisterRow(key, this._registeredRows[key].tr); + delete this._registeredRows[key]; + } + if (this._actionObjectManager) { + this._actionObjectManager.clear(); + } + for (key in this._indexMap) { + delete this._indexMap[key]; + } + this._total = 0; + this._focusedEntry = null; + this._invertSelection = false; + this._selectAll = false; + this._inUpdate = false; + }; + et2_dataview_selectionManager.prototype.setIndexMap = function (_indexMap) { + this._indexMap = _indexMap; + }; + et2_dataview_selectionManager.prototype.setTotalCount = function (_total) { + this._total = _total; + }; + et2_dataview_selectionManager.prototype.registerRow = function (_uid, _idx, _tr, _links) { + // Get the corresponding entry from the registered rows array + var entry = this._getRegisteredRowsEntry(_uid); + // If the row has changed unregister the old one and do not delete + // entry from the entry map + if (entry.tr && entry.tr !== _tr) { + this.unregisterRow(_uid, entry.tr, true); + } + // Create the AOI for the tr + if (!entry.tr && _links) { + this._attachActionObjectInterface(entry, _tr, _uid); + this._attachActionObject(entry, _tr, _uid, _links, _idx); + } + // Update the entry + if (entry.ao) + entry.ao._index; + entry.idx = _idx; + entry.tr = _tr; + // Update the visible state of the _tr + this._updateEntryState(entry, entry.state); + }; + et2_dataview_selectionManager.prototype.unregisterRow = function (_uid, _tr, _noDelete) { + // _noDelete defaults to false + _noDelete = _noDelete ? true : false; + if (typeof this._registeredRows[_uid] !== "undefined" + && this._registeredRows[_uid].tr === _tr) { + this._inUpdate = true; + this._registeredRows[_uid].tr = null; + this._registeredRows[_uid].aoi = null; + // Remove the action object from its container + if (this._registeredRows[_uid].ao) { + this._registeredRows[_uid].ao.remove(); + this._registeredRows[_uid].ao = null; + } + if (!_noDelete + && this._registeredRows[_uid].state === EGW_AO_STATE_NORMAL) { + delete this._registeredRows[_uid]; + } + this._inUpdate = false; + } + }; + et2_dataview_selectionManager.prototype.resetSelection = function () { + this._invertSelection = false; + this._selectAll = false; + this._actionObjectManager.setAllSelected(false); + for (var key in this._registeredRows) { + this.setSelected(key, false); + } + for (var i = 0; i < this._children.length; i++) { + this._children[i].resetSelection(); + } + }; + et2_dataview_selectionManager.prototype.setSelected = function (_uid, _selected) { + this._selectAll = false; + var entry = this._getRegisteredRowsEntry(_uid); + this._updateEntryState(entry, egwSetBit(entry.state, EGW_AO_STATE_SELECTED, _selected)); + }; + et2_dataview_selectionManager.prototype.getAllSelected = function () { + var selected = this.getSelected(); + return selected.all || (selected.ids.length === this._total); + }; + et2_dataview_selectionManager.prototype.setFocused = function (_uid, _focused) { + // Reset the state of the currently focused entry + if (this._focusedEntry) { + this._updateEntryState(this._focusedEntry, egwSetBit(this._focusedEntry.state, EGW_AO_STATE_FOCUSED, false)); + this._focusedEntry = null; + } + // Mark the new given uid as focused + if (_focused) { + //console.log('et2_dataview_controller_selection::setFocused -> UID:'+_uid+' is focused by:'+this._actionObjectManager.name); + var entry = this._focusedEntry = this._getRegisteredRowsEntry(_uid); + this._updateEntryState(entry, egwSetBit(entry.state, EGW_AO_STATE_FOCUSED, true)); + } + }; + et2_dataview_selectionManager.prototype.selectAll = function () { + // Reset the selection + this.resetSelection(); + this._selectAll = true; + // Run as a range if there's less then the max + if (egw.dataKnownUIDs(this._context._dataProvider.dataStorePrefix).length !== this._total && + this._total <= et2_dataview_selectionManager.MAX_SELECTION) { + this._selectRange(0, this._total); + } + // Tell action manager to do all + this._actionObjectManager.setAllSelected(true); + // Update the selection + for (var key in this._registeredRows) { + var entry = this._registeredRows[key]; + this._updateEntryState(entry, entry.state); + } + this._selectAll = true; + }; + et2_dataview_selectionManager.prototype.getSelected = function () { + // Collect all currently selected ids + var ids = []; + for (var key in this._registeredRows) { + if (egwBitIsSet(this._registeredRows[key].state, EGW_AO_STATE_SELECTED)) { + ids.push(key); + } + } + // Push all events of the child managers onto the list + for (var i = 0; i < this._children.length; i++) { + ids = ids.concat(this._children[i].getSelected().ids); + } + // Return an array containing those ids + // RB: we are currently NOT using "inverted" + return { + //"inverted": this._invertSelection, + "all": this._selectAll, + "ids": ids + }; + }; + /** -- PRIVATE FUNCTIONS -- **/ + et2_dataview_selectionManager.prototype._attachActionObjectInterface = function (_entry, _tr, _uid) { + // Create the AOI which is used internally in the selection manager + // this AOI is not connected to the AO, as the selection manager + // cares about selection etc. + _entry.aoi = new et2_dataview_rowAOI(_tr); + _entry.aoi.setStateChangeCallback(function (_newState, _changedBit, _shiftState) { + if (_changedBit === EGW_AO_STATE_SELECTED) { + // Call the select handler + this._handleSelect(_uid, _entry, egwBitIsSet(_shiftState, EGW_AO_SHIFT_STATE_BLOCK), egwBitIsSet(_shiftState, EGW_AO_SHIFT_STATE_MULTI)); + } + }, this); + }; + et2_dataview_selectionManager.prototype._getDummyAOI = function (_entry, _tr, _uid, _idx) { + // Create AOI + var dummyAOI = new egwActionObjectInterface(); + var self = this; + // Handling for Action Implementations updating the state + dummyAOI.doSetState = function (_state) { + if (!self._inUpdate) { + // Update the "focused" flag + self.setFocused(_uid, egwBitIsSet(_state, EGW_AO_STATE_FOCUSED)); + // Generally update the state + self._updateState(_uid, _state); + } + }; + // Handle the "make visible" event, pass the request to the parent + // controller + dummyAOI.doMakeVisible = function () { + self._makeVisibleCallback.call(self._context, _idx); + }; + // Connect the the two AOIs + dummyAOI.doTriggerEvent = _entry.aoi.doTriggerEvent; + // Implementation of the getDOMNode function, so that the event + // handlers can be properly bound + dummyAOI.getDOMNode = function () { return _tr; }; + return dummyAOI; + }; + et2_dataview_selectionManager.prototype._attachActionObject = function (_entry, _tr, _uid, _links, _idx) { + // Get the dummyAOI which connects the action object to the tr but + // does no selection handling + var dummyAOI = this._getDummyAOI(_entry, _tr, _uid, _idx); + // Create an action object for the tr and connect it to a dummy AOI + if (this._actionObjectManager) { + if (this._actionObjectManager.getObjectById(_uid)) { + var state = _entry.state; + this._actionObjectManager.getObjectById(_uid).remove(); + _entry.state = state; + } + _entry.ao = this._actionObjectManager.addObject(_uid, dummyAOI); + } + // Force context (actual widget) in here, it's the last place it's available + _entry.ao._context = this._context; + _entry.ao.updateActionLinks(_links); + _entry.ao._index = _idx; + // Overwrite some functions like "traversePath", "getNext" and + // "getPrevious" + var self = this; + function getIndexAO(_idx) { + // Check whether the index is in the index map + if (typeof self._indexMap[_idx] !== "undefined" + && self._indexMap[_idx].uid) { + return self._getRegisteredRowsEntry(self._indexMap[_idx].uid).ao; + } + return null; + } + function getElementRelatively(_step) { + var total = self._total || Object.keys(self._indexMap).length; + var max_index = Math.max.apply(Math, Object.keys(self._indexMap)); + // Get a reasonable number of iterations - not all + var count = Math.max(1, Math.min(self._total, 50)); + var element = null; + var idx = _entry.idx; + while (element == null && count > 0 && max_index > 0) { + count--; + element = getIndexAO(Math.max(0, Math.min(max_index, idx += _step))); + } + return element; + } + _entry.ao.getPrevious = function (_step) { + return getElementRelatively(-_step); + }; + _entry.ao.getNext = function (_step) { + return getElementRelatively(_step); + }; + _entry.ao.traversePath = function (_obj) { + // Get the start and the stop index + var s = Math.min(this._index, _obj._index); + var e = Math.max(this._index, _obj._index); + var result = []; + for (var i = s; i < e; i++) { + var ao = getIndexAO(i); + if (ao) { + result.push(ao); + } + } + return result; + }; + }; + et2_dataview_selectionManager.prototype._updateState = function (_uid, _state) { + var entry = this._getRegisteredRowsEntry(_uid); + this._updateEntryState(entry, _state); + return entry; + }; + et2_dataview_selectionManager.prototype._updateEntryState = function (_entry, _state) { + if (this._selectAll) { + _state |= EGW_AO_STATE_SELECTED; + } + else if (this._invertSelection) { + _state ^= EGW_AO_STATE_SELECTED; + } + // Attach ao if not there, happens for rows loaded for selection, but + // not displayed yet + if (!_entry.ao && _entry.uid && this._actionObjectManager) { + var _links = []; + for (var key in this._registeredRows) { + if (this._registeredRows[key].ao && this._registeredRows[key].ao.actionLinks) { + _links = this._registeredRows[key].ao.actionLinks; + break; + } + } + if (_links) { + this._attachActionObjectInterface(_entry, null, _entry.uid); + this._attachActionObject(_entry, null, _entry.uid, _links, _entry.idx); + } + } + // Update the state if it has changed + if ((_entry.aoi && _entry.aoi.getState() !== _state) || _entry.state != _state) { + this._inUpdate = true; // Recursion prevention + // Update the state of the action object + if (_entry.ao) { + _entry.ao.setSelected(egwBitIsSet(_state, EGW_AO_STATE_SELECTED)); + _entry.ao.setFocused(egwBitIsSet(_state, EGW_AO_STATE_FOCUSED)); + } + this._inUpdate = false; + // Delete the element if state was set to "NORMAL" and there is + // no tr + if (_state === EGW_AO_STATE_NORMAL && !_entry.tr) { + delete this._registeredRows[_entry.uid]; + } + } + // Update the visual state + if (_entry.aoi && _entry.aoi.doSetState) { + _entry.aoi.doSetState(_state); + } + // Update the state of the entry + _entry.state = _state; + }; + et2_dataview_selectionManager.prototype._getRegisteredRowsEntry = function (_uid) { + if (typeof this._registeredRows[_uid] === "undefined") { + this._registeredRows[_uid] = { + "uid": _uid, + "idx": null, + "state": EGW_AO_STATE_NORMAL, + "tr": null, + "aoi": null, + "ao": null + }; + } + return this._registeredRows[_uid]; + }; + et2_dataview_selectionManager.prototype._handleSelect = function (_uid, _entry, _shift, _ctrl) { + // If not "_ctrl" is set, reset the selection + if (!_ctrl) { + var top = this; + while (top._parent !== null) { + top = top._parent; + } + top.resetSelection(); + this._actionObjectManager.setAllSelected(false); // needed for hirachical stuff + } + // Mark the element that was clicked as selected + var entry = this._getRegisteredRowsEntry(_uid); + this.setSelected(_uid, !_ctrl || !egwBitIsSet(entry.state, EGW_AO_STATE_SELECTED)); + // Focus the element if shift is not pressed + if (!_shift) { + this.setFocused(_uid, true); + } + else if (this._focusedEntry) { + this._selectRange(this._focusedEntry.idx, _entry.idx); + } + if (this.select_callback && typeof this.select_callback == "function") { + this.select_callback.apply(this._context, arguments); + } + }; + et2_dataview_selectionManager.prototype._selectRange = function (_start, _stop) { + // Contains ranges that are not currently in the index map and that have + // to be queried + var queryRanges = []; + // Iterate over the given range and select the elements in the range + // from _start to _stop + var naStart = false; + var s = Math.min(_start, _stop); + var e = Math.max(_stop, _start); + var RANGE_MAX = 50; + var range_break = s + RANGE_MAX; + for (var i = s; i <= e; i++) { + if (typeof this._indexMap[i] !== "undefined" && + this._indexMap[i].uid && egw.dataGetUIDdata(this._indexMap[i].uid)) { + // Add the range to the "queryRanges" + if (naStart !== false) { + queryRanges.push(et2_bounds(naStart, i - 1)); + naStart = false; + range_break += RANGE_MAX; + } + // Select the element, unless flagged for exclusion + // Check for no_actions flag via data + var data = egw.dataGetUIDdata(this._indexMap[i].uid); + if (data && data.data && !data.data.no_actions) { + this.setSelected(this._indexMap[i].uid, true); + } + } + else if (naStart === false) { + naStart = i; + range_break = naStart + RANGE_MAX; + } + else if (i >= range_break) { + queryRanges.push(et2_bounds(naStart ? naStart : s, i - 1)); + naStart = i; + range_break += RANGE_MAX; + } + } + // Add the last range to the "queryRanges" + if (naStart !== false) { + queryRanges.push(et2_bounds(naStart, i - 1)); + naStart = false; + } + // Query all unknown ranges from the server + if (queryRanges.length > 0) { + this._query_ranges(queryRanges); + } + }; + et2_dataview_selectionManager.prototype._query_ranges = function (queryRanges) { + var that = this; + var record_count = 0; + var range_index = 0; + var range_count = queryRanges.length; + var cont = true; + var fetchPromise = new Promise(function (resolve) { + resolve(); + }); + // Found after dialog loads + var progressbar; + var parent = et2_dialog._create_parent(); + var dialog = et2_createWidget("dialog", { + callback: + // Abort the long task if they canceled the data load + function () { cont = false; }, + template: egw.webserverUrl + '/api/templates/default/long_task.xet', + message: egw.lang('Loading'), + title: egw.lang('please wait...'), + buttons: [{ "button_id": et2_dialog.CANCEL_BUTTON, "text": egw.lang('cancel'), id: 'dialog[cancel]', image: 'cancel' }], + width: 300 + }, parent); + jQuery(dialog.template.DOMContainer).on('load', function () { + // Get access to template widgets + progressbar = dialog.template.widgetContainer.getWidgetById('progressbar'); + }); + for (var i = 0; i < queryRanges.length; i++) { + if (record_count + (queryRanges[i].bottom - queryRanges[i].top + 1) > that.MAX_SELECTION) { + egw.message(egw.lang('Too many rows selected.
Select all, or less than %1 rows', that.MAX_SELECTION)); + break; + } + else { + record_count += (queryRanges[i].bottom - queryRanges[i].top + 1); + fetchPromise = fetchPromise.then((function () { + // Check for abort + if (!cont) + return; + return new Promise(function (resolve) { + that._queryRangeCallback.call(that._context, this, function (_order) { + for (var j = 0; j < _order.length; j++) { + // Check for no_actions flag via data since entry isn't there/available + var data = egw.dataGetUIDdata(_order[j]); + if (!data || data && data.data && !data.data.no_actions) { + var entry = this._getRegisteredRowsEntry(_order[j]); + this._updateEntryState(entry, egwSetBit(entry.state, EGW_AO_STATE_SELECTED, true)); + } + } + progressbar.set_value(100 * (++range_index / range_count)); + resolve(); + }, that); + }.bind(this)); + }).bind(queryRanges[i])); + } + } + fetchPromise.finally(function () { + dialog.destroy(); + }); + }; + // Maximum number of rows we can safely fetch for selection + // Actual selection may have more rows if we already have some + et2_dataview_selectionManager.MAX_SELECTION = 1000; + return et2_dataview_selectionManager; +}()); +exports.et2_dataview_selectionManager = et2_dataview_selectionManager; +//# sourceMappingURL=et2_dataview_controller_selection.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_dataview_controller_selection.ts b/api/js/etemplate/et2_dataview_controller_selection.ts new file mode 100644 index 0000000000..e9907da391 --- /dev/null +++ b/api/js/etemplate/et2_dataview_controller_selection.ts @@ -0,0 +1,720 @@ +/** + * EGroupware eTemplate2 + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage dataview + * @link http://www.egroupware.org + * @author Andreas Stöckel + * @copyright Stylite 2011-2012 + * + +/*egw:uses + et2_dataview_view_aoi; + + egw_action.egw_keymanager; +*/ + +/** + * The selectioManager is internally used by the et2_dataview_controller class + * to manage the row selection. + * As the action system does not allow selection of entries which are currently + * not in the dom tree, we have to manage this in this class. The idea is to + * manage an external action object interface for each visible row and proxy all + * state changes between an dummy action object, that does no selection handling, + * and the external action object interface. + * + * @augments Class + */ +export class et2_dataview_selectionManager +{ + + // Maximum number of rows we can safely fetch for selection + // Actual selection may have more rows if we already have some + public static readonly MAX_SELECTION = 1000; + + private _parent: any; + private _indexMap: any; + private _actionObjectManager: any; + private _makeVisibleCallback: any; + private _queryRangeCallback: any; + private select_callback: null; + + private _context: any; + private _registeredRows: {}; + private _focusedEntry: null; + private _invertSelection: boolean; + private _selectAll: boolean; + private _inUpdate: boolean; + private _total: number; + private _children: any[]; + + /** + * Constructor + * + * @param _parent + * @param _indexMap + * @param _actionObjectManager + * @param _queryRangeCallback + * @param _makeVisibleCallback + * @param _context + * @memberOf et2_dataview_selectionManager + */ + constructor(_parent, _indexMap, _actionObjectManager, + _queryRangeCallback, _makeVisibleCallback, _context) + { + + // Copy the arguments + this._parent = _parent; + this._indexMap = _indexMap; + this._actionObjectManager = _actionObjectManager; + this._queryRangeCallback = _queryRangeCallback; + this._makeVisibleCallback = _makeVisibleCallback; + this._context = _context; + + // Attach this manager to the parent manager if one is given + if (this._parent) + { + this._parent._children.push(this); + } + + // Use our selection instead of object manager's to handle not-loaded rows + if(_actionObjectManager) + { + this._actionObjectManager.getAllSelected = jQuery.proxy( + this.getAllSelected, this + ); + } + + // Internal map which contains all curently selected uids and their + // state + this._registeredRows = {}; + this._focusedEntry = null; + this._invertSelection = false; + this._selectAll = false; + this._inUpdate = false; + this._total = 0; + this._children = []; + + // Callback for when the selection changes + this.select_callback = null; + } + + destroy( ) + { + + // If we have a parent, unregister from that + if (this._parent) + { + var idx = this._parent._children.indexOf(this); + this._parent._children.splice(idx, 1); + } + + // Destroy all children + for (var i = this._children.length - 1; i >= 0; i--) + { + this._children[i].destroy(); + } + + // Delete all still registered rows + for (var key in this._registeredRows) + { + this.unregisterRow(key, this._registeredRows[key].tr); + } + this.select_callback = null; + } + + clear( ) + { + for (var key in this._registeredRows) + { + this.unregisterRow(key, this._registeredRows[key].tr); + delete this._registeredRows[key]; + } + if(this._actionObjectManager) + { + this._actionObjectManager.clear(); + } + for (key in this._indexMap) { + delete this._indexMap[key]; + } + this._total = 0; + this._focusedEntry = null; + this._invertSelection = false; + this._selectAll = false; + this._inUpdate = false; + } + + setIndexMap( _indexMap) + { + this._indexMap = _indexMap; + } + + setTotalCount( _total) + { + this._total = _total; + } + + registerRow( _uid, _idx, _tr, _links) + { + + // Get the corresponding entry from the registered rows array + var entry = this._getRegisteredRowsEntry(_uid); + + // If the row has changed unregister the old one and do not delete + // entry from the entry map + if (entry.tr && entry.tr !== _tr) { + this.unregisterRow(_uid, entry.tr, true); + } + + // Create the AOI for the tr + if (!entry.tr && _links) + { + this._attachActionObjectInterface(entry, _tr, _uid); + this._attachActionObject(entry, _tr, _uid, _links, _idx); + } + + // Update the entry + if(entry.ao) entry.ao._index; + entry.idx = _idx; + entry.tr = _tr; + + // Update the visible state of the _tr + this._updateEntryState(entry, entry.state); + } + + unregisterRow( _uid, _tr, _noDelete? : boolean) + { + + // _noDelete defaults to false + _noDelete = _noDelete ? true : false; + + if (typeof this._registeredRows[_uid] !== "undefined" + && this._registeredRows[_uid].tr === _tr) + { + this._inUpdate = true; + + this._registeredRows[_uid].tr = null; + this._registeredRows[_uid].aoi = null; + + // Remove the action object from its container + if (this._registeredRows[_uid].ao) + { + this._registeredRows[_uid].ao.remove(); + this._registeredRows[_uid].ao = null; + } + + if (!_noDelete + && this._registeredRows[_uid].state === EGW_AO_STATE_NORMAL) + { + delete this._registeredRows[_uid]; + } + + this._inUpdate = false; + } + } + + resetSelection( ) + { + this._invertSelection = false; + this._selectAll = false; + this._actionObjectManager.setAllSelected(false); + + for (var key in this._registeredRows) + { + this.setSelected(key, false); + } + for(var i = 0; i < this._children.length; i++) + { + this._children[i].resetSelection(); + } + } + + setSelected( _uid, _selected) + { + this._selectAll = false; + var entry = this._getRegisteredRowsEntry(_uid); + this._updateEntryState(entry, + egwSetBit(entry.state, EGW_AO_STATE_SELECTED, _selected)); + } + + getAllSelected() + { + var selected = this.getSelected(); + return selected.all || (selected.ids.length === this._total); + } + + setFocused( _uid, _focused) + { + // Reset the state of the currently focused entry + if (this._focusedEntry) + { + this._updateEntryState(this._focusedEntry, + egwSetBit(this._focusedEntry.state, EGW_AO_STATE_FOCUSED, + false)); + this._focusedEntry = null; + } + // Mark the new given uid as focused + if (_focused) + { + //console.log('et2_dataview_controller_selection::setFocused -> UID:'+_uid+' is focused by:'+this._actionObjectManager.name); + var entry = this._focusedEntry = this._getRegisteredRowsEntry(_uid); + this._updateEntryState(entry, + egwSetBit(entry.state, EGW_AO_STATE_FOCUSED, true)); + } + } + + selectAll( ) + { + // Reset the selection + this.resetSelection(); + + this._selectAll = true; + + // Run as a range if there's less then the max + if(egw.dataKnownUIDs(this._context._dataProvider.dataStorePrefix).length !== this._total && + this._total <= et2_dataview_selectionManager.MAX_SELECTION + ) + { + this._selectRange(0, this._total); + } + // Tell action manager to do all + this._actionObjectManager.setAllSelected(true); + + // Update the selection + for (var key in this._registeredRows) + { + var entry = this._registeredRows[key]; + this._updateEntryState(entry, entry.state); + } + + this._selectAll = true; + } + + getSelected( ) + { + // Collect all currently selected ids + var ids = []; + for (var key in this._registeredRows) + { + if (egwBitIsSet(this._registeredRows[key].state, EGW_AO_STATE_SELECTED)) + { + ids.push(key); + } + } + + // Push all events of the child managers onto the list + for (var i = 0; i < this._children.length; i++) + { + ids = ids.concat(this._children[i].getSelected().ids); + } + + // Return an array containing those ids + // RB: we are currently NOT using "inverted" + return { + //"inverted": this._invertSelection, + "all": this._selectAll, + "ids": ids + }; + } + + + /** -- PRIVATE FUNCTIONS -- **/ + + + _attachActionObjectInterface( _entry, _tr, _uid) + { + // Create the AOI which is used internally in the selection manager + // this AOI is not connected to the AO, as the selection manager + // cares about selection etc. + _entry.aoi = new et2_dataview_rowAOI(_tr); + _entry.aoi.setStateChangeCallback( + function (_newState, _changedBit, _shiftState) { + if (_changedBit === EGW_AO_STATE_SELECTED) + { + // Call the select handler + this._handleSelect( + _uid, + _entry, + egwBitIsSet(_shiftState, EGW_AO_SHIFT_STATE_BLOCK), + egwBitIsSet(_shiftState, EGW_AO_SHIFT_STATE_MULTI) + ); + } + }, this); + } + + _getDummyAOI( _entry, _tr, _uid, _idx) + { + // Create AOI + var dummyAOI = new egwActionObjectInterface(); + var self = this; + + // Handling for Action Implementations updating the state + dummyAOI.doSetState = function (_state) { + if (!self._inUpdate) + { + // Update the "focused" flag + self.setFocused(_uid, egwBitIsSet(_state, EGW_AO_STATE_FOCUSED)); + + // Generally update the state + self._updateState(_uid, _state); + } + }; + + // Handle the "make visible" event, pass the request to the parent + // controller + dummyAOI.doMakeVisible = function () { + self._makeVisibleCallback.call(self._context, _idx); + }; + + // Connect the the two AOIs + dummyAOI.doTriggerEvent = _entry.aoi.doTriggerEvent; + + // Implementation of the getDOMNode function, so that the event + // handlers can be properly bound + dummyAOI.getDOMNode = function () { return _tr; }; + + return dummyAOI; + } + + _attachActionObject( _entry, _tr, _uid, _links, _idx) + { + + // Get the dummyAOI which connects the action object to the tr but + // does no selection handling + var dummyAOI = this._getDummyAOI(_entry, _tr, _uid, _idx); + + // Create an action object for the tr and connect it to a dummy AOI + if(this._actionObjectManager) + { + if(this._actionObjectManager.getObjectById(_uid)) + { + var state = _entry.state; + this._actionObjectManager.getObjectById(_uid).remove(); + _entry.state = state; + } + _entry.ao = this._actionObjectManager.addObject(_uid, dummyAOI); + } + + // Force context (actual widget) in here, it's the last place it's available + _entry.ao._context = this._context; + _entry.ao.updateActionLinks(_links); + _entry.ao._index = _idx; + + // Overwrite some functions like "traversePath", "getNext" and + // "getPrevious" + var self = this; + + function getIndexAO (_idx) { + // Check whether the index is in the index map + if (typeof self._indexMap[_idx] !== "undefined" + && self._indexMap[_idx].uid) + { + return self._getRegisteredRowsEntry(self._indexMap[_idx].uid).ao; + } + + return null; + } + + function getElementRelatively (_step) { + var total = self._total || Object.keys(self._indexMap).length; + var max_index = Math.max.apply(Math,Object.keys(self._indexMap)); + // Get a reasonable number of iterations - not all + var count = Math.max(1,Math.min(self._total,50)); + var element = null; + var idx = _entry.idx; + while(element == null && count > 0 && max_index > 0) + { + count--; + element = getIndexAO(Math.max(0, + Math.min(max_index, idx += _step))); + } + return element; + } + + _entry.ao.getPrevious = function (_step) { + return getElementRelatively(-_step); + }; + + _entry.ao.getNext = function (_step) { + return getElementRelatively(_step); + }; + + _entry.ao.traversePath = function (_obj) { + // Get the start and the stop index + var s = Math.min(this._index, _obj._index); + var e = Math.max(this._index, _obj._index); + + var result = []; + + for (var i = s; i < e; i++) + { + var ao = getIndexAO(i); + if (ao) + { + result.push(ao); + } + } + + return result; + }; + } + + _updateState( _uid, _state) + { + var entry = this._getRegisteredRowsEntry(_uid); + + this._updateEntryState(entry, _state); + + return entry; + } + + _updateEntryState( _entry, _state) + { + + if (this._selectAll) + { + _state |= EGW_AO_STATE_SELECTED; + } + else if (this._invertSelection) + { + _state ^= EGW_AO_STATE_SELECTED; + } + + // Attach ao if not there, happens for rows loaded for selection, but + // not displayed yet + if(!_entry.ao && _entry.uid && this._actionObjectManager) + { + var _links = []; + for (var key in this._registeredRows) + { + if(this._registeredRows[key].ao && this._registeredRows[key].ao.actionLinks) + { + _links = this._registeredRows[key].ao.actionLinks; + break; + } + } + if(_links) + { + this._attachActionObjectInterface(_entry, null, _entry.uid); + this._attachActionObject(_entry, null, _entry.uid, _links, _entry.idx); + } + } + + // Update the state if it has changed + if ((_entry.aoi && _entry.aoi.getState() !== _state) || _entry.state != _state) + { + this._inUpdate = true; // Recursion prevention + + // Update the state of the action object + if (_entry.ao) + { + _entry.ao.setSelected(egwBitIsSet(_state, EGW_AO_STATE_SELECTED)); + _entry.ao.setFocused(egwBitIsSet(_state, EGW_AO_STATE_FOCUSED)); + } + + this._inUpdate = false; + + // Delete the element if state was set to "NORMAL" and there is + // no tr + if (_state === EGW_AO_STATE_NORMAL && !_entry.tr) + { + delete this._registeredRows[_entry.uid]; + } + } + + // Update the visual state + if (_entry.aoi && _entry.aoi.doSetState) + { + _entry.aoi.doSetState(_state); + } + + // Update the state of the entry + _entry.state = _state; + } + + _getRegisteredRowsEntry( _uid) + { + if (typeof this._registeredRows[_uid] === "undefined") + { + this._registeredRows[_uid] = { + "uid": _uid, + "idx": null, + "state": EGW_AO_STATE_NORMAL, + "tr": null, + "aoi": null, + "ao": null + }; + } + + return this._registeredRows[_uid]; + } + + _handleSelect( _uid, _entry, _shift, _ctrl) + { + // If not "_ctrl" is set, reset the selection + if (!_ctrl) + { + var top = this; + while(top._parent !== null) + { + top = top._parent; + } + top.resetSelection(); + this._actionObjectManager.setAllSelected(false); // needed for hirachical stuff + } + + // Mark the element that was clicked as selected + var entry = this._getRegisteredRowsEntry(_uid); + this.setSelected(_uid, + !_ctrl || !egwBitIsSet(entry.state, EGW_AO_STATE_SELECTED)); + + // Focus the element if shift is not pressed + if (!_shift) + { + this.setFocused(_uid, true); + } + else if (this._focusedEntry) + { + this._selectRange(this._focusedEntry.idx, _entry.idx); + } + + if(this.select_callback && typeof this.select_callback == "function") + { + this.select_callback.apply(this._context, arguments); + } + } + + _selectRange( _start, _stop) + { + // Contains ranges that are not currently in the index map and that have + // to be queried + var queryRanges = []; + + // Iterate over the given range and select the elements in the range + // from _start to _stop + var naStart = false; + var s = Math.min(_start, _stop); + var e = Math.max(_stop, _start); + var RANGE_MAX = 50; + var range_break = s + RANGE_MAX; + for (var i = s; i <= e; i++) + { + if (typeof this._indexMap[i] !== "undefined" && + this._indexMap[i].uid && egw.dataGetUIDdata(this._indexMap[i].uid)) + { + // Add the range to the "queryRanges" + if (naStart !== false) + { + queryRanges.push(et2_bounds(naStart, i - 1)); + naStart = false; + range_break += RANGE_MAX; + } + + // Select the element, unless flagged for exclusion + // Check for no_actions flag via data + var data = egw.dataGetUIDdata(this._indexMap[i].uid); + if(data && data.data && !data.data.no_actions) + { + this.setSelected(this._indexMap[i].uid, true); + } + } + else if (naStart === false) + { + naStart = i; + range_break = naStart + RANGE_MAX; + } + else if(i >= range_break) + { + queryRanges.push(et2_bounds(naStart ? naStart : s, i - 1)); + naStart = i; + range_break += RANGE_MAX; + } + } + + // Add the last range to the "queryRanges" + if (naStart !== false) + { + queryRanges.push(et2_bounds(naStart, i - 1)); + naStart = false; + } + + // Query all unknown ranges from the server + if(queryRanges.length > 0) + { + this._query_ranges(queryRanges); + } + } + + _query_ranges(queryRanges) + { + var that = this; + var record_count = 0; + var range_index = 0; + var range_count = queryRanges.length; + var cont = true; + var fetchPromise = new Promise(function (resolve) { + resolve(); + }); + // Found after dialog loads + var progressbar; + + var parent = et2_dialog._create_parent(); + var dialog = et2_createWidget("dialog", { + callback: + // Abort the long task if they canceled the data load + function() {cont = false}, + template: egw.webserverUrl+'/api/templates/default/long_task.xet', + message: egw.lang('Loading'), + title: egw.lang('please wait...'), + buttons: [{"button_id": et2_dialog.CANCEL_BUTTON,"text": egw.lang('cancel'),id: 'dialog[cancel]',image: 'cancel'}], + width: 300 + }, parent); + jQuery(dialog.template.DOMContainer).on('load', function() { + // Get access to template widgets + progressbar = dialog.template.widgetContainer.getWidgetById('progressbar'); + }); + + for (var i = 0; i < queryRanges.length; i++) + { + if(record_count + (queryRanges[i].bottom - queryRanges[i].top+1) > that.MAX_SELECTION) + { + egw.message(egw.lang('Too many rows selected.
Select all, or less than %1 rows', that.MAX_SELECTION)); + break; + } + else + { + record_count += (queryRanges[i].bottom - queryRanges[i].top+1); + fetchPromise = fetchPromise.then((function () + { + // Check for abort + if(!cont) return; + + return new Promise(function(resolve) { + that._queryRangeCallback.call(that._context, this, + function (_order) { + for (var j = 0; j < _order.length; j++) + { + // Check for no_actions flag via data since entry isn't there/available + var data = egw.dataGetUIDdata(_order[j]); + if(!data || data && data.data && !data.data.no_actions) + { + var entry = this._getRegisteredRowsEntry(_order[j]); + this._updateEntryState(entry, + egwSetBit(entry.state, EGW_AO_STATE_SELECTED, true)); + } + } + progressbar.set_value(100*(++range_index/range_count)); + resolve(); + }, that); + }.bind(this)); + }).bind(queryRanges[i])); + } + } + fetchPromise.finally(function() { + dialog.destroy(); + }); + } + +} + diff --git a/api/js/etemplate/et2_dataview_interfaces.js b/api/js/etemplate/et2_dataview_interfaces.js index 8a057fafe1..2ab93036d3 100644 --- a/api/js/etemplate/et2_dataview_interfaces.js +++ b/api/js/etemplate/et2_dataview_interfaces.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - Contains interfaces used inside the dataview * @@ -9,89 +10,17 @@ * @copyright Stylite 2011 * @version $Id$ */ - -/*egw:uses - et2_core_inheritance; -*/ - -var et2_dataview_IInvalidatable = new Interface({ - - invalidate: function() {} - -}); - -var et2_dataview_IViewRange = new Interface({ - - setViewRange: function(_range) {} - -}); - -/** - * Interface a data provider has to implement. The data provider functions are - * called by the et2_dataview_controller class. The data provider basically acts - * like the egw api egw_data extension, but some etemplate specific stuff has - * been stripped away -- the implementation (for the nextmatch widget that is - * et2_extension_nextmatch_dataprovider) has to take care of that. - */ -var et2_IDataProvider = new Interface({ - - /** - * This function is used by the et2_dataview_controller to fetch data for - * a certain range. The et2_dataview_controller provides data which allows - * to only update elements which really have changed. - * - * @param queriedRange is an object of the following form: - * { - * start: , - * num_rows: - * } - * @param knownRange is an array of the above form and informs the - * implementation which range is already known to the client. This parameter - * may be null in order to indicate that the client currently has no valid - * data. - * @param lastModification is the last timestamp that was returned from the - * data provider and for which the client has data. It may be null in order - * to indicate, that the client currently has no data or needs a complete - * refresh. - * @param callback is the function that should get called, once the data - * is available. The data passed to the callback function has the - * following form: - * { - * order: [uid, ...], - * total: , - * lastModification: - * } - * @param context is the context in which the callback function will get - * called. - */ - dataFetch: function (_queriedRange, _lastModification, _callback, _context) {}, - - /** - * Registers the intrest in a certain uid for a callback function. If - * the data for that uid changes or gets loaded, the given callback - * function is called. If the data for the given uid is available at the - * time of registering the callback, the callback is called immediately. - * - * @param _uid is the uid for which the callback should be registered. - * @param _callback is the callback which should get called. - * @param _context is an optional parameter which can - */ - dataRegisterUID: function (_uid, _callback, _context) {}, - - /** - * Unregisters the intrest of updates for a certain data uid. - * - * @param _uid is the data uid for which the callbacks should be - * unregistered. - * @param _callback specifies the specific callback that should be - * unregistered. If it evaluates to false, all callbacks (or those - * matching the optionally given context) are removed. - * @param _context specifies the callback context that should be - * unregistered. If it evaluates to false, all callbacks (or those - * matching the optionally given callback function) are removed. - */ - dataUnregisterUID: function (_uid, _callback, _context) {} - -}); - - +Object.defineProperty(exports, "__esModule", { value: true }); +var et2_dataviewIInvalidatable = "et2_dataview_IInvalidatable"; +function implements_et2_dataview_IInvalidatable(obj) { + return implements_methods(obj, ["invalidate"]); +} +var et2_dataview_IViewRange = "et2_dataview_IViewRange"; +function implements_et2_dataview_IViewRange(obj) { + return implements_methods(obj, ["setViewRange"]); +} +var et2_IDataProvider = "et2_IDataProvider"; +function implements_et2_IDataProvider(obj) { + return implements_methods(obj, ["dataFetch", "dataRegisterUID", "dataUnregisterUID"]); +} +//# sourceMappingURL=et2_dataview_interfaces.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_dataview_interfaces.ts b/api/js/etemplate/et2_dataview_interfaces.ts new file mode 100644 index 0000000000..216e00a38a --- /dev/null +++ b/api/js/etemplate/et2_dataview_interfaces.ts @@ -0,0 +1,110 @@ +/** + * EGroupware eTemplate2 - Contains interfaces used inside the dataview + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage dataview + * @link http://www.egroupware.org + * @author Andreas Stöckel + * @copyright Stylite 2011 + * @version $Id$ + */ + +/*egw:uses + et2_core_inheritance; +*/ + +export interface et2_dataview_IInvalidatable +{ + invalidate() +} +var et2_dataviewIInvalidatable = "et2_dataview_IInvalidatable"; +function implements_et2_dataview_IInvalidatable(obj : et2_widget) +{ + return implements_methods(obj, ["invalidate"]); +} +export interface et2_dataview_IViewRange +{ + setViewRange(_range) +} +var et2_dataview_IViewRange = "et2_dataview_IViewRange"; +function implements_et2_dataview_IViewRange(obj : et2_widget) +{ + return implements_methods(obj, ["setViewRange"]); +} + +/** + * Interface a data provider has to implement. The data provider functions are + * called by the et2_dataview_controller class. The data provider basically acts + * like the egw api egw_data extension, but some etemplate specific stuff has + * been stripped away -- the implementation (for the nextmatch widget that is + * et2_extension_nextmatch_dataprovider) has to take care of that. + */ +export interface et2_IDataProvider +{ + + /** + * This function is used by the et2_dataview_controller to fetch data for + * a certain range. The et2_dataview_controller provides data which allows + * to only update elements which really have changed. + * + * @param _queriedRange is an object of the following form: + * { + * start: , + * num_rows: + * } + * @param _knownRange is an array of the above form and informs the + * implementation which range is already known to the client. This parameter + * may be null in order to indicate that the client currently has no valid + * data. + * @param _lastModification is the last timestamp that was returned from the + * data provider and for which the client has data. It may be null in order + * to indicate, that the client currently has no data or needs a complete + * refresh. + * @param _callback is the function that should get called, once the data + * is available. The data passed to the callback function has the + * following form: + * { + * order: [uid, ...], + * total: , + * lastModification: + * } + * @param _context is the context in which the callback function will get + * called. + */ + dataFetch (_queriedRange : {start: number, num_rows:number}, _lastModification, _callback : Function, _context : object) + + /** + * Registers the intrest in a certain uid for a callback function. If + * the data for that uid changes or gets loaded, the given callback + * function is called. If the data for the given uid is available at the + * time of registering the callback, the callback is called immediately. + * + * @param _uid is the uid for which the callback should be registered. + * @param _callback is the callback which should get called. + * @param _context is an optional parameter which can + */ + dataRegisterUID (_uid : string, _callback : Function, _context : object) + + /** + * Unregisters the intrest of updates for a certain data uid. + * + * @param _uid is the data uid for which the callbacks should be + * unregistered. + * @param _callback specifies the specific callback that should be + * unregistered. If it evaluates to false, all callbacks (or those + * matching the optionally given context) are removed. + * @param _context specifies the callback context that should be + * unregistered. If it evaluates to false, all callbacks (or those + * matching the optionally given callback function) are removed. + */ + dataUnregisterUID (_uid : string, _callback : Function, _context : object) + +} +var et2_IDataProvider = "et2_IDataProvider"; +function implements_et2_IDataProvider(obj : et2_widget) +{ + return implements_methods(obj, ["dataFetch", "dataRegisterUID", "dataUnregisterUID"]); +} + + diff --git a/api/js/etemplate/et2_dataview_model_columns.js b/api/js/etemplate/et2_dataview_model_columns.js index 0acdb4ca42..83a7c10757 100755 --- a/api/js/etemplate/et2_dataview_model_columns.js +++ b/api/js/etemplate/et2_dataview_model_columns.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - Class which contains a the columns model * @@ -9,451 +10,421 @@ * @copyright Stylite 2011 * @version $Id$ */ - +Object.defineProperty(exports, "__esModule", { value: true }); /*egw:uses - et2_inheritance; + et2_core_inheritance; + et2_inheritance; */ - -var ET2_COL_TYPE_DEFAULT = 0; -var ET2_COL_TYPE_NAME_ICON_FIXED = 1; - -var ET2_COL_VISIBILITY_ALWAYS = 0; -var ET2_COL_VISIBILITY_VISIBLE = 1; -var ET2_COL_VISIBILITY_INVISIBLE = 2; -var ET2_COL_VISIBILITY_ALWAYS_NOSELECT = 3; -var ET2_COL_VISIBILITY_DISABLED = 4; - /** * Class which stores the data of a single column. * * @augments Class */ -var et2_dataview_column = (function(){ "use strict"; return ClassWithAttributes.extend({ - - attributes: { - "id": { - "name": "ID", - "type": "string", - "description": "Unique identifier for this column. It is used to " + - "store changed column widths or visibilities." - }, - "visibility": { - "name": "Visibility", - "type": "integer", - "default": ET2_COL_VISIBILITY_VISIBLE, - "description": "Defines the visibility state of this column." - }, - "caption": { - "name": "Caption", - "type": "string", - "description": "Caption of the column as it is displayed in the " + - "select columns popup." - }, - "type": { - "name": "Column type", - "type": "integer", - "default": ET2_COL_TYPE_DEFAULT, - "description": "Type of the column" - }, - "width": { - "name": "Width", - "type": "dimension", - "default": "80px", - "description": "Width of the column." - }, - "minWidth": { - "name": "Minimum width", - "type": "integer", - "default": 20, - "description": "Minimum width of the column, in pixels. Values below this are rejected." - }, - "maxWidth": { - "name": "Maximum width", - "type": "integer", - "default": 0, - "description": "Maximum width of the column" - } - }, - - /** - * Constructor - * - * @param _attrs - * @memberOf et2_dataview_column - */ - init: function(_attrs) { - this.fixedWidth = false; - this.relativeWidth = false; - - // Do the sanity check on the attributes and load them - this.generateAttributeSet(_attrs); - this.initAttributes(_attrs); - }, - - /** - * Set the column width - * - * Posible value types are: - * 1. "100" => fixedWidth 100px - * 2. "100px" => fixedWidth 100px - * 3. "50%" => relativeWidth 50% - * 4. 0.5 => relativeWidth 50% - * - * @param {float|string} _value - */ - set_width: function(_value) { - // Parse the width parameter. - this.relativeWidth = false; - this.fixedWidth = false; - var w = _value; - - if (typeof w == 'number') - { - this.relativeWidth = parseFloat(w.toFixed(3)); - } - else if (w.charAt(w.length - 1) == "%" && !isNaN(w.substr(0, w.length - 1))) - { - this.relativeWidth = parseInt(w.substr(0, w.length - 1)) / 100; - - // Relative widths with more than 100% are not allowed! - if (this.relativeWidth > 1) - { - this.relativeWidth = false; - } - } - else if (w.substr(w.length - 2, 2) == "px" && !isNaN(w.substr(0, w.length - 2))) - { - this.fixedWidth = parseInt(w.substr(0, w.length - 2)); - } - else if (typeof w == 'string' && !isNaN(w)) - { - this.fixedWidth = parseInt(w); - } - }, - - set_visibility: function(_value) { - // If visibility is always, don't turn it off - if(this.visibility == ET2_COL_VISIBILITY_ALWAYS || this.visibility == ET2_COL_VISIBILITY_ALWAYS_NOSELECT) return; - - if(_value === true) - { - this.visibility = ET2_COL_VISIBILITY_VISIBLE; - } - else if (_value === false) - { - this.visibility = ET2_COL_VISIBILITY_INVISIBLE; - } - else if (typeof _value == "number") - { - this.visibility = _value; - } - else - { - this.egw().debug("warn", "Invalid visibility option for column: ", _value); - } - } -});}).call(this); - +var et2_dataview_column = /** @class */ (function () { + /** + * Constructor + */ + function et2_dataview_column(_attrs) { + /** + * Defines the visibility state of this column. + */ + this.visibility = et2_dataview_column.ET2_COL_VISIBILITY_VISIBLE; + this.caption = ''; + /** + * Column type - Type of the column + * + * One of ET2_COL_TYPE_DEFAULT or ET2_COL_TYPE_NAME_ICON_FIXED + */ + this.type = et2_dataview_column.ET2_COL_TYPE_DEFAULT; + /** + * Width of the column + */ + this.width = 80; + /** + * Maximum width of the column + */ + this.maxWidth = 0; + /** + * Minimum width of the column, in pixels. Values below this are rejected. + */ + this.minWidth = 20; + this.id = _attrs.id; + if (typeof _attrs.visibility !== "undefined") { + this.visibility = _attrs.visibility; + } + this.caption = _attrs.caption; + if (typeof _attrs.type !== "undefined") { + this.type = _attrs.type; + } + if (typeof _attrs.width !== "undefined") { + this.set_width(_attrs.width); + } + if (typeof _attrs.maxWidth !== "undefined") { + this.maxWidth = _attrs.maxWidth; + } + if (typeof _attrs.minWidth !== "undefined") { + this.minWidth = _attrs.minWidth; + } + } + /** + * Set the column width + * + * Posible value types are: + * 1. "100" => fixedWidth 100px + * 2. "100px" => fixedWidth 100px + * 3. "50%" => relativeWidth 50% + * 4. 0.5 => relativeWidth 50% + * + * @param {float|string} _value + */ + et2_dataview_column.prototype.set_width = function (_value) { + // Parse the width parameter. + this.relativeWidth = false; + this.fixedWidth = false; + var w = _value; + if (typeof w == 'number') { + this.relativeWidth = parseFloat(w.toFixed(3)); + } + else if (w.charAt(w.length - 1) == "%" && !isNaN(w.substr(0, w.length - 1))) { + this.relativeWidth = parseInt(w.substr(0, w.length - 1)) / 100; + // Relative widths with more than 100% are not allowed! + if (this.relativeWidth > 1) { + this.relativeWidth = false; + } + } + else if (w.substr(w.length - 2, 2) == "px" && !isNaN(w.substr(0, w.length - 2))) { + this.fixedWidth = parseInt(w.substr(0, w.length - 2)); + } + else if (typeof w == 'string' && !isNaN(parseFloat(w))) { + this.fixedWidth = parseInt(w); + } + }; + et2_dataview_column.prototype.set_visibility = function (_value) { + // If visibility is always, don't turn it off + if (this.visibility == et2_dataview_column.ET2_COL_VISIBILITY_ALWAYS || this.visibility == et2_dataview_column.ET2_COL_VISIBILITY_ALWAYS_NOSELECT) + return; + if (_value === true) { + this.visibility = et2_dataview_column.ET2_COL_VISIBILITY_VISIBLE; + } + else if (_value === false) { + this.visibility = et2_dataview_column.ET2_COL_VISIBILITY_INVISIBLE; + } + else if (typeof _value == "number") { + this.visibility = _value; + } + else { + egw().debug("warn", "Invalid visibility option for column: ", _value); + } + }; + et2_dataview_column.ET2_COL_TYPE_DEFAULT = 0; + et2_dataview_column.ET2_COL_TYPE_NAME_ICON_FIXED = 1; + et2_dataview_column.ET2_COL_VISIBILITY_ALWAYS = 0; + et2_dataview_column.ET2_COL_VISIBILITY_VISIBLE = 1; + et2_dataview_column.ET2_COL_VISIBILITY_INVISIBLE = 2; + et2_dataview_column.ET2_COL_VISIBILITY_ALWAYS_NOSELECT = 3; + et2_dataview_column.ET2_COL_VISIBILITY_DISABLED = 4; + et2_dataview_column._attributes = { + "id": { + "name": "ID", + "type": "string", + "description": "Unique identifier for this column. It is used to " + + "store changed column widths or visibilities." + }, + "visibility": { + "name": "Visibility", + "type": "integer", + "default": et2_dataview_column.ET2_COL_VISIBILITY_VISIBLE, + "description": "Defines the visibility state of this column." + }, + "caption": { + "name": "Caption", + "type": "string", + "description": "Caption of the column as it is displayed in the " + + "select columns popup." + }, + "type": { + "name": "Column type", + "type": "integer", + "default": et2_dataview_column.ET2_COL_TYPE_DEFAULT, + "description": "Type of the column" + }, + "width": { + "name": "Width", + "type": "dimension", + "default": "80px", + "description": "Width of the column." + }, + "minWidth": { + "name": "Minimum width", + "type": "integer", + "default": 20, + "description": "Minimum width of the column, in pixels. Values below this are rejected." + }, + "maxWidth": { + "name": "Maximum width", + "type": "integer", + "default": 0, + "description": "Maximum width of the column" + } + }; + return et2_dataview_column; +}()); +exports.et2_dataview_column = et2_dataview_column; /** * Contains logic for the columns class. The columns class represents the unique set * of columns a grid view owns. The parameters of the columns (except for visibility) * do normaly not change. */ - -var et2_dataview_columns = (function(){ "use strict"; return Class.extend({ - - init: function(_columnData) { - // Initialize some variables - this.totalWidth = 0; - this.totalFixed = 0; - this.columnWidths = []; - - // Create the columns object - this.columns = new Array(_columnData.length); - for (var i = 0; i < _columnData.length; i++) - { - this.columns[i] = new et2_dataview_column(_columnData[i]); - } - - this.updated = true; - }, - - destroy: function() { - // Free all column objects - for (var i = 0; i < this.columns.length; i++) - { - this.columns[i].free(); - } - }, - - /** - * Set the total width of the header row - * - * @param {(string|number)} _width - */ - setTotalWidth: function(_width) { - if (_width != this.totalWidth && _width > 0) - { - this.totalWidth = _width; - this.updated = true; - } - }, - - /** - * Returns the index of the colum with the given id - * - * @param {string} _id - */ - getColumnIndexById: function(_id) { - for (var i = 0; i < this.columns.length; i++) - { - if (this.columns[i].id == _id) - { - return i; - } - } - return -1; - }, - - /** - * Returns the column with the given id - * - * @param {string} _id - */ - getColumnById: function(_id) { - var idx = this.getColumnIndexById(_id); - return (idx == -1) ? null : this.columns[idx]; - }, - - /** - * Returns the width of the column with the given index - * - * @param {number} _idx - */ - getColumnWidth: function(_idx) { - if (this.totalWidth > 0 && _idx >= 0 && _idx < this.columns.length) - { - // Recalculate the column widths if something has changed. - if (this.updated) - { - this._calculateWidths(); - this.updated = false; - } - - // Return the calculated width for the column with the given index. - return this.columnWidths[_idx]; - } - - return 0; - }, - - /** - * Returns an array containing the width of the column and its visibility - * state. - */ - getColumnData: function() { - var result = []; - - for (var i = 0; i < this.columns.length; i++) - { - result.push({ - "id": this.columns[i].id, - "width": this.getColumnWidth(i), - "visible": this.columns[i].visibility !== ET2_COL_VISIBILITY_INVISIBLE && - this.columns[i].visibility !== ET2_COL_VISIBILITY_DISABLED - - }); - } - - return result; - }, - - /** - * Returns an associative array which contains data about the visibility - * state of the columns. - */ - getColumnVisibilitySet: function() { - var result = {}; - - for (var i = 0; i < this.columns.length; i++) - { - if (this.columns[i].visibility != ET2_COL_VISIBILITY_ALWAYS_NOSELECT) - { - result[this.columns[i].id] = { - "caption": this.columns[i].caption, - "enabled": (this.columns[i].visibility != ET2_COL_VISIBILITY_ALWAYS) && - (this.columns[i].visibility != ET2_COL_VISIBILITY_DISABLED) && - (this.columns[i].type != ET2_COL_TYPE_NAME_ICON_FIXED), - "visible": this.columns[i].visibility != ET2_COL_VISIBILITY_INVISIBLE - }; - } - } - - return result; - }, - - /** - * Sets a column visiblity set - * - * @param {object} _set - */ - setColumnVisibilitySet: function(_set) { - for (var k in _set) - { - var col = this.getColumnById(k); - if (col) - { - col.set_visibility(_set[k].visible ? ET2_COL_VISIBILITY_VISIBLE : - ET2_COL_VISIBILITY_INVISIBLE); - } - } - - this.updated = true; - }, - - /* ---- PRIVATE FUNCTIONS ---- */ - - /** - * Calculates the absolute column width depending on the previously set - * "totalWidth" value. The calculated values are stored in the columnWidths - * array. - */ - _calculateWidths: function() - { - // Reset some values which are used during the calculation - for (var i = 0; i < this.columns.length; i++) - { - this.columns[i]._larger = false; - this.columns[i]._newWidth = false; - } - - // Remove the spacing between the columns from the total width - var tw = this.totalWidth; - - // Calculate how many space is - relatively - not occupied with columns with - // relative or fixed width - var totalRelative = 0; - var fixedCount = 0; - this.totalFixed = 0; - - for (var i = 0; i < this.columns.length; i++) - { - var col = this.columns[i]; - if (col.visibility !== ET2_COL_VISIBILITY_INVISIBLE && - col.visibility !== ET2_COL_VISIBILITY_DISABLED - ) - { - // Some bounds sanity checking - if(col.fixedWidth > tw || col.fixedWidth < 0) - { - col.fixedWidth = false; - } - else if (col.relativeWidth > 1 || col.relativeWidth < 0) - { - col.relativeWidth = false; - } - if (col.relativeWidth) - { - totalRelative += col.relativeWidth; - } - else if (col.fixedWidth) - { - this.totalFixed += col.fixedWidth; - fixedCount++; - } - } - } - - // Now calculate the absolute width of the columns in pixels - var usedTotal = 0; - this.columnWidths = []; - for (var i = 0; i < this.columns.length; i++) - { - var w = 0; - var col = this.columns[i]; - if (col.visibility != ET2_COL_VISIBILITY_INVISIBLE && - col.visibility !== ET2_COL_VISIBILITY_DISABLED - ) - { - if (col._larger) - { - w = col.maxWidth; - } - else if (col.fixedWidth) - { - w = col.fixedWidth; - } - else if (col.relativeWidth) - { - // Reset relative to an actual percentage (of 1.00) or - // resizing eventually sends them to 0 - col.relativeWidth = col.relativeWidth / totalRelative; - w = Math.round((tw-this.totalFixed) * col.relativeWidth); - } - if (w > tw || (col.maxWidth && w > col.maxWidth)) - { - w = Math.min(tw - usedTotal, col.maxWidth); - } - if (w < 0 || w < col.minWidth) - { - w = Math.max(0, col.minWidth); - } - } - this.columnWidths.push(w); - usedTotal += w; - } - - // Deal with any accumulated rounding errors - if(usedTotal != tw) - { - var column, columnIndex; - var remaining_width = (usedTotal - tw); - - // Pick the first relative column and use it - for(columnIndex = 0; columnIndex < this.columns.length; columnIndex++) - { - if(this.columns[columnIndex].visibility === ET2_COL_VISIBILITY_INVISIBLE || - this.columns[columnIndex].visibility === ET2_COL_VISIBILITY_DISABLED || - this.columnWidths[columnIndex] <= 0 || - remaining_width > 0 && this.columnWidths[columnIndex] <= this.columns[columnIndex].minWidth) - { - continue; - } - - var col = this.columns[columnIndex]; - if(col.relativeWidth || !col.fixedWidth) - { - column = col; - break; - } - else if (!col.fixedWidth) - { - column = col; - } - } - if(!column) - { - // Distribute shortage over all fixed width columns - var diff = Math.round(remaining_width / fixedCount); - for(var i = 0; i < this.columns.length; i++) - { - var col = this.columns[i]; - var col_diff = (diff < 0 ? - Math.max(remaining_width, diff) : - Math.min(remaining_width, diff) - ); - if(!col.fixedWidth) continue; - var new_width = this.columnWidths[i] - col_diff; - remaining_width -= col_diff; - this.columnWidths[i] = Math.max(0, Math.min(new_width,tw)); - } - } - else - { - this.columnWidths[columnIndex] = Math.max(column.minWidth, this.columnWidths[columnIndex] - remaining_width); - } - } - } - -});}).call(this); - +var et2_dataview_columns = /** @class */ (function () { + function et2_dataview_columns(_columnData) { + // Initialize some variables + this._totalWidth = 0; + this._totalFixed = 0; + this.columnWidths = []; + // Create the columns object + this.columns = new Array(_columnData.length); + for (var i = 0; i < _columnData.length; i++) { + this.columns[i] = new et2_dataview_column(_columnData[i]); + } + this._updated = true; + } + et2_dataview_columns.prototype.destroy = function () { + // Free all column objects + for (var i = 0; i < this.columns.length; i++) { + this.columns[i] = null; + } + }; + et2_dataview_columns.prototype.updated = function () { + this._updated = true; + }; + et2_dataview_columns.prototype.columnCount = function () { + return this.columns.length; + }; + Object.defineProperty(et2_dataview_columns.prototype, "totalWidth", { + get: function () { + return this._totalWidth; + }, + enumerable: true, + configurable: true + }); + Object.defineProperty(et2_dataview_columns.prototype, "totalFixed", { + get: function () { + return this._totalFixed ? this._totalFixed : 0; + }, + enumerable: true, + configurable: true + }); + /** + * Set the total width of the header row + * + * @param {(string|number)} _width + */ + et2_dataview_columns.prototype.setTotalWidth = function (_width) { + if (_width != this._totalWidth && _width > 0) { + this._totalWidth = _width; + this._updated = true; + } + }; + /** + * Returns the index of the colum with the given id + * + * @param {string} _id + */ + et2_dataview_columns.prototype.getColumnIndexById = function (_id) { + for (var i = 0; i < this.columns.length; i++) { + if (this.columns[i].id == _id) { + return i; + } + } + return -1; + }; + /** + * Returns the column with the given id + * + * @param {string} _id + */ + et2_dataview_columns.prototype.getColumnById = function (_id) { + var idx = this.getColumnIndexById(_id); + return (idx == -1) ? null : this.columns[idx]; + }; + /** + * Returns the width of the column with the given index + * + * @param {number} _idx + */ + et2_dataview_columns.prototype.getColumnWidth = function (_idx) { + if (this._totalWidth > 0 && _idx >= 0 && _idx < this.columns.length) { + // Recalculate the column widths if something has changed. + if (this._updated) { + this._calculateWidths(); + this._updated = false; + } + // Return the calculated width for the column with the given index. + return this.columnWidths[_idx]; + } + return 0; + }; + /** + * Returns an array containing the width of the column and its visibility + * state. + */ + et2_dataview_columns.prototype.getColumnData = function () { + var result = []; + for (var i = 0; i < this.columns.length; i++) { + result.push({ + "id": this.columns[i].id, + "width": this.getColumnWidth(i), + "visible": this.columns[i].visibility !== et2_dataview_column.ET2_COL_VISIBILITY_INVISIBLE && + this.columns[i].visibility !== et2_dataview_column.ET2_COL_VISIBILITY_DISABLED + }); + } + return result; + }; + /** + * Returns an associative array which contains data about the visibility + * state of the columns. + */ + et2_dataview_columns.prototype.getColumnVisibilitySet = function () { + var result = {}; + for (var i = 0; i < this.columns.length; i++) { + if (this.columns[i].visibility != et2_dataview_column.ET2_COL_VISIBILITY_ALWAYS_NOSELECT) { + result[this.columns[i].id] = { + "caption": this.columns[i].caption, + "enabled": (this.columns[i].visibility != et2_dataview_column.ET2_COL_VISIBILITY_ALWAYS) && + (this.columns[i].visibility != et2_dataview_column.ET2_COL_VISIBILITY_DISABLED) && + (this.columns[i].type != et2_dataview_column.ET2_COL_TYPE_NAME_ICON_FIXED), + "visible": this.columns[i].visibility != et2_dataview_column.ET2_COL_VISIBILITY_INVISIBLE + }; + } + } + return result; + }; + /** + * Sets a column visiblity set + * + * @param {object} _set + */ + et2_dataview_columns.prototype.setColumnVisibilitySet = function (_set) { + for (var k in _set) { + var col = this.getColumnById(k); + if (col) { + col.set_visibility(_set[k].visible ? et2_dataview_column.ET2_COL_VISIBILITY_VISIBLE : + et2_dataview_column.ET2_COL_VISIBILITY_INVISIBLE); + } + } + this._updated = true; + }; + /* ---- PRIVATE FUNCTIONS ---- */ + /** + * Calculates the absolute column width depending on the previously set + * "totalWidth" value. The calculated values are stored in the columnWidths + * array. + */ + et2_dataview_columns.prototype._calculateWidths = function () { + // Reset some values which are used during the calculation + var _larger = Array(this.columns.length); + for (var i = 0; i < this.columns.length; i++) { + _larger[i] = false; + } + // Remove the spacing between the columns from the total width + var tw = this._totalWidth; + // Calculate how many space is - relatively - not occupied with columns with + // relative or fixed width + var totalRelative = 0; + var fixedCount = 0; + this._totalFixed = 0; + for (var i = 0; i < this.columns.length; i++) { + var col = this.columns[i]; + if (col.visibility !== et2_dataview_column.ET2_COL_VISIBILITY_INVISIBLE && + col.visibility !== et2_dataview_column.ET2_COL_VISIBILITY_DISABLED) { + // Some bounds sanity checking + if (col.fixedWidth > tw || col.fixedWidth < 0) { + col.fixedWidth = false; + } + else if (col.relativeWidth > 1 || col.relativeWidth < 0) { + col.relativeWidth = false; + } + if (col.relativeWidth) { + totalRelative += col.relativeWidth; + } + else if (col.fixedWidth) { + this._totalFixed += col.fixedWidth; + fixedCount++; + } + } + } + // Now calculate the absolute width of the columns in pixels + var usedTotal = 0; + this.columnWidths = []; + for (var i = 0; i < this.columns.length; i++) { + var w = 0; + var col = this.columns[i]; + if (col.visibility != et2_dataview_column.ET2_COL_VISIBILITY_INVISIBLE && + col.visibility !== et2_dataview_column.ET2_COL_VISIBILITY_DISABLED) { + if (_larger[i]) { + w = col.maxWidth; + } + else if (col.fixedWidth) { + w = col.fixedWidth; + } + else if (col.relativeWidth) { + // Reset relative to an actual percentage (of 1.00) or + // resizing eventually sends them to 0 + col.relativeWidth = col.relativeWidth / totalRelative; + w = Math.round((tw - this._totalFixed) * col.relativeWidth); + } + if (w > tw || (col.maxWidth && w > col.maxWidth)) { + w = Math.min(tw - usedTotal, col.maxWidth); + } + if (w < 0 || w < col.minWidth) { + w = Math.max(0, col.minWidth); + } + } + this.columnWidths.push(w); + usedTotal += w; + } + // Deal with any accumulated rounding errors + if (usedTotal != tw) { + var column, columnIndex; + var remaining_width = (usedTotal - tw); + // Pick the first relative column and use it + for (columnIndex = 0; columnIndex < this.columns.length; columnIndex++) { + if (this.columns[columnIndex].visibility === et2_dataview_column.ET2_COL_VISIBILITY_INVISIBLE || + this.columns[columnIndex].visibility === et2_dataview_column.ET2_COL_VISIBILITY_DISABLED || + this.columnWidths[columnIndex] <= 0 || + remaining_width > 0 && this.columnWidths[columnIndex] <= this.columns[columnIndex].minWidth) { + continue; + } + var col = this.columns[columnIndex]; + if (col.relativeWidth || !col.fixedWidth) { + column = col; + break; + } + else if (!col.fixedWidth) { + column = col; + } + } + if (!column) { + // Distribute shortage over all fixed width columns + var diff = Math.round(remaining_width / fixedCount); + for (var i = 0; i < this.columns.length; i++) { + var col = this.columns[i]; + var col_diff = (diff < 0 ? + Math.max(remaining_width, diff) : + Math.min(remaining_width, diff)); + if (!col.fixedWidth) + continue; + var new_width = this.columnWidths[i] - col_diff; + remaining_width -= col_diff; + this.columnWidths[i] = Math.max(0, Math.min(new_width, tw)); + } + } + else { + this.columnWidths[columnIndex] = Math.max(column.minWidth, this.columnWidths[columnIndex] - remaining_width); + } + } + }; + return et2_dataview_columns; +}()); +exports.et2_dataview_columns = et2_dataview_columns; +//# sourceMappingURL=et2_dataview_model_columns.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_dataview_model_columns.ts b/api/js/etemplate/et2_dataview_model_columns.ts new file mode 100755 index 0000000000..6484f47fb8 --- /dev/null +++ b/api/js/etemplate/et2_dataview_model_columns.ts @@ -0,0 +1,548 @@ +/** + * EGroupware eTemplate2 - Class which contains a the columns model + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage dataview + * @link http://www.egroupware.org + * @author Andreas Stöckel + * @copyright Stylite 2011 + * @version $Id$ + */ + +/*egw:uses + et2_core_inheritance; + et2_inheritance; +*/ + + +/** + * Class which stores the data of a single column. + * + * @augments Class + */ +export class et2_dataview_column +{ + + public static readonly ET2_COL_TYPE_DEFAULT = 0; + public static readonly ET2_COL_TYPE_NAME_ICON_FIXED = 1; + + public static readonly ET2_COL_VISIBILITY_ALWAYS = 0; + public static readonly ET2_COL_VISIBILITY_VISIBLE = 1; + public static readonly ET2_COL_VISIBILITY_INVISIBLE = 2; + public static readonly ET2_COL_VISIBILITY_ALWAYS_NOSELECT = 3; + public static readonly ET2_COL_VISIBILITY_DISABLED = 4; + + static readonly _attributes: any = { + "id": { + "name": "ID", + "type": "string", + "description": "Unique identifier for this column. It is used to " + + "store changed column widths or visibilities." + }, + "visibility": { + "name": "Visibility", + "type": "integer", + "default": et2_dataview_column.ET2_COL_VISIBILITY_VISIBLE, + "description": "Defines the visibility state of this column." + }, + "caption": { + "name": "Caption", + "type": "string", + "description": "Caption of the column as it is displayed in the " + + "select columns popup." + }, + "type": { + "name": "Column type", + "type": "integer", + "default": et2_dataview_column.ET2_COL_TYPE_DEFAULT, + "description": "Type of the column" + }, + "width": { + "name": "Width", + "type": "dimension", + "default": "80px", + "description": "Width of the column." + }, + "minWidth": { + "name": "Minimum width", + "type": "integer", + "default": 20, + "description": "Minimum width of the column, in pixels. Values below this are rejected." + }, + "maxWidth": { + "name": "Maximum width", + "type": "integer", + "default": 0, + "description": "Maximum width of the column" + } + }; + + fixedWidth: number | boolean; + relativeWidth: number | boolean; + + /** + * Unique identifier for this column. It is used to store changed column widths or visibilities. + */ + public id: string; + + /** + * Defines the visibility state of this column. + */ + public visibility: number = et2_dataview_column.ET2_COL_VISIBILITY_VISIBLE; + + public caption: string = ''; + + /** + * Column type - Type of the column + * + * One of ET2_COL_TYPE_DEFAULT or ET2_COL_TYPE_NAME_ICON_FIXED + */ + public type: number = et2_dataview_column.ET2_COL_TYPE_DEFAULT; + + /** + * Width of the column + */ + public width: number = 80; + + /** + * Maximum width of the column + */ + public maxWidth: number = 0; + + /** + * Minimum width of the column, in pixels. Values below this are rejected. + */ + public minWidth: number = 20; + + /** + * Constructor + */ + constructor(_attrs) + { + this.id = _attrs.id; + + if(typeof _attrs.visibility !== "undefined") + { + this.visibility = _attrs.visibility; + } + this.caption = _attrs.caption; + if(typeof _attrs.type !== "undefined") + { + this.type = _attrs.type; + } + if(typeof _attrs.width !== "undefined") + { + this.set_width( _attrs.width ); + } + if(typeof _attrs.maxWidth !== "undefined") + { + this.maxWidth = _attrs.maxWidth; + } + if(typeof _attrs.minWidth !== "undefined") + { + this.minWidth = _attrs.minWidth; + } + } + + /** + * Set the column width + * + * Posible value types are: + * 1. "100" => fixedWidth 100px + * 2. "100px" => fixedWidth 100px + * 3. "50%" => relativeWidth 50% + * 4. 0.5 => relativeWidth 50% + * + * @param {float|string} _value + */ + set_width(_value) + { + // Parse the width parameter. + this.relativeWidth = false; + this.fixedWidth = false; + var w = _value; + + if (typeof w == 'number') + { + this.relativeWidth = parseFloat(w.toFixed(3)); + } + else if (w.charAt(w.length - 1) == "%" && !isNaN(w.substr(0, w.length - 1))) + { + this.relativeWidth = parseInt(w.substr(0, w.length - 1)) / 100; + + // Relative widths with more than 100% are not allowed! + if (this.relativeWidth > 1) + { + this.relativeWidth = false; + } + } + else if (w.substr(w.length - 2, 2) == "px" && !isNaN(w.substr(0, w.length - 2))) + { + this.fixedWidth = parseInt(w.substr(0, w.length - 2)); + } + else if (typeof w == 'string' && !isNaN(parseFloat(w))) + { + this.fixedWidth = parseInt(w); + } + } + + set_visibility(_value) + { + // If visibility is always, don't turn it off + if(this.visibility == et2_dataview_column.ET2_COL_VISIBILITY_ALWAYS || this.visibility == et2_dataview_column.ET2_COL_VISIBILITY_ALWAYS_NOSELECT) return; + + if(_value === true) + { + this.visibility = et2_dataview_column.ET2_COL_VISIBILITY_VISIBLE; + } + else if (_value === false) + { + this.visibility = et2_dataview_column.ET2_COL_VISIBILITY_INVISIBLE; + } + else if (typeof _value == "number") + { + this.visibility = _value; + } + else + { + egw().debug("warn", "Invalid visibility option for column: ", _value); + } + } +} + +/** + * Contains logic for the columns class. The columns class represents the unique set + * of columns a grid view owns. The parameters of the columns (except for visibility) + * do normaly not change. + */ + +export class et2_dataview_columns +{ + private _totalWidth: number; + private _totalFixed: number | boolean; + private columnWidths: any[]; + private columns: et2_dataview_column[]; + private _updated: boolean; + + constructor(_columnData) + { + // Initialize some variables + this._totalWidth = 0; + this._totalFixed = 0; + this.columnWidths = []; + + // Create the columns object + this.columns = new Array(_columnData.length); + for (var i = 0; i < _columnData.length; i++) + { + this.columns[i] = new et2_dataview_column(_columnData[i]); + } + + this._updated = true; + } + + destroy() { + // Free all column objects + for (var i = 0; i < this.columns.length; i++) + { + this.columns[i] = null; + } + } + + public updated() + { + this._updated = true; + } + + columnCount() : number + { + return this.columns.length; + } + + get totalWidth(): number + { + return this._totalWidth; + } + + get totalFixed(): number { + return this._totalFixed ? this._totalFixed : 0; + } + + /** + * Set the total width of the header row + * + * @param {(string|number)} _width + */ + setTotalWidth(_width) + { + if (_width != this._totalWidth && _width > 0) + { + this._totalWidth = _width; + this._updated = true; + } + } + + /** + * Returns the index of the colum with the given id + * + * @param {string} _id + */ + getColumnIndexById( _id) + { + for (var i = 0; i < this.columns.length; i++) + { + if (this.columns[i].id == _id) + { + return i; + } + } + return -1; + } + + /** + * Returns the column with the given id + * + * @param {string} _id + */ + getColumnById( _id) + { + var idx = this.getColumnIndexById(_id); + return (idx == -1) ? null : this.columns[idx]; + } + + /** + * Returns the width of the column with the given index + * + * @param {number} _idx + */ + getColumnWidth( _idx) + { + if (this._totalWidth > 0 && _idx >= 0 && _idx < this.columns.length) + { + // Recalculate the column widths if something has changed. + if (this._updated) + { + this._calculateWidths(); + this._updated = false; + } + + // Return the calculated width for the column with the given index. + return this.columnWidths[_idx]; + } + + return 0; + } + + /** + * Returns an array containing the width of the column and its visibility + * state. + */ + getColumnData( ) + { + var result = []; + + for (var i = 0; i < this.columns.length; i++) + { + result.push({ + "id": this.columns[i].id, + "width": this.getColumnWidth(i), + "visible": this.columns[i].visibility !== et2_dataview_column.ET2_COL_VISIBILITY_INVISIBLE && + this.columns[i].visibility !== et2_dataview_column.ET2_COL_VISIBILITY_DISABLED + + }); + } + + return result; + } + + /** + * Returns an associative array which contains data about the visibility + * state of the columns. + */ + getColumnVisibilitySet() + { + var result = {}; + + for (var i = 0; i < this.columns.length; i++) + { + if (this.columns[i].visibility != et2_dataview_column.ET2_COL_VISIBILITY_ALWAYS_NOSELECT) + { + result[this.columns[i].id] = { + "caption": this.columns[i].caption, + "enabled": (this.columns[i].visibility != et2_dataview_column.ET2_COL_VISIBILITY_ALWAYS) && + (this.columns[i].visibility != et2_dataview_column.ET2_COL_VISIBILITY_DISABLED) && + (this.columns[i].type != et2_dataview_column.ET2_COL_TYPE_NAME_ICON_FIXED), + "visible": this.columns[i].visibility != et2_dataview_column.ET2_COL_VISIBILITY_INVISIBLE + }; + } + } + + return result; + } + + /** + * Sets a column visiblity set + * + * @param {object} _set + */ + setColumnVisibilitySet( _set) + { + for (var k in _set) + { + var col = this.getColumnById(k); + if (col) + { + col.set_visibility(_set[k].visible ? et2_dataview_column.ET2_COL_VISIBILITY_VISIBLE : + et2_dataview_column.ET2_COL_VISIBILITY_INVISIBLE); + } + } + + this._updated = true; + } + + /* ---- PRIVATE FUNCTIONS ---- */ + + /** + * Calculates the absolute column width depending on the previously set + * "totalWidth" value. The calculated values are stored in the columnWidths + * array. + */ + _calculateWidths() + { + // Reset some values which are used during the calculation + let _larger = Array(this.columns.length); + for (var i = 0; i < this.columns.length; i++) + { + _larger[i] = false; + } + + // Remove the spacing between the columns from the total width + var tw = this._totalWidth; + + // Calculate how many space is - relatively - not occupied with columns with + // relative or fixed width + var totalRelative: number = 0; + var fixedCount = 0; + this._totalFixed = 0; + + for (var i = 0; i < this.columns.length; i++) + { + var col = this.columns[i]; + if (col.visibility !== et2_dataview_column.ET2_COL_VISIBILITY_INVISIBLE && + col.visibility !== et2_dataview_column.ET2_COL_VISIBILITY_DISABLED + ) + { + // Some bounds sanity checking + if(col.fixedWidth > tw || col.fixedWidth < 0) + { + col.fixedWidth = false; + } + else if (col.relativeWidth > 1 || col.relativeWidth < 0) + { + col.relativeWidth = false; + } + if (col.relativeWidth) + { + totalRelative += col.relativeWidth; + } + else if (col.fixedWidth) + { + this._totalFixed += col.fixedWidth; + fixedCount++; + } + } + } + + // Now calculate the absolute width of the columns in pixels + var usedTotal = 0; + this.columnWidths = []; + for (var i = 0; i < this.columns.length; i++) + { + var w = 0; + var col = this.columns[i]; + if (col.visibility != et2_dataview_column.ET2_COL_VISIBILITY_INVISIBLE && + col.visibility !== et2_dataview_column.ET2_COL_VISIBILITY_DISABLED + ) + { + if (_larger[i]) + { + w = col.maxWidth; + } + else if (col.fixedWidth) + { + w = col.fixedWidth; + } + else if (col.relativeWidth) + { + // Reset relative to an actual percentage (of 1.00) or + // resizing eventually sends them to 0 + col.relativeWidth = col.relativeWidth / totalRelative; + w = Math.round((tw-this._totalFixed) * col.relativeWidth); + } + if (w > tw || (col.maxWidth && w > col.maxWidth)) + { + w = Math.min(tw - usedTotal, col.maxWidth); + } + if (w < 0 || w < col.minWidth) + { + w = Math.max(0, col.minWidth); + } + } + this.columnWidths.push(w); + usedTotal += w; + } + + // Deal with any accumulated rounding errors + if(usedTotal != tw) + { + var column, columnIndex; + var remaining_width = (usedTotal - tw); + + // Pick the first relative column and use it + for(columnIndex = 0; columnIndex < this.columns.length; columnIndex++) + { + if(this.columns[columnIndex].visibility === et2_dataview_column.ET2_COL_VISIBILITY_INVISIBLE || + this.columns[columnIndex].visibility === et2_dataview_column.ET2_COL_VISIBILITY_DISABLED || + this.columnWidths[columnIndex] <= 0 || + remaining_width > 0 && this.columnWidths[columnIndex] <= this.columns[columnIndex].minWidth) + { + continue; + } + + var col = this.columns[columnIndex]; + if(col.relativeWidth || !col.fixedWidth) + { + column = col; + break; + } + else if (!col.fixedWidth) + { + column = col; + } + } + if(!column) + { + // Distribute shortage over all fixed width columns + var diff = Math.round(remaining_width / fixedCount); + for(var i = 0; i < this.columns.length; i++) + { + var col = this.columns[i]; + var col_diff = (diff < 0 ? + Math.max(remaining_width, diff) : + Math.min(remaining_width, diff) + ); + if(!col.fixedWidth) continue; + var new_width = this.columnWidths[i] - col_diff; + remaining_width -= col_diff; + this.columnWidths[i] = Math.max(0, Math.min(new_width,tw)); + } + } + else + { + this.columnWidths[columnIndex] = Math.max(column.minWidth, this.columnWidths[columnIndex] - remaining_width); + } + } + } + +} diff --git a/api/js/etemplate/et2_dataview_view_container.js b/api/js/etemplate/et2_dataview_view_container.js index f7007dcb29..08646d22d2 100644 --- a/api/js/etemplate/et2_dataview_view_container.js +++ b/api/js/etemplate/et2_dataview_view_container.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - dataview code * @@ -9,12 +10,7 @@ * @copyright Stylite 2012 * @version $Id$ */ - -/*egw:uses - /vendor/bower-asset/jquery/dist/jquery.js; - et2_dataview_interfaces; -*/ - +Object.defineProperty(exports, "__esModule", { value: true }); /** * The et2_dataview_container class is the main object each dataview consits of. * Each row, spacer as well as the grid itself are containers. A container is @@ -31,352 +27,287 @@ * * @augments Class */ -var et2_dataview_container = (function(){ "use strict"; return Class.extend(et2_dataview_IInvalidatable, -{ - /** - * Initializes the container object. - * - * @param _parent is an object which implements the IInvalidatable - * interface. _parent may not be null. - * @memberOf et2_dataview_container - */ - init: function(_parent) { - // Copy the given invalidation element - this._parent = _parent; - - this._nodes = []; // contains all DOM-Nodes this container exists of - this._inTree = false; // - this._attachData = {"node": null, "prepend": false}; - this._destroyCallback = null; - this._destroyContext = null; - - this._height = false; - this._index = 0; - this._top = 0; - }, - - /** - * Destroys this container. Classes deriving from et2_dataview_container - * should override this method and take care of unregistering all event - * handlers etc. - */ - destroy: function() { - // Remove the nodes from the tree - this.removeFromTree(); - - // Call the callback function (if one is registered) - if (this._destroyCallback) - { - this._destroyCallback.call(this._destroyContext, this); - } - }, - - /** - * Sets the "destroyCallback" -- the given function gets called whenever - * the container is destroyed. This instance is passed as an parameter to - * the callback. - * - * @param {function} _callback - * @param {object} _context - */ - setDestroyCallback: function(_callback, _context) { - this._destroyCallback = _callback; - this._destroyContext = _context; - }, - - /** - * Inserts all container nodes into the DOM tree after or before the given - * element. - * - * @param _node is the node after/before which the container "tr"s should - * get inserted. _node should be a simple DOM node, not a jQuery object. - * @param _prepend specifies whether the container should be inserted before - * or after the given node. Inserting before is needed for inserting the - * first element in front of an spacer. - */ - insertIntoTree: function(_node, _prepend) { - - if (!this._inTree && _node != null && this._nodes.length > 0) - { - // Store the parent node and indicate that this element is now in - // the tree. - this._attachData = {"node": _node, "prepend": _prepend}; - this._inTree = true; - - for (var i = 0; i < this._nodes.length; i++) - { - if (i == 0) - { - if (_prepend) - { - _node.before(this._nodes[0]); - } - else - { - _node.after(this._nodes[0]); - } - } - else - { - // Insert all following nodes after the previous node - this._nodes[i - 1].after(this._nodes[i]); - } - } - - // Invalidate this element in order to update the height of the - // parent - this.invalidate(); - } - }, - - /** - * Removes all container nodes from the tree. - */ - removeFromTree: function() { - if (this._inTree) - { - // Call the jQuery remove function to remove all nodes from the tree - // again. - for (var i = 0; i < this._nodes.length; i++) - { - this._nodes[i].remove(); - } - - // Reset the "attachData" - this._inTree = false; - this._attachData = {"node": null, "prepend": false}; - } - }, - - /** - * Appends a node to the container. - * - * @param _node is the DOM-Node which should be appended. - */ - appendNode: function(_node) { - // Add the given node to the "nodes" array - this._nodes.push(_node); - - // If the container is already in the tree, attach the given node to the - // tree. - if (this._inTree) - { - if (this._nodes.length === 1) - { - if (this._attachData.prepend) - { - this._attachData.node.before(_node); - } - else - { - this._attachData.node.after(_node); - } - } - else - { - this._nodes[this._nodes.length - 2].after(_node); - } - - this.invalidate(); - } - }, - - /** - * Removes a certain node from the container - * - * @param {DOMElement} _node - */ - removeNode: function(_node) { - // Get the index of the node in the nodes array - var idx = this._nodes.indexOf(_node); - - if (idx >= 0) - { - // Remove the node if the container is currently attached - if (this._inTree) - { - _node.parentNode.removeChild(_node); - } - - // Remove the node from the nodes array - this._nodes.splice(idx, 1); - } - }, - - /** - * Returns the last node of the container - new nodes have to be appended - * after it. - */ - getLastNode: function() { - - if (this._nodes.length > 0) - { - return this._nodes[this._nodes.length - 1]; - } - - return null; - }, - - /** - * Returns the first node of the container. - */ - getFirstNode: function() { - return this._nodes.length > 0 ? this._nodes[0] : null; - }, - - /** - * Returns the accumulated height of all container nodes. Only visible nodes - * (without "display: none" etc.) are taken into account. - */ - getHeight: function() { - if (this._height === false && this._inTree) - { - this._height = 0; - - // Setting this before measuring height helps with issues getting the - // wrong height due to margins & collapsed borders - this.tr.css('display','block'); - - // Increment the height value for each visible container node - for (var i = 0; i < this._nodes.length; i++) - { - if (this._isVisible(this._nodes[i][0])) - { - this._height += this._nodeHeight(this._nodes[i][0]); - } - } - this.tr.css('display',''); - } - - return this._height === false ? 0 : this._height; - }, - - /** - * Returns a datastructure containing information used for calculating the - * average row height of a grid. - * The datastructure has the - * { - * avgHeight: , - * avgCount: - * } - */ - getAvgHeightData: function() { - return { - "avgHeight": this.getHeight(), - "avgCount": 1 - }; - }, - - /** - * Returns the previously set "pixel top" of the container. - */ - getTop: function() { - return this._top; - }, - - /** - * Returns the "pixel bottom" of the container. - */ - getBottom: function() { - return this._top + this.getHeight(); - }, - - /** - * Returns the range of the element. - */ - getRange: function() { - return et2_bounds(this.getTop(), this.getBottom()); - }, - - /** - * Returns the index of the element. - */ - getIndex: function() { - return this._index; - }, - - /** - * Returns how many elements this container represents. - */ - getCount: function() { - return 1; - }, - - /** - * Sets the top of the element. - * - * @param {number} _value - */ - setTop: function(_value) { - this._top = _value; - }, - - /** - * Sets the index of the element. - * - * @param {number} _value - */ - setIndex: function(_value) { - this._index = _value; - }, - - /* -- et2_dataview_IInvalidatable -- */ - - /** - * Broadcasts an invalidation through the container tree. Marks the own - * height as invalid. - */ - invalidate: function() { - // Abort if this element is already marked as invalid. - if (this._height !== false) - { - // Delete the own, probably computed height - this._height = false; - - // Broadcast the invalidation to the parent element - this._parent.invalidate(); - } - }, - - - /* -- PRIVATE FUNCTIONS -- */ - - - /** - * Used to check whether an element is visible or not (non recursive). - * - * @param _obj is the element which should be checked for visibility, it is - * only checked whether some stylesheet makes the element invisible, not if - * the given object is actually inside the DOM. - */ - _isVisible: function(_obj) { - - // Check whether the element is localy invisible - if (_obj.style && (_obj.style.display === "none" - || _obj.style.visiblity === "none")) - { - return false; - } - - // Get the computed style of the element - var style = window.getComputedStyle ? window.getComputedStyle(_obj, null) - : _obj.currentStyle; - if (style.display === "none" || style.visibility === "none") - { - return false; - } - - return true; - }, - - /** - * Returns the height of a node in pixels and zero if the element is not - * visible. The height is clamped to positive values. - * - * @param {DOMElement} _node - */ - _nodeHeight: function(_node) - { - return _node.offsetHeight; - } -});}).call(this); +var et2_dataview_container = /** @class */ (function () { + /** + * Initializes the container object. + * + * @param _parent is an object which implements the IInvalidatable + * interface. _parent may not be null. + * @memberOf et2_dataview_container + */ + function et2_dataview_container(_parent) { + // Copy the given invalidation element + this._parent = _parent; + this._nodes = []; + this._inTree = false; + this._attachData = { "node": null, "prepend": false }; + this._destroyCallback = null; + this._destroyContext = null; + this._height = -1; + this._index = 0; + this._top = 0; + } + /** + * Destroys this container. Classes deriving from et2_dataview_container + * should override this method and take care of unregistering all event + * handlers etc. + */ + et2_dataview_container.prototype.destroy = function () { + // Remove the nodes from the tree + this.removeFromTree(); + // Call the callback function (if one is registered) + if (this._destroyCallback) { + this._destroyCallback.call(this._destroyContext, this); + } + }; + /** + * Sets the "destroyCallback" -- the given function gets called whenever + * the container is destroyed. This instance is passed as an parameter to + * the callback. + * + * @param {function} _callback + * @param {object} _context + */ + et2_dataview_container.prototype.setDestroyCallback = function (_callback, _context) { + this._destroyCallback = _callback; + this._destroyContext = _context; + }; + /** + * Inserts all container nodes into the DOM tree after or before the given + * element. + * + * @param _node is the node after/before which the container "tr"s should + * get inserted. _node should be a simple DOM node, not a jQuery object. + * @param _prepend specifies whether the container should be inserted before + * or after the given node. Inserting before is needed for inserting the + * first element in front of an spacer. + */ + et2_dataview_container.prototype.insertIntoTree = function (_node, _prepend) { + if (!this._inTree && _node != null && this._nodes.length > 0) { + // Store the parent node and indicate that this element is now in + // the tree. + this._attachData = { node: _node, prepend: _prepend }; + this._inTree = true; + for (var i = 0; i < this._nodes.length; i++) { + if (i == 0) { + if (_prepend) { + _node.before(this._nodes[0]); + } + else { + _node.after(this._nodes[0]); + } + } + else { + // Insert all following nodes after the previous node + this._nodes[i - 1].after(this._nodes[i]); + } + } + // Invalidate this element in order to update the height of the + // parent + this.invalidate(); + } + }; + /** + * Removes all container nodes from the tree. + */ + et2_dataview_container.prototype.removeFromTree = function () { + if (this._inTree) { + // Call the jQuery remove function to remove all nodes from the tree + // again. + for (var i = 0; i < this._nodes.length; i++) { + this._nodes[i].remove(); + } + // Reset the "attachData" + this._inTree = false; + this._attachData = { "node": null, "prepend": false }; + } + }; + /** + * Appends a node to the container. + * + * @param _node is the DOM-Node which should be appended. + */ + et2_dataview_container.prototype.appendNode = function (_node) { + // Add the given node to the "nodes" array + this._nodes.push(_node); + // If the container is already in the tree, attach the given node to the + // tree. + if (this._inTree) { + if (this._nodes.length === 1) { + if (this._attachData.prepend) { + this._attachData.node.before(_node); + } + else { + this._attachData.node.after(_node); + } + } + else { + this._nodes[this._nodes.length - 2].after(_node); + } + this.invalidate(); + } + }; + /** + * Removes a certain node from the container + * + * @param {HTMLElement} _node + */ + et2_dataview_container.prototype.removeNode = function (_node) { + // Get the index of the node in the nodes array + var idx = this._nodes.indexOf(_node); + if (idx >= 0) { + // Remove the node if the container is currently attached + if (this._inTree) { + _node.parentNode.removeChild(_node); + } + // Remove the node from the nodes array + this._nodes.splice(idx, 1); + } + }; + /** + * Returns the last node of the container - new nodes have to be appended + * after it. + */ + et2_dataview_container.prototype.getLastNode = function () { + if (this._nodes.length > 0) { + return this._nodes[this._nodes.length - 1]; + } + return null; + }; + /** + * Returns the first node of the container. + */ + et2_dataview_container.prototype.getFirstNode = function () { + return this._nodes.length > 0 ? this._nodes[0] : null; + }; + /** + * Returns the accumulated height of all container nodes. Only visible nodes + * (without "display: none" etc.) are taken into account. + */ + et2_dataview_container.prototype.getHeight = function () { + if (this._height === -1 && this._inTree) { + this._height = 0; + // Setting this before measuring height helps with issues getting the + // wrong height due to margins & collapsed borders + this.tr.css('display', 'block'); + // Increment the height value for each visible container node + for (var i = 0; i < this._nodes.length; i++) { + if (et2_dataview_container._isVisible(this._nodes[i][0])) { + this._height += et2_dataview_container._nodeHeight(this._nodes[i][0]); + } + } + this.tr.css('display', ''); + } + return (this._height === -1) ? 0 : this._height; + }; + /** + * Returns a datastructure containing information used for calculating the + * average row height of a grid. + * The datastructure has the + * { + * avgHeight: , + * avgCount: + * } + */ + et2_dataview_container.prototype.getAvgHeightData = function () { + return { + "avgHeight": this.getHeight(), + "avgCount": 1 + }; + }; + /** + * Returns the previously set "pixel top" of the container. + */ + et2_dataview_container.prototype.getTop = function () { + return this._top; + }; + /** + * Returns the "pixel bottom" of the container. + */ + et2_dataview_container.prototype.getBottom = function () { + return this._top + this.getHeight(); + }; + /** + * Returns the range of the element. + */ + et2_dataview_container.prototype.getRange = function () { + return et2_bounds(this.getTop(), this.getBottom()); + }; + /** + * Returns the index of the element. + */ + et2_dataview_container.prototype.getIndex = function () { + return this._index; + }; + /** + * Returns how many elements this container represents. + */ + et2_dataview_container.prototype.getCount = function () { + return 1; + }; + /** + * Sets the top of the element. + * + * @param {number} _value + */ + et2_dataview_container.prototype.setTop = function (_value) { + this._top = _value; + }; + /** + * Sets the index of the element. + * + * @param {number} _value + */ + et2_dataview_container.prototype.setIndex = function (_value) { + this._index = _value; + }; + /* -- et2_dataview_IInvalidatable -- */ + /** + * Broadcasts an invalidation through the container tree. Marks the own + * height as invalid. + */ + et2_dataview_container.prototype.invalidate = function () { + // Abort if this element is already marked as invalid. + if (this._height !== -1) { + // Delete the own, probably computed height + this._height = -1; + // Broadcast the invalidation to the parent element + this._parent.invalidate(); + } + }; + /* -- PRIVATE FUNCTIONS -- */ + /** + * Used to check whether an element is visible or not (non recursive). + * + * @param _obj is the element which should be checked for visibility, it is + * only checked whether some stylesheet makes the element invisible, not if + * the given object is actually inside the DOM. + */ + et2_dataview_container._isVisible = function (_obj) { + // Check whether the element is localy invisible + if (_obj.style && (_obj.style.display === "none" + || _obj.style.visibility === "none")) { + return false; + } + // Get the computed style of the element + var style = window.getComputedStyle ? window.getComputedStyle(_obj, null) + // @ts-ignore + : _obj.currentStyle; + if (style.display === "none" || style.visibility === "none") { + return false; + } + return true; + }; + /** + * Returns the height of a node in pixels and zero if the element is not + * visible. The height is clamped to positive values. + * + * @param {HTMLElement} _node + */ + et2_dataview_container._nodeHeight = function (_node) { + return _node.offsetHeight; + }; + return et2_dataview_container; +}()); +exports.et2_dataview_container = et2_dataview_container; +//# sourceMappingURL=et2_dataview_view_container.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_dataview_view_container.ts b/api/js/etemplate/et2_dataview_view_container.ts new file mode 100644 index 0000000000..0281ce78fd --- /dev/null +++ b/api/js/etemplate/et2_dataview_view_container.ts @@ -0,0 +1,421 @@ +/** + * EGroupware eTemplate2 - dataview code + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage dataview + * @link http://www.egroupware.org + * @author Andreas Stöckel + * @copyright Stylite 2012 + * @version $Id$ + */ + +/*egw:uses + /vendor/bower-asset/jquery/dist/jquery.js; + et2_dataview_interfaces; +*/ + +import {et2_dataview_IInvalidatable} from "./et2_dataview_interfaces"; + +/** + * The et2_dataview_container class is the main object each dataview consits of. + * Each row, spacer as well as the grid itself are containers. A container is + * described by its parent element and a certain height. On the DOM-Level a + * container may consist of multiple "tr" nodes, which are treated as a unit. + * Some containers (like grid containers) are capable of managing a set of child + * containers. Each container can indicate, that it thinks that it's height + * might have changed. In that case it informs its parent element about that. + * The only requirement for the parent element is, that it implements the + * et2_dataview_IInvalidatable interface. + * A container does not know where it resides inside the grid, or whether it is + * currently visible or not -- this information is efficiently managed by the + * et2_dataview_grid container. + * + * @augments Class + */ +export class et2_dataview_container implements et2_dataview_IInvalidatable +{ + protected _parent: any; + + // contains all DOM-Nodes this container exists of + private _nodes: any[]; + private _inTree: boolean; + + private _attachData: { node: JQuery; prepend: boolean }; + + private _destroyCallback: Function; + _destroyContext: any; + + private _height: number; + private _index: number; + private _top: number; + + protected tr: any; + /** + * Initializes the container object. + * + * @param _parent is an object which implements the IInvalidatable + * interface. _parent may not be null. + * @memberOf et2_dataview_container + */ + constructor(_parent) + { + // Copy the given invalidation element + this._parent = _parent; + + this._nodes = []; + this._inTree = false; + this._attachData = {"node": null, "prepend": false}; + this._destroyCallback = null; + this._destroyContext = null; + + this._height = -1; + this._index = 0; + this._top = 0; + } + + /** + * Destroys this container. Classes deriving from et2_dataview_container + * should override this method and take care of unregistering all event + * handlers etc. + */ + destroy() + { + // Remove the nodes from the tree + this.removeFromTree(); + + // Call the callback function (if one is registered) + if (this._destroyCallback) + { + this._destroyCallback.call(this._destroyContext, this); + } + } + + /** + * Sets the "destroyCallback" -- the given function gets called whenever + * the container is destroyed. This instance is passed as an parameter to + * the callback. + * + * @param {function} _callback + * @param {object} _context + */ + setDestroyCallback(_callback : Function, _context : object) { + this._destroyCallback = _callback; + this._destroyContext = _context; + } + + /** + * Inserts all container nodes into the DOM tree after or before the given + * element. + * + * @param _node is the node after/before which the container "tr"s should + * get inserted. _node should be a simple DOM node, not a jQuery object. + * @param _prepend specifies whether the container should be inserted before + * or after the given node. Inserting before is needed for inserting the + * first element in front of an spacer. + */ + insertIntoTree(_node: JQuery, _prepend: boolean) + { + + if (!this._inTree && _node != null && this._nodes.length > 0) + { + // Store the parent node and indicate that this element is now in + // the tree. + this._attachData = {node: _node, prepend: _prepend}; + this._inTree = true; + + for (let i = 0; i < this._nodes.length; i++) + { + if (i == 0) + { + if (_prepend) + { + _node.before(this._nodes[0]); + } + else + { + _node.after(this._nodes[0]); + } + } + else + { + // Insert all following nodes after the previous node + this._nodes[i - 1].after(this._nodes[i]); + } + } + + // Invalidate this element in order to update the height of the + // parent + this.invalidate(); + } + } + + /** + * Removes all container nodes from the tree. + */ + removeFromTree() + { + if (this._inTree) + { + // Call the jQuery remove function to remove all nodes from the tree + // again. + for (let i = 0; i < this._nodes.length; i++) + { + this._nodes[i].remove(); + } + + // Reset the "attachData" + this._inTree = false; + this._attachData = {"node": null, "prepend": false}; + } + } + + /** + * Appends a node to the container. + * + * @param _node is the DOM-Node which should be appended. + */ + appendNode(_node : JQuery | HTMLElement) + { + // Add the given node to the "nodes" array + this._nodes.push(_node); + + // If the container is already in the tree, attach the given node to the + // tree. + if (this._inTree) + { + if (this._nodes.length === 1) + { + if (this._attachData.prepend) + { + this._attachData.node.before(_node); + } + else + { + this._attachData.node.after(_node); + } + } + else + { + this._nodes[this._nodes.length - 2].after(_node); + } + + this.invalidate(); + } + } + + /** + * Removes a certain node from the container + * + * @param {HTMLElement} _node + */ + removeNode(_node: HTMLElement) + { + // Get the index of the node in the nodes array + const idx = this._nodes.indexOf(_node); + + if (idx >= 0) + { + // Remove the node if the container is currently attached + if (this._inTree) + { + _node.parentNode.removeChild(_node); + } + + // Remove the node from the nodes array + this._nodes.splice(idx, 1); + } + } + + /** + * Returns the last node of the container - new nodes have to be appended + * after it. + */ + getLastNode() + { + + if (this._nodes.length > 0) + { + return this._nodes[this._nodes.length - 1]; + } + + return null; + } + + /** + * Returns the first node of the container. + */ + getFirstNode() + { + return this._nodes.length > 0 ? this._nodes[0] : null; + } + + /** + * Returns the accumulated height of all container nodes. Only visible nodes + * (without "display: none" etc.) are taken into account. + */ + getHeight() + { + if (this._height === -1 && this._inTree) + { + this._height = 0; + + // Setting this before measuring height helps with issues getting the + // wrong height due to margins & collapsed borders + this.tr.css('display','block'); + + // Increment the height value for each visible container node + for (let i = 0; i < this._nodes.length; i++) + { + if (et2_dataview_container._isVisible(this._nodes[i][0])) + { + this._height += et2_dataview_container._nodeHeight(this._nodes[i][0]); + } + } + this.tr.css('display',''); + } + + return ( this._height === -1 ) ? 0 : this._height; + } + + /** + * Returns a datastructure containing information used for calculating the + * average row height of a grid. + * The datastructure has the + * { + * avgHeight: , + * avgCount: + * } + */ + getAvgHeightData() + { + return { + "avgHeight": this.getHeight(), + "avgCount": 1 + }; + } + + /** + * Returns the previously set "pixel top" of the container. + */ + getTop() + { + return this._top; + } + + /** + * Returns the "pixel bottom" of the container. + */ + getBottom() + { + return this._top + this.getHeight(); + } + + /** + * Returns the range of the element. + */ + getRange() + { + return et2_bounds(this.getTop(), this.getBottom()); + } + + /** + * Returns the index of the element. + */ + getIndex() + { + return this._index; + } + + /** + * Returns how many elements this container represents. + */ + getCount() + { + return 1; + } + + /** + * Sets the top of the element. + * + * @param {number} _value + */ + setTop(_value) + { + this._top = _value; + } + + /** + * Sets the index of the element. + * + * @param {number} _value + */ + setIndex(_value) + { + this._index = _value; + } + + /* -- et2_dataview_IInvalidatable -- */ + + /** + * Broadcasts an invalidation through the container tree. Marks the own + * height as invalid. + */ + invalidate() + { + // Abort if this element is already marked as invalid. + if ( this._height !== -1) + { + // Delete the own, probably computed height + this._height = -1; + + // Broadcast the invalidation to the parent element + this._parent.invalidate(); + } + } + + + /* -- PRIVATE FUNCTIONS -- */ + + + /** + * Used to check whether an element is visible or not (non recursive). + * + * @param _obj is the element which should be checked for visibility, it is + * only checked whether some stylesheet makes the element invisible, not if + * the given object is actually inside the DOM. + */ + private static _isVisible(_obj : HTMLElement) + { + + // Check whether the element is localy invisible + if (_obj.style && (_obj.style.display === "none" + || _obj.style.visibility === "none")) + { + return false; + } + + // Get the computed style of the element + const style = window.getComputedStyle ? window.getComputedStyle(_obj, null) + // @ts-ignore + : _obj.currentStyle; + + if (style.display === "none" || style.visibility === "none") + { + return false; + } + + return true; + } + + /** + * Returns the height of a node in pixels and zero if the element is not + * visible. The height is clamped to positive values. + * + * @param {HTMLElement} _node + */ + private static _nodeHeight(_node : HTMLElement) + { + return _node.offsetHeight; + } +} diff --git a/api/js/etemplate/et2_dataview_view_grid.js b/api/js/etemplate/et2_dataview_view_grid.js index 7b770f45e0..9f5457b3d0 100644 --- a/api/js/etemplate/et2_dataview_view_grid.js +++ b/api/js/etemplate/et2_dataview_view_grid.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - Class which contains the "grid" base class * @@ -8,1382 +9,1085 @@ * @author Andreas Stöckel * @copyright Stylite 2011 * @version $Id$ - */ + * /*egw:uses - /vendor/bower-asset/jquery/dist/jquery.js; - et2_core_common; + /vendor/bower-asset/jquery/dist/jquery.js; + et2_core_common; - et2_dataview_interfaces; - et2_dataview_view_container; - et2_dataview_view_spacer; + et2_dataview_interfaces; + et2_dataview_view_container; + et2_dataview_view_spacer; */ - -/** - * Determines how many pixels the view range of the gridview is extended inside - * the scroll callback. - */ -var ET2_GRID_VIEW_EXT = 50; - -/** - * Determines the timeout after which the scroll-event is processed. - */ -var ET2_GRID_SCROLL_TIMEOUT = 50; - -/** - * Determines the timeout after which the invalidate-request gets processed. - */ -var ET2_GRID_INVALIDATE_TIMEOUT = 25; - -/** - * Determines how many elements are kept displayed outside of the current view - * range until they get removed. - */ -var ET2_GRID_HOLD_COUNT = 50; -/** - * @augments et2_dataview_container - */ -var et2_dataview_grid = (function(){ "use strict"; return et2_dataview_container.extend(et2_dataview_IViewRange, -{ - /** - * Creates the grid. - * - * @param _parent is the parent grid class - if null, this means that this - * is the outer grid which manages the scrollarea. If not null, all other - * parameters are ignored and copied from the given grid instance. - * @param _avgHeight is the starting average height of the column rows. - * @memberOf et2_dataview_grid - */ - init: function (_parent, _parentGrid, _egw, _rowProvider, _avgHeight) { - - // Call the inherited constructor - this._super(_parent); - - // If the parent is given, copy all other parameters from it - if (_parentGrid != null) - { - this.egw = _parent.egw; - this._orgAvgHeight = false; - this._rowProvider = _parentGrid._rowProvider; - } - else - { - // Otherwise copy the given parameters - this.egw = _egw; - this._orgAvgHeight = _avgHeight; - this._rowProvider = _rowProvider; - - // As this grid instance has no parent, we need a scroll container - this._scrollHeight = 0; - this._scrollTimeout = null; - } - - this._parentGrid = _parentGrid; - - this._scrollTimeout = null; - - this._invalidateTimeout = null; - - this._invalidateCallback = null; - this._invalidateContext = null; - - // Flag for stopping invalidate while working - this.doInvalidate = true; - - // _map contains a mapping between the grid indices and the elements - // associated to it. The first element in the array always refers to the - // element starting at index zero (being a spacer if the grid currently - // displays another range). - this._map = []; - - // _viewRange contains the current pixel-range of the grid which is - // visible. - this._viewRange = et2_range(0, 0); - - // Holds the maximum count of elements - this._total = 0; - - // Holds data used for storing the current average height data - this._avgHeight = false; - this._avgCount = false; - - // Build the outer grid nodes - this._createNodes(); - }, - - destroy: function () { - // Destroy all containers - this.setTotalCount(0); - - // Stop the scroll timeout - if (this._scrollTimeout) - { - window.clearTimeout(this._scrollTimeout); - } - - // Stop the invalidate timeout - if (this._invalidateTimeout) - { - window.clearTimeout(this._invalidateTimeout); - } - - this._super(); - }, - - clear: function () { - // Store the old total count and rescue the current average height in - // form of the "original average height" - var oldTotalCount = this._total; - this._orgAvgHeight = this.getAverageHeight(); - - // Set the total count to zero - this.setTotalCount(0); - - // Reset the total count value - this.setTotalCount(oldTotalCount); - }, - - /** - * Throws all elements away which are outside the current view range - */ - cleanup: function () { - // Update the pixel positions - this._recalculateElementPosition(); - - // Get the visible mapping indices and recalculate index and pixel - // position of the containers. - var mapVis = this._calculateVisibleMappingIndices(); - - // Delete all invisible elements -- if anything changed, we have to - // recalculate the pixel positions again - this._cleanupOutOfRangeElements(mapVis, 0); - }, - - /** - * The insertRow function can be called to insert the given container(s) at - * the given row index. If there currently is another container at that - * given position, the new container(s) will be inserted above the old - * container. Yet the "total count" of the grid will be preserved by - * removing the correct count of elements from the next possible spacer. If - * no spacer is found, the last containers will be removed. This causes - * inserting new containers at the end of a grid to be immediately removed - * again. - * - * @param _index is the row index at which the given container(s) should be - * inserted. - * @param _container is eiter a single et2_dataview_container instance - * which should be inserted at the given position. Or an array of - * et2_dataview_container instances. If you want to remove the container - * don't do that manually by calling its "destroy" function but use the - * deleteRow function. - */ - insertRow: function (_index, _container) { - // Calculate the map element the given index refers to - var idx = this._calculateMapIndex(_index); - - if (idx !== false) - { - // Wrap the container inside an array - if (_container instanceof et2_dataview_container) - { - _container = [_container]; - } - - // Fetch the average height - var avg = this.getAverageHeight(); - - // Call the internal _doInsertContainer function - for (var i = 0; i < _container.length; i++) - { - this._doInsertContainer(_index, idx, _container[i], avg); - } - - // Schedule an "invalidate" event - this.invalidate(); - } - }, - - /** - * The deleteRow function can be used to remove the element at the given - * index. - * - * @param _index is the index from which should be deleted. If the given - * index is outside the so called "managedRange" nothing will happen, as the - * container has already been destroyed by the grid instance. - */ - deleteRow: function (_index) { - // Calculate the map element the given index refers to - var idx = this._calculateMapIndex(_index); - - if (idx !== false) - { - this._doDeleteContainer(idx, false); - - // Schedule an "invalidate" event - this.invalidate(); - } - }, - - /** - * The given callback gets called whenever the scroll position changed or - * the visible element range changed. The element indices are passed to the - * function as et2_range. - */ - setInvalidateCallback: function (_callback, _context) { - this._invalidateCallback = _callback; - this._invalidateContext = _context; - }, - - /** - * The setDataCallback function is used to set the callback that will be - * called when the grid requires new data. - * - * @param _callback is the callback function which gets called when the grid - * needs some new rows. - * @param _context is the context in which the callback function gets - * called. - */ - setDataCallback: function (_callback, _context) { - this._callback = _callback; - this._context = _context; - }, - - /** - * The updateTotalCount function can be used to update the total count of - * rows that are displayed inside the grid. Changing the count always causes - * the spacer at the bottom (if it exists) to be - * - * @param _count specifies how many entries the grid can show. - */ - setTotalCount: function (_count) { - // Abort if the total count has not changed - if (_count === this._total) - return; - - // Calculate how many elements have to be removed/added - var delta = Math.max(0, _count) - this._total; - - if (delta > 0) - { - this._appendEmptyRows(delta); - } - else - { - this._decreaseTotal(-delta); - } - - this._total = Math.max(0, _count); - - // Schedule an invalidate - this.invalidate(); - }, - - /** - * Returns the current "total" count. - */ - getTotalCount: function () { - return this._total; - }, - - /** - * The setViewRange function updates the range in which rows are shown. - */ - setViewRange: function (_range) { - // Set the new view range - this._viewRange = _range; - - // Immediately call the "invalidate" function - this._doInvalidate(); - }, - - /** - * Return the indices of the currently visible rows. - */ - getVisibleIndexRange: function (_viewRange) { - - function getElemIdx(_elem, _px) - { - if (_elem instanceof et2_dataview_spacer) - { - return _elem.getIndex() - + Math.floor((_px - _elem.getTop()) / this.getAverageHeight()); - } - - return _elem.getIndex(); - } - - var idxTop = 0; - var idxBottom = 0; - var vr; - - if (_viewRange) - { - vr = _viewRange; - } - else - { - // Calculate the "correct" view range by removing ET2_GRID_VIEW_EXT - vr = et2_bounds( - this._viewRange.top + ET2_GRID_VIEW_EXT, - this._viewRange.bottom - ET2_GRID_VIEW_EXT); - } - - // Get the elements at the top and the bottom of the view - var topElem = null; - var botElem = null; - for (var i = 0; i < this._map.length; i++) - { - if (!topElem && this._map[i].getBottom() > vr.top) - { - topElem = this._map[i]; - } - - if (this._map[i].getTop() > vr.bottom) - { - botElem = this._map[i]; - break; - } - } - - if (!botElem) - { - botElem = this._map[this._map.length - 1]; - } - - if (topElem) - { - idxTop = getElemIdx.call(this, topElem, vr.top); - idxBottom = getElemIdx.call(this, botElem, vr.bottom); - } - - // Return the calculated index top and bottom - return et2_bounds(idxTop, idxBottom); - }, - - /** - * Returns index range of all currently managed rows. - */ - getIndexRange: function () { - var idxTop = false; - var idxBottom = false; - - for (var i = 0; i < this._map.length; i++) - { - if (!(this._map[i] instanceof et2_dataview_spacer)) - { - var idx = this._map[i].getIndex(); - - if (idxTop === false) - { - idxTop = idx; - } - - idxBottom = idx; - } - } - - return et2_bounds(idxTop, idxBottom); - }, - - /** - * Updates the scrollheight - */ - setScrollHeight: function (_height) { - this._scrollHeight = _height; - - // Update the height of the outer container - if (this.scrollarea) - { - this.scrollarea.height(_height); - } - - // Update the viewing range - this.setViewRange(et2_range(this._viewRange.top, this._scrollHeight)); - }, - - /** - * Returns the average row height data, overrides the corresponding function - * of the et2_dataview_container. - */ - getAvgHeightData: function () { - - if (this._avgHeight === false) - { - var avgCount = 0; - var avgSum = 0; - - for (var i = 0; i < this._map.length; i++) - { - var data = this._map[i].getAvgHeightData(); - - if (data !== null) - { - avgSum += data.avgHeight * data.avgCount; - avgCount += data.avgCount; - } - } - - // Calculate the average height, but only if we have a height - if (avgCount > 0 && avgSum > 0) - { - this._avgHeight = avgSum / avgCount; - this._avgCount = avgCount; - } - } - - // Return the calculated average height if it is available - if (this._avgHeight !== false) - { - return { - "avgCount": this._avgCount, - "avgHeight": this._avgHeight - }; - } - - // Otherwise return the parent average height - if (this._parent) - { - return this._parent.getAvgHeightData(); - } - - // Otherwise return the original average height given in the constructor - if (this._orgAvgHeight !== false) - { - return { - "avgCount": 1, - "avgHeight": this._orgAvgHeight - }; - } - return null; - }, - - /** - * Returns the average row height in pixels. - */ - getAverageHeight: function () { - var data = this.getAvgHeightData(); - return data ? data.avgHeight : 19; - }, - - /** - * Returns the row provider. - */ - getRowProvider: function () { - return this._rowProvider; - }, - - /** - * Called whenever the size of this or another element in the container tree - * changes. - */ - invalidate: function() { - - // Clear any existing "invalidate" timeout - if (this._invalidateTimeout) - { - window.clearTimeout(this._invalidateTimeout); - } - - if(!this.doInvalidate) - { - return; - } - - var self = this; - var _super = this._super; - this._invalidateTimeout = window.setTimeout(function() { - egw.debug("log","Dataview grid timed invalidate"); - // Clear the "_avgHeight" - self._avgHeight = false; - self._avgCount = false; - self._invalidateTimeout = null; - self._doInvalidate(_super); - }, ET2_GRID_INVALIDATE_TIMEOUT); - }, - - /** - * Makes the given index visible: TODO: Propagate this to the parent grid. - */ - makeIndexVisible: function (_idx) - { - // Get the element range - var elemRange = this._getElementRange(_idx); - - // Abort if the index was out of range - if (!elemRange) - { - return false; - } - - // Calculate the current visible range - var visibleRange = et2_bounds( - this._viewRange.top + ET2_GRID_VIEW_EXT, - this._viewRange.bottom - ET2_GRID_VIEW_EXT); - - // Check whether the element is currently completely visible -- if yes, - // do nothing - if (visibleRange.top < elemRange.top - && visibleRange.bottom > elemRange.bottom) - { - return true; - } - - if (elemRange.top < visibleRange.top) - { - this.scrollarea.scrollTop(elemRange.top); - } - else - { - var h = elemRange.bottom - elemRange.top; - this.scrollarea.scrollTop(elemRange.top - this._scrollHeight + h); - } - - }, - - - /* ---- PRIVATE FUNCTIONS ---- */ - -/* _inspectStructuralIntegrity: function() { - var idx = 0; - for (var i = 0; i < this._map.length; i++) - { - if (this._map[i].getIndex() != idx) - { - throw "Index missmatch!"; - } - idx += this._map[i].getCount(); - } - - if (idx !== this._total) - { - throw "Total count missmatch!"; - } - },*/ - - /** - * Translates the given index to a range, returns false if the index is - * out of range. - */ - _getElementRange: function (_idx) - { - // Recalculate the element positions - this._recalculateElementPosition(); - - // Translate the given index to the map index - var mapIdx = this._calculateMapIndex(_idx); - - // Do nothing if the given index is out of range - if (mapIdx === false) - { - return false; - } - - // Get the map element - var elem = this._map[mapIdx]; - - // Get the element range - if (elem instanceof et2_dataview_spacer) - { - var avg = this.getAverageHeight(); - return et2_range(elem.getTop() + avg * (elem.getIndex() - _idx), - avg); - } - - return elem.getRange(); - }, - - /** - * Recalculates the position of the currently managed containers. This - * routine only updates the pixel position of the elements -- the index of - * the elements is guaranteed to be maintained correctly by all high level - * functions of the grid, as the index position is needed to be correct for - * the "deleteRow" and "insertRow" functions, and we cannot effort to call - * this calculation method after every change in the grid mapping. - */ - _recalculateElementPosition: function() { - for (var i = 0; i < this._map.length; i++) - { - if (i == 0) - { - this._map[i].setTop(0); - } - else - { - this._map[i].setTop(this._map[i - 1].getBottom()); - } - } - }, - - /** - * The "_calculateVisibleMappingIndices" function calculates the indices of - * the _map array, which refer to containers that are currently (partially) - * visible. This function is used internally by "_doInvalidate". - */ - _calculateVisibleMappingIndices: function() { - // First update the "top" and "bottom", and "index" values of all - // managed elements, and at the same time calculate the mapping indices - // of the elements which are inside the current view range. - var mapVis = {"top": false, "bottom": false}; - - for (var i = 0; i < this._map.length; i++) - { - // Update the top of the "map visible index" -- set it to the first - // element index, where the bottom line is beneath the top line - // of the view range. - if (mapVis.top === false - && this._map[i].getBottom() > this._viewRange.top) - { - mapVis.top = i; - } - - // Update the bottom of the "map visible index" -- set it to the - // first element index, where the top line is beneath the bottom - // line of the view range. - if (mapVis.bottom === false - && this._map[i].getTop() > this._viewRange.bottom) - { - mapVis.bottom = i; - break; - } - } - - return mapVis; - }, - - /** - * Deletes all elements which are "out of the view range". This function is - * internally used by "_doInvalidate". How many elements that are out of the - * view range get preserved fully depends on the _holdCount parameter - * variable. - * - * @param _mapVis contains the _map indices of the just visible containers. - * @param _holdCount contains the number of elements that should be kept, - * if not given, this parameter defaults to ET2_GRID_HOLD_COUNT - */ - _cleanupOutOfRangeElements: function(_mapVis, _holdCount) { - - // Iterates over the map from and to the given indices and pushes all - // elements onto the given array, which are more than _holdCount - // elements remote from the start. - function searchElements(_arr, _start, _stop, _dir) - { - var dist = 0; - for (var i = _start; _dir > 0 ? i <= _stop : i >= _stop; i += _dir) - { - if (dist > _holdCount) - { - _arr.push(i); - } - else - { - dist += this._map[i].getCount(); - } - } - } - - // Set _holdCount to ET2_GRID_HOLD_COUNT if the parameters is not given - _holdCount = typeof _holdCount === "undefined" ? ET2_GRID_HOLD_COUNT : - _holdCount; - - // Collect all elements that will be deleted at the top and at the - // bottom of the grid - var deleteTop = []; - var deleteBottom = []; - - if (_mapVis.top !== false) - { - searchElements.call(this, deleteTop, _mapVis.top, 0, -1); - } - - if (_mapVis.bottom !== false) - { - searchElements.call(this, deleteBottom, _mapVis.bottom, - this._map.length - 1, 1); - } - - // The offset variable specifies how many elements have been deleted - // from the map -- this variable is needed as deleting elements from the - // map shifts the map indices. We iterate in oposite direction over the - // elements, as this allows the _doDeleteContainer/ container function - // to extend the (possibly) existing spacer at the top of the grid - var offs = 0; - for (var i = deleteTop.length - 1; i >= 0; i--) - { - // Delete the container and calculate the new offset - var mapLength = this._map.length; - this._doDeleteContainer(deleteTop[i] - offs, true); - offs += mapLength - this._map.length; - } - - for (var i = deleteBottom.length - 1; i >= 0; i--) - { - this._doDeleteContainer(deleteBottom[i] - offs, true); - } - - return deleteBottom.length + deleteTop.length > 0; - }, - - /** - * The _updateContainers function is used internally by "_doInvalidate" in - * order to call the "setViewRange" function of all containers the implement - * that interfaces (needed for nested grids), and to request new elements - * for all currently visible spacers. - */ - _updateContainers: function() { - for (var i = 0; i < this._map.length; i++) - { - var container = this._map[i]; - - // Check which type the container object has - var isSpacer = container instanceof et2_dataview_spacer; - var hasIViewRange = !isSpacer - && container.implements(et2_dataview_IViewRange); - - // If the container has one of those special types, calculate the - // view range and use that to update the view range of the element - // or to request new elements for the spacer - if (isSpacer || hasIViewRange) - { - // Calculate the relative view range and check whether - // the element is really visible - var elemRange = container.getRange(); - - // Abort if the element is not inside the visible range - if (!et2_rangeIntersect(this._viewRange, elemRange)) - { - continue; - } - - if (hasIViewRange) - { - // Update the view range of the container - container.setViewRange(et2_bounds( - this._viewRange.top - elemRange.top, - this._viewRange.bottom - elemRange.top)); - } - else // This container is a spacer - { - // Obtain the average element height - var avg = container._rowHeight; - - // Get the visible container range (vcr) - var vcr_top = Math.max(this._viewRange.top, elemRange.top); - var vcr_bot = Math.min(this._viewRange.bottom, elemRange.bottom); - - // Calculate the indices of the elements which will be - // requested - var cidx = container.getIndex(); - var ccnt = container.getCount(); - - // Calculate the start index -- prevent vtop from getting - // negative (and so idxStart being smaller than cidx) and - // ensure that idxStart is not larger than the maximum - // container index. - var vtop = Math.max(0, vcr_top); - var idxStart = Math.floor( - Math.min(cidx + ccnt - 1, - cidx + (vtop - elemRange.top) / avg, - this._total - )); - - // Calculate the end index -- prevent vtop from getting - // negative (and so idxEnd being smaller than cidx) and - // ensure that idxEnd is not larger than the maximum - // container index. - var vbot = Math.max(0, vcr_bot); - var idxEnd = Math.ceil( - Math.min(cidx + ccnt - 1, - cidx + (vbot - elemRange.top) / avg, - this._total - )); - - // Initial resize while the grid is hidden will give NaN - // This is an important optimisation, as it is involved in not - // loading all rows, so we override in that case so - // there are more than the 2-3 that fit in the min height. - if(isNaN(idxStart) && isSpacer) idxStart = cidx-1; - if(isNaN(idxEnd) && isSpacer && this._scrollHeight > 0 && elemRange.bottom == 0) - { - idxEnd = Math.min(ccnt,cidx + Math.ceil( - (this._viewRange.bottom - container._top) / this._orgAvgHeight - )); - } - - // Call the data callback - if (this._callback) - { - var self = this; - egw.debug("log","Dataview grid flag for update: ", {start:idxStart,end:idxEnd}); - window.setTimeout(function() { - // If row template changes, self._callback might disappear - if(typeof self._callback != "undefined") - { - self._callback.call(self._context, idxStart, idxEnd); - } - }, 0); - } - } - } - } - }, - - /** - * Invalidate iterates over the "mapping" array. It calculates which - * containers have to be removed and where new containers should be added. - */ - _doInvalidate: function(_super) { - if(!this.doInvalidate) return; - - // Update the pixel positions - this._recalculateElementPosition(); - - // Call the callback - if (this._invalidateCallback) - { - var range = this.getVisibleIndexRange( - et2_range(this.scrollarea.scrollTop(), this._scrollHeight)); - this._invalidateCallback.call(this._invalidateContext, range); - } - - // Get the visible mapping indices and recalculate index and pixel - // position of the containers. - var mapVis = this._calculateVisibleMappingIndices(); - - // Delete all invisible elements -- if anything changed, we have to - // recalculate the pixel positions again - if (this._cleanupOutOfRangeElements(mapVis)) - { - this._recalculateElementPosition(); - } - - // Update the view range of all visible elements that implement the - // corresponding interface and request elements for all visible spacers - this._updateContainers(); - - // Call the inherited invalidate function, broadcast the invalidation - // through the container tree. - if (this._parent && _super) - { - _super.call(this); - } - }, - - /** - * Translates the given grid index into the element index of the map. If the - * given index is completely out of the range, "false" is returned. - */ - _calculateMapIndex: function(_index) { - - var top = 0; - var bot = this._map.length - 1; - - while (top <= bot) - { - var idx = Math.floor((top + bot) / 2); - var elem = this._map[idx]; - - var realIdx = elem.getIndex(); - var realCnt = elem.getCount(); - - if (_index >= realIdx && _index < realIdx + realCnt) - { - return idx; - } - else if (_index < realIdx) - { - bot = idx - 1; - } - else - { - top = idx + 1; - } - } - - return false; - }, - - _insertContainerAtSpacer: function(_index, _mapIndex, _mapElem, _container, - _avg) { - // Set the index of the new container - _container.setIndex(_index); - - // Calculate at which position the spacer has to be splitted - var splitIdx = _index - _mapElem.getIndex(); - - // Get the count of elements that remain at the top of the splitter - var cntTop = splitIdx; - - // Get the count of elements that remain at the bottom of the splitter - // -- it has to be one element less than before - var cntBottom = _mapElem.getCount() - splitIdx - 1; - - // Split the containers if cntTop and cntBottom are larger than zero - if (cntTop > 0 && cntBottom > 0) - { - // Set the new count of the currently existing container, preserving - // its height as it was - _mapElem.setCount(cntTop); - - // Add the new element after the old container - _container.insertIntoTree(_mapElem.getLastNode()); - - // Create a new spacer and add it after the newly inserted container - var newSpacer = new et2_dataview_spacer(this, - this._rowProvider); - newSpacer.setCount(cntBottom, _avg); - newSpacer.setIndex(_index + 1); - newSpacer.insertIntoTree(_container.getLastNode()); - - // Insert the container and the new spacer into the map - this._map.splice(_mapIndex + 1, 0, _container, newSpacer); - } - else if (cntTop === 0 && cntBottom > 0) - { - // Simply adjust the size of the old spacer and insert the new - // container in front of it - _container.insertIntoTree(_mapElem.getFirstNode(), true); - _mapElem.setIndex(_index + 1); - _mapElem.setCount(cntBottom, _avg); - - this._map.splice(_mapIndex, 0, _container); - } - else if (cntTop > 0 && cntBottom === 0) - { - // Simply add the new container to the end of the old container and - // adjust the count of the old spacer to the remaining count. - _container.insertIntoTree(_mapElem.getLastNode()); - _mapElem.setCount(cntTop); - - this._map.splice(_mapIndex + 1, 0, _container); - } - else // if (cntTop === 0 && cntBottom === 0) - { - // Append the new container to the current container and then - // destroy the old container - _container.insertIntoTree(_mapElem.getLastNode()); - _mapElem.free(); - - this._map.splice(_mapIndex, 1, _container); - } - }, - - _insertContainerAtElement: function(_index, _mapIndex, _mapElem, _container, - _avg) { - // In a first step, simply insert the element at the specified position, - // in front of the element _mapElem. - _container.setIndex(_index); - _container.insertIntoTree(_mapElem.getFirstNode(), true); - this._map.splice(_mapIndex, 0, _container); - - // Search for the next spacer and increment the indices of all other - // elements until there - var _newIndex = _index + 1; - for (var i = _mapIndex + 1; i < this._map.length; i++) - { - // Update the index of the element - this._map[i].setIndex(_newIndex++); - - // We've found a spacer -- decrement its element count and abort - if (this._map[i] instanceof et2_dataview_spacer) - { - this._decrementSpacerCount(i, _avg); - return; - } - } - - // We've found no spacer so far, remove the last element from the map in - // order to obtain the "totalCount" (especially the last element is no - // spacer, so the following code cannot remove a spacer) - this._map.pop().free(); - }, - - /** - * Inserts the given container at the given index. - */ - _doInsertContainer: function(_index, _mapIndex, _container, _avg) { - // Check whether the given element at the map index is a spacer. If - // yes, we have to split the spacer at that position. - var mapElem = this._map[_mapIndex]; - - if (mapElem instanceof et2_dataview_spacer) - { - this._insertContainerAtSpacer(_index, _mapIndex, mapElem, - _container, _avg); - } - else - { - this._insertContainerAtElement(_index, _mapIndex, mapElem, - _container, _avg); - } - -// this._inspectStructuralIntegrity(); - }, - - /** - * Replaces the container at the given index with a spacer. The function - * tries to extend any spacer lying above or below the given _mapIndex. - * This code does not destroy the given container, but maintains its map - * index. - * - * @param _mapIndex is the index of _mapElem in the _map array. - * @param _mapElem is the container which should be replaced. - */ - _replaceContainerWithSpacer: function(_mapIndex, _mapElem) { - // Check whether a spacer can be extended above or below the given - // _mapIndex - var spacerAbove = null; - var spacerBelow = null; - - if (_mapIndex > 0 - && this._map[_mapIndex - 1] instanceof et2_dataview_spacer) - { - spacerAbove = this._map[_mapIndex - 1]; - } - - if (_mapIndex < this._map.length - 1 - && this._map[_mapIndex + 1] instanceof et2_dataview_spacer) - { - spacerBelow = this._map[_mapIndex + 1]; - } - - if (!spacerAbove && !spacerBelow) - { - // No spacer can be extended -- simply create a new one - var spacer = new et2_dataview_spacer(this, this._rowProvider); - spacer.setIndex(_mapElem.getIndex()); - spacer.addAvgHeight(_mapElem.getHeight()); - spacer.setCount(1, _mapElem.getHeight()); - - // Insert the new spacer at the correct place into the DOM tree and - // the mapping - spacer.insertIntoTree(_mapElem.getLastNode()); - this._map.splice(_mapIndex + 1, 0, spacer); - } - else if (spacerAbove && spacerBelow) - { - // We're going to consolidate the upper and the lower spacer. To do - // that we'll calculate a new count of elements and a new average - // height, so that the upper container can get the height of all - // three elements together - var totalHeight = spacerAbove.getHeight() + spacerBelow.getHeight() - + _mapElem.getHeight(); - var totalCount = spacerAbove.getCount() + spacerBelow.getCount() - + 1; - var newAvg = totalHeight / totalCount; - - // Update the upper spacer - spacerAbove.addAvgHeight(_mapElem.getHeight()); - spacerAbove.setCount(totalCount, newAvg); - - // Delete the lower spacer and remove it from the mapping - spacerBelow.free(); - this._map.splice(_mapIndex + 1, 1); - } - else - { - // One of the two spacers is available - var spacer = spacerAbove || spacerBelow; - - // Calculate the new count and the new average height of that spacer - var totalCount = spacer.getCount() + 1; - var totalHeight = spacer.getHeight() + _mapElem.getHeight(); - var newAvg = totalHeight / totalCount; - - // Set the new container height - spacer.setIndex(Math.min(spacer.getIndex(), _mapElem.getIndex())); - spacer.addAvgHeight(_mapElem.getHeight()); - spacer.setCount(totalCount, newAvg); - } - }, - - /** - * Checks whether there is another spacer below the given map index and if - * yes, consolidates the two. - */ - _consolidateSpacers: function(_mapIndex) { - if (_mapIndex < this._map.length - 1 - && this._map[_mapIndex] instanceof et2_dataview_spacer - && this._map[_mapIndex + 1] instanceof et2_dataview_spacer) - { - var spacerAbove = this._map[_mapIndex]; - var spacerBelow = this._map[_mapIndex + 1]; - - // Calculate the new height/count of both containers - var totalHeight = spacerAbove.getHeight() + spacerBelow.getHeight(); - var totalCount = spacerAbove.getCount() + spacerBelow.getCount(); - var newAvg = totalCount / totalHeight; - - // Extend the new spacer - spacerAbove.setCount(totalCount, newAvg); - - // Delete the old spacer - spacerBelow.free(); - this._map.splice(_mapIndex + 1, 1); - } - }, - - /** - * Decrements the count of the spacer at the given _mapIndex by one. If the - * given spacer has no more elements, it will be removed from the mapping. - * Note that this function does not update the indices of the following - * elements, this function is only used internally by the - * _insertContainerAtElement function and the _doDeleteContainer function - * where appropriate adjustments to the map data structure are done. - * - * @param _mapIndex is the index of the spacer in the "map" data structure. - * @param _avg is the new average height of the container, may be - * "undefined" in which case the height of the spacer rows is kept as it - * was. - */ - _decrementSpacerCount: function(_mapIndex, _avg) { - var cnt = this._map[_mapIndex].getCount() - 1; - if (cnt > 0) - { - this._map[_mapIndex].setCount(cnt, _avg); - } - else - { - this._map[_mapIndex].free(); - this._map.splice(_mapIndex, 1); - } - }, - - /** - * Deletes the container at the given index. - */ - _doDeleteContainer: function(_mapIndex, _replaceWithSpacer) { - // _replaceWithSpacer defaults to false - _replaceWithSpacer = _replaceWithSpacer ? _replaceWithSpacer : false; - - // Fetch the element at the given map index - var mapElem = this._map[_mapIndex]; - - // Indicates whether an element has really been removed -- if yes, the - // bottom spacer will be extended - var removedElement = false; - - // Check whether the map element is a spacer -- if yes, we have to do - // some special treatment - if (mapElem instanceof et2_dataview_spacer) - { - // Do nothing if the "_replaceWithSpacer" flag is true as the - // element already is a spacer - if (!_replaceWithSpacer) - { - this._decrementSpacerCount(_mapIndex); - removedElement = true; - } - } - else - { - if (_replaceWithSpacer) - { - this._replaceContainerWithSpacer(_mapIndex, mapElem); - } - else - { - removedElement = true; - } - - // Remove the complete (current) container, decrement the _mapIndex - this._map[_mapIndex].free(); - this._map.splice(_mapIndex, 1); - _mapIndex--; - - // The delete operation may have created two joining spacers -- this - // is highly suboptimal, so we'll consolidate those two spacers - this._consolidateSpacers(_mapIndex); - } - - // Update the indices of all elements after the current one, if we've - // really removed an element - if (removedElement) - { - for (var i = _mapIndex + 1; i < this._map.length; i++) - { - this._map[i].setIndex(this._map[i].getIndex() - 1); - } - - // Extend the last spacer as we have to maintain the spacer count - this._appendEmptyRows(1); - } - -// this._inspectStructuralIntegrity(); - }, - - /** - * The appendEmptyRows function is used internally to append empty rows to - * the end of the table. This functionality is needed in order to maintain - * the "total count" in the _doDeleteContainer function and to increase the - * "total count" in the "setCount" function. - * - * @param _count specifies the count of empty rows that will be added to the - * end of the table. - */ - _appendEmptyRows: function(_count) { - // Special case -- the last element in the "_map" is no spacer -- this - // means, that the "managedRange" is currently at the bottom of the list - // -- so we have to insert a new spacer - var spacer = null; - var lastIndex = this._map.length - 1; - if (this._map.length === 0 || - !(this._map[lastIndex] instanceof et2_dataview_spacer)) - { - // Create a new spacer - spacer = new et2_dataview_spacer(this, this._rowProvider); - - // Insert the spacer -- we have a special case if there currently is - // no element inside the mapping - if (this._map.length === 0) - { - // Add a dummy element to the grid - var dummy = jQuery(document.createElement("tr")); - this.innerTbody.append(dummy); - - // Append the spacer to the grid - spacer.setIndex(0); - spacer.insertIntoTree(dummy, false); - - // Remove the dummy element - dummy.remove(); - } - else - { - // Insert the new spacer after the last element - spacer.setIndex(this._map[lastIndex].getIndex() + 1); - spacer.insertIntoTree(this._map[lastIndex].getLastNode()); - } - - // Add the new spacer to the mapping - this._map.push(spacer); - } - else - { - // Get the spacer at the bottom of the mapping - spacer = this._map[lastIndex]; - } - - // Update the spacer count - spacer.setCount(_count + spacer.getCount(), this.getAverageHeight()); - }, - - /** - * The _decreaseTotal function is used to decrease the total row count in - * the grid. It tries to remove the given count of rows from the spacer - * located at the bottom of the grid, if this is not possible, it starts - * removing complete rows. - * - * @param _delta specifies how many rows should be removed. - */ - _decreaseTotal: function(_delta) { - // Iterate over the current mapping, starting at the bottom and delete - // rows. _delta is decreased for each removed row. Abort when delta is - // zero or the map is empty - while (_delta > 0 && this._map.length > 0) - { - var cont = this._map[this._map.length - 1]; - - // Remove as many containers as possible from spacers - if (cont instanceof et2_dataview_spacer) - { - var diff = cont.getCount() - _delta; - - if (diff > 0) - { - // We're done as the spacer still has entries left - _delta = 0; - cont.setCount(diff, this.getAverageHeight()); - break; - } - else - { - // Decrease _delta by the count of rows the spacer had - _delta -= diff + _delta; - } - } - else - { - // We're going to remove a single row: remove it - _delta -= 1; - } - - // Destroy the container if there are no rows left - cont.free(); - this._map.pop(); - } - - // Check whether _delta is really zero - if (_delta > 0) - { - this.egw.debug('error', "Error while decreasing the total count - requested to remove more rows than available."); - } - }, - - /** - * Creates the grid DOM-Nodes - */ - _createNodes: function() { - - this.tr = jQuery(document.createElement("tr")); - - this.outerCell = jQuery(document.createElement("td")) - .addClass("frame") - .attr("colspan", this._rowProvider.getColumnCount() - + (this._parentGrid ? 0 : 1)) - .appendTo(this.tr); - - // Create the scrollarea div if this is the outer grid - this.scrollarea = null; - if (this._parentGrid == null) - { - this.scrollarea = jQuery(document.createElement("div")) - .addClass("egwGridView_scrollarea") - .scroll(this, function(e) { - - // Clear any older scroll timeout - if (e.data._scrollTimeout) - { - window.clearTimeout(e.data._scrollTimeout); - } - - // Clear any existing "invalidate" timeout (as the - // "setViewRange" function triggered by the scroll event - // forces an "invalidate"). - if (e.data._invalidateTimeout) - { - window.clearTimeout(e.data._invalidateTimeout); - e.data._invalidateTimeout = null; - } - - // Set a new timeout which calls the setViewArea - // function - e.data._scrollTimeout = window.setTimeout(jQuery.proxy(function() { - var newRange = et2_range( - this.scrollarea.scrollTop() - ET2_GRID_VIEW_EXT, - this._scrollHeight + ET2_GRID_VIEW_EXT * 2 - ); - - if (!et2_rangeEqual(newRange, this._viewRange)) - { - this.setViewRange(newRange); - } - },e.data), ET2_GRID_SCROLL_TIMEOUT); - }) - .height(this._scrollHeight) - .appendTo(this.outerCell); - } - - // Create the inner table - var table = jQuery(document.createElement("table")) - .addClass("egwGridView_grid") - .appendTo(this.scrollarea ? this.scrollarea : this.outerCell); - - this.innerTbody = jQuery(document.createElement("tbody")) - .appendTo(table); - - // Set the tr as container element - this.appendNode(jQuery(this.tr[0])); - } - -});}).call(this); - +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 }); +var et2_dataview_view_container_1 = require("./et2_dataview_view_container"); +var et2_dataview_view_spacer_1 = require("./et2_dataview_view_spacer"); +var et2_dataview_grid = /** @class */ (function (_super_1) { + __extends(et2_dataview_grid, _super_1); + /** + * Creates the grid. + * + * @param _parent is the parent grid class - if null, this means that this + * is the outer grid which manages the scrollarea. If not null, all other + * parameters are ignored and copied from the given grid instance. + * @param _parentGrid + * @param _egw + * @param _rowProvider + * @param _avgHeight is the starting average height of the column rows. + * @memberOf et2_dataview_grid + */ + function et2_dataview_grid(_parent, _parentGrid, _egw, _rowProvider, _avgHeight) { + var _this = + // Call the inherited constructor + _super_1.call(this, _parent) || this; + // If the parent is given, copy all other parameters from it + if (_parentGrid != null) { + _this.egw = _parent.egw; + _this._orgAvgHeight = false; + _this._rowProvider = _parentGrid._rowProvider; + } + else { + // Otherwise copy the given parameters + _this.egw = _egw; + _this._orgAvgHeight = _avgHeight; + _this._rowProvider = _rowProvider; + // As this grid instance has no parent, we need a scroll container + _this._scrollHeight = 0; + _this._scrollTimeout = null; + } + _this._parentGrid = _parentGrid; + _this._scrollTimeout = null; + _this._invalidateTimeout = null; + _this._invalidateCallback = null; + _this._invalidateContext = null; + // Flag for stopping invalidate while working + _this.doInvalidate = true; + // _map contains a mapping between the grid indices and the elements + // associated to it. The first element in the array always refers to the + // element starting at index zero (being a spacer if the grid currently + // displays another range). + _this._map = []; + // _viewRange contains the current pixel-range of the grid which is + // visible. + _this._viewRange = et2_range(0, 0); + // Holds the maximum count of elements + _this._total = 0; + // Holds data used for storing the current average height data + _this._avgHeight = false; + _this._avgCount = -1; + // Build the outer grid nodes + _this._createNodes(); + return _this; + } + et2_dataview_grid.prototype.destroy = function () { + // Destroy all containers + this.setTotalCount(0); + // Stop the scroll timeout + if (this._scrollTimeout) { + window.clearTimeout(this._scrollTimeout); + } + // Stop the invalidate timeout + if (this._invalidateTimeout) { + window.clearTimeout(this._invalidateTimeout); + } + _super_1.prototype.destroy.call(this); + }; + et2_dataview_grid.prototype.clear = function () { + // Store the old total count and rescue the current average height in + // form of the "original average height" + var oldTotalCount = this._total; + this._orgAvgHeight = this.getAverageHeight(); + // Set the total count to zero + this.setTotalCount(0); + // Reset the total count value + this.setTotalCount(oldTotalCount); + }; + /** + * Throws all elements away which are outside the current view range + */ + et2_dataview_grid.prototype.cleanup = function () { + // Update the pixel positions + this._recalculateElementPosition(); + // Get the visible mapping indices and recalculate index and pixel + // position of the containers. + var mapVis = this._calculateVisibleMappingIndices(); + // Delete all invisible elements -- if anything changed, we have to + // recalculate the pixel positions again + this._cleanupOutOfRangeElements(mapVis, 0); + }; + /** + * The insertRow function can be called to insert the given container(s) at + * the given row index. If there currently is another container at that + * given position, the new container(s) will be inserted above the old + * container. Yet the "total count" of the grid will be preserved by + * removing the correct count of elements from the next possible spacer. If + * no spacer is found, the last containers will be removed. This causes + * inserting new containers at the end of a grid to be immediately removed + * again. + * + * @param _index is the row index at which the given container(s) should be + * inserted. + * @param _container is eiter a single et2_dataview_container instance + * which should be inserted at the given position. Or an array of + * et2_dataview_container instances. If you want to remove the container + * don't do that manually by calling its "destroy" function but use the + * deleteRow function. + */ + et2_dataview_grid.prototype.insertRow = function (_index, _container) { + // Calculate the map element the given index refers to + var idx = this._calculateMapIndex(_index); + if (idx !== false) { + // Wrap the container inside an array + if (_container instanceof et2_dataview_view_container_1.et2_dataview_container) { + _container = [_container]; + } + // Fetch the average height + var avg = this.getAverageHeight(); + // Call the internal _doInsertContainer function + for (var i = 0; i < _container.length; i++) { + this._doInsertContainer(_index, idx, _container[i], avg); + } + // Schedule an "invalidate" event + this.invalidate(); + } + }; + /** + * The deleteRow function can be used to remove the element at the given + * index. + * + * @param _index is the index from which should be deleted. If the given + * index is outside the so called "managedRange" nothing will happen, as the + * container has already been destroyed by the grid instance. + */ + et2_dataview_grid.prototype.deleteRow = function (_index) { + // Calculate the map element the given index refers to + var idx = this._calculateMapIndex(_index); + if (idx !== false) { + this._doDeleteContainer(idx, false); + // Schedule an "invalidate" event + this.invalidate(); + } + }; + /** + * The given callback gets called whenever the scroll position changed or + * the visible element range changed. The element indices are passed to the + * function as et2_range. + */ + et2_dataview_grid.prototype.setInvalidateCallback = function (_callback, _context) { + this._invalidateCallback = _callback; + this._invalidateContext = _context; + }; + /** + * The setDataCallback function is used to set the callback that will be + * called when the grid requires new data. + * + * @param _callback is the callback function which gets called when the grid + * needs some new rows. + * @param _context is the context in which the callback function gets + * called. + */ + et2_dataview_grid.prototype.setDataCallback = function (_callback, _context) { + this._callback = _callback; + this._context = _context; + }; + /** + * The updateTotalCount function can be used to update the total count of + * rows that are displayed inside the grid. Changing the count always causes + * the spacer at the bottom (if it exists) to be + * + * @param _count specifies how many entries the grid can show. + */ + et2_dataview_grid.prototype.setTotalCount = function (_count) { + // Abort if the total count has not changed + if (_count === this._total) + return; + // Calculate how many elements have to be removed/added + var delta = Math.max(0, _count) - this._total; + if (delta > 0) { + this._appendEmptyRows(delta); + } + else { + this._decreaseTotal(-delta); + } + this._total = Math.max(0, _count); + // Schedule an invalidate + this.invalidate(); + }; + /** + * Returns the current "total" count. + */ + et2_dataview_grid.prototype.getTotalCount = function () { + return this._total; + }; + /** + * The setViewRange function updates the range in which rows are shown. + */ + et2_dataview_grid.prototype.setViewRange = function (_range) { + // Set the new view range + this._viewRange = _range; + // Immediately call the "invalidate" function + this._doInvalidate(); + }; + /** + * Return the indices of the currently visible rows. + */ + et2_dataview_grid.prototype.getVisibleIndexRange = function (_viewRange) { + function getElemIdx(_elem, _px) { + if (_elem instanceof et2_dataview_view_spacer_1.et2_dataview_spacer) { + return _elem.getIndex() + + Math.floor((_px - _elem.getTop()) / this.getAverageHeight()); + } + return _elem.getIndex(); + } + var idxTop = 0; + var idxBottom = 0; + var vr; + if (_viewRange) { + vr = _viewRange; + } + else { + // Calculate the "correct" view range by removing ET2_GRID_VIEW_EXT + vr = et2_bounds(this._viewRange.top + et2_dataview_grid.ET2_GRID_VIEW_EXT, this._viewRange.bottom - et2_dataview_grid.ET2_GRID_VIEW_EXT); + } + // Get the elements at the top and the bottom of the view + var topElem = null; + var botElem = null; + for (var i = 0; i < this._map.length; i++) { + if (!topElem && this._map[i].getBottom() > vr.top) { + topElem = this._map[i]; + } + if (this._map[i].getTop() > vr.bottom) { + botElem = this._map[i]; + break; + } + } + if (!botElem) { + botElem = this._map[this._map.length - 1]; + } + if (topElem) { + idxTop = getElemIdx.call(this, topElem, vr.top); + idxBottom = getElemIdx.call(this, botElem, vr.bottom); + } + // Return the calculated index top and bottom + return et2_bounds(idxTop, idxBottom); + }; + /** + * Returns index range of all currently managed rows. + */ + et2_dataview_grid.prototype.getIndexRange = function () { + var idxTop = false; + var idxBottom = false; + for (var i = 0; i < this._map.length; i++) { + if (!(this._map[i] instanceof et2_dataview_view_spacer_1.et2_dataview_spacer)) { + var idx = this._map[i].getIndex(); + if (idxTop === false) { + idxTop = idx; + } + idxBottom = idx; + } + } + return et2_bounds(idxTop, idxBottom); + }; + /** + * Updates the scrollheight + */ + et2_dataview_grid.prototype.setScrollHeight = function (_height) { + this._scrollHeight = _height; + // Update the height of the outer container + if (this.scrollarea) { + this.scrollarea.height(_height); + } + // Update the viewing range + this.setViewRange(et2_range(this._viewRange.top, this._scrollHeight)); + }; + /** + * Returns the average row height data, overrides the corresponding function + * of the et2_dataview_container. + */ + et2_dataview_grid.prototype.getAvgHeightData = function () { + if (this._avgHeight === false) { + var avgCount = 0; + var avgSum = 0; + for (var i = 0; i < this._map.length; i++) { + var data = this._map[i].getAvgHeightData(); + if (data !== null) { + avgSum += data.avgHeight * data.avgCount; + avgCount += data.avgCount; + } + } + // Calculate the average height, but only if we have a height + if (avgCount > 0 && avgSum > 0) { + this._avgHeight = avgSum / avgCount; + this._avgCount = avgCount; + } + } + // Return the calculated average height if it is available + if (this._avgHeight !== false) { + return { + "avgCount": this._avgCount, + "avgHeight": this._avgHeight + }; + } + // Otherwise return the parent average height + if (this._parent) { + return this._parent.getAvgHeightData(); + } + // Otherwise return the original average height given in the constructor + if (this._orgAvgHeight !== false) { + return { + "avgCount": 1, + "avgHeight": this._orgAvgHeight + }; + } + return null; + }; + /** + * Returns the average row height in pixels. + */ + et2_dataview_grid.prototype.getAverageHeight = function () { + var data = this.getAvgHeightData(); + return data ? data.avgHeight : 19; + }; + /** + * Returns the row provider. + */ + et2_dataview_grid.prototype.getRowProvider = function () { + return this._rowProvider; + }; + /** + * Called whenever the size of this or another element in the container tree + * changes. + */ + et2_dataview_grid.prototype.invalidate = function () { + // Clear any existing "invalidate" timeout + if (this._invalidateTimeout) { + window.clearTimeout(this._invalidateTimeout); + } + if (!this.doInvalidate) { + return; + } + var self = this; + var _super = _super_1.prototype.invalidate.call(this); + this._invalidateTimeout = window.setTimeout(function () { + egw.debug("log", "Dataview grid timed invalidate"); + // Clear the "_avgHeight" + self._avgHeight = false; + self._avgCount = -1; + self._invalidateTimeout = null; + self._doInvalidate(_super); + }, et2_dataview_grid.ET2_GRID_INVALIDATE_TIMEOUT); + }; + /** + * Makes the given index visible: TODO: Propagate this to the parent grid. + */ + et2_dataview_grid.prototype.makeIndexVisible = function (_idx) { + // Get the element range + var elemRange = this._getElementRange(_idx); + // Abort if the index was out of range + if (!elemRange) { + return false; + } + // Calculate the current visible range + var visibleRange = et2_bounds(this._viewRange.top + et2_dataview_grid.ET2_GRID_VIEW_EXT, this._viewRange.bottom - et2_dataview_grid.ET2_GRID_VIEW_EXT); + // Check whether the element is currently completely visible -- if yes, + // do nothing + if (visibleRange.top < elemRange.top + && visibleRange.bottom > elemRange.bottom) { + return true; + } + if (elemRange.top < visibleRange.top) { + this.scrollarea.scrollTop(elemRange.top); + } + else { + var h = elemRange.bottom - elemRange.top; + this.scrollarea.scrollTop(elemRange.top - this._scrollHeight + h); + } + }; + /* ---- PRIVATE FUNCTIONS ---- */ + /* _inspectStructuralIntegrity: function() { + var idx = 0; + for (var i = 0; i < this._map.length; i++) + { + if (this._map[i].getIndex() != idx) + { + throw "Index missmatch!"; + } + idx += this._map[i].getCount(); + } + + if (idx !== this._total) + { + throw "Total count missmatch!"; + } + },*/ + /** + * Translates the given index to a range, returns false if the index is + * out of range. + */ + et2_dataview_grid.prototype._getElementRange = function (_idx) { + // Recalculate the element positions + this._recalculateElementPosition(); + // Translate the given index to the map index + var mapIdx = this._calculateMapIndex(_idx); + // Do nothing if the given index is out of range + if (mapIdx === false) { + return false; + } + // Get the map element + var elem = this._map[mapIdx]; + // Get the element range + if (elem instanceof et2_dataview_view_spacer_1.et2_dataview_spacer) { + var avg = this.getAverageHeight(); + return et2_range(elem.getTop() + avg * (elem.getIndex() - _idx), avg); + } + return elem.getRange(); + }; + /** + * Recalculates the position of the currently managed containers. This + * routine only updates the pixel position of the elements -- the index of + * the elements is guaranteed to be maintained correctly by all high level + * functions of the grid, as the index position is needed to be correct for + * the "deleteRow" and "insertRow" functions, and we cannot effort to call + * this calculation method after every change in the grid mapping. + */ + et2_dataview_grid.prototype._recalculateElementPosition = function () { + for (var i = 0; i < this._map.length; i++) { + if (i == 0) { + this._map[i].setTop(0); + } + else { + this._map[i].setTop(this._map[i - 1].getBottom()); + } + } + }; + /** + * The "_calculateVisibleMappingIndices" function calculates the indices of + * the _map array, which refer to containers that are currently (partially) + * visible. This function is used internally by "_doInvalidate". + */ + et2_dataview_grid.prototype._calculateVisibleMappingIndices = function () { + // First update the "top" and "bottom", and "index" values of all + // managed elements, and at the same time calculate the mapping indices + // of the elements which are inside the current view range. + var mapVis = { "top": -1, "bottom": -1 }; + for (var i = 0; i < this._map.length; i++) { + // Update the top of the "map visible index" -- set it to the first + // element index, where the bottom line is beneath the top line + // of the view range. + if (mapVis.top === -1 + && this._map[i].getBottom() > this._viewRange.top) { + mapVis.top = i; + } + // Update the bottom of the "map visible index" -- set it to the + // first element index, where the top line is beneath the bottom + // line of the view range. + if (mapVis.bottom === -1 + && this._map[i].getTop() > this._viewRange.bottom) { + mapVis.bottom = i; + break; + } + } + return mapVis; + }; + /** + * Deletes all elements which are "out of the view range". This function is + * internally used by "_doInvalidate". How many elements that are out of the + * view range get preserved fully depends on the _holdCount parameter + * variable. + * + * @param _mapVis contains the _map indices of the just visible containers. + * @param _holdCount contains the number of elements that should be kept, + * if not given, this parameter defaults to ET2_GRID_HOLD_COUNT + */ + et2_dataview_grid.prototype._cleanupOutOfRangeElements = function (_mapVis, _holdCount) { + // Iterates over the map from and to the given indices and pushes all + // elements onto the given array, which are more than _holdCount + // elements remote from the start. + function searchElements(_arr, _start, _stop, _dir) { + var dist = 0; + for (var i_1 = _start; _dir > 0 ? i_1 <= _stop : i_1 >= _stop; i_1 += _dir) { + if (dist > _holdCount) { + _arr.push(i_1); + } + else { + dist += this._map[i_1].getCount(); + } + } + } + // Set _holdCount to ET2_GRID_HOLD_COUNT if the parameters is not given + _holdCount = typeof _holdCount === "undefined" ? et2_dataview_grid.ET2_GRID_HOLD_COUNT : + _holdCount; + // Collect all elements that will be deleted at the top and at the + // bottom of the grid + var deleteTop = []; + var deleteBottom = []; + if (_mapVis.top !== -1) { + searchElements.call(this, deleteTop, _mapVis.top, 0, -1); + } + if (_mapVis.bottom !== -1) { + searchElements.call(this, deleteBottom, _mapVis.bottom, this._map.length - 1, 1); + } + // The offset variable specifies how many elements have been deleted + // from the map -- this variable is needed as deleting elements from the + // map shifts the map indices. We iterate in oposite direction over the + // elements, as this allows the _doDeleteContainer/ container function + // to extend the (possibly) existing spacer at the top of the grid + var offs = 0; + for (var i = deleteTop.length - 1; i >= 0; i--) { + // Delete the container and calculate the new offset + var mapLength = this._map.length; + this._doDeleteContainer(deleteTop[i] - offs, true); + offs += mapLength - this._map.length; + } + for (var i = deleteBottom.length - 1; i >= 0; i--) { + this._doDeleteContainer(deleteBottom[i] - offs, true); + } + return deleteBottom.length + deleteTop.length > 0; + }; + /** + * The _updateContainers function is used internally by "_doInvalidate" in + * order to call the "setViewRange" function of all containers the implement + * that interfaces (needed for nested grids), and to request new elements + * for all currently visible spacers. + */ + et2_dataview_grid.prototype._updateContainers = function () { + var _loop_1 = function (i) { + var container = this_1._map[i]; + // Check which type the container object has + var isSpacer = container instanceof et2_dataview_view_spacer_1.et2_dataview_spacer; + var hasIViewRange = !isSpacer + && implements_et2_dataview_IViewRange(container); + // If the container has one of those special types, calculate the + // view range and use that to update the view range of the element + // or to request new elements for the spacer + if (isSpacer || hasIViewRange) { + // Calculate the relative view range and check whether + // the element is really visible + var elemRange = container.getRange(); + // Abort if the element is not inside the visible range + if (!et2_rangeIntersect(this_1._viewRange, elemRange)) { + return "continue"; + } + if (hasIViewRange) { + // Update the view range of the container + container.setViewRange(et2_bounds(this_1._viewRange.top - elemRange.top, this_1._viewRange.bottom - elemRange.top)); + } + else // This container is a spacer + { + // Obtain the average element height + var avg = container._rowHeight; + // Get the visible container range (vcr) + var vcr_top = Math.max(this_1._viewRange.top, elemRange.top); + var vcr_bot = Math.min(this_1._viewRange.bottom, elemRange.bottom); + // Calculate the indices of the elements which will be + // requested + var cidx = container.getIndex(); + var ccnt = container.getCount(); + // Calculate the start index -- prevent vtop from getting + // negative (and so idxStart being smaller than cidx) and + // ensure that idxStart is not larger than the maximum + // container index. + var vtop = Math.max(0, vcr_top); + var idxStart_1 = Math.floor(Math.min(cidx + ccnt - 1, cidx + (vtop - elemRange.top) / avg, this_1._total)); + // Calculate the end index -- prevent vtop from getting + // negative (and so idxEnd being smaller than cidx) and + // ensure that idxEnd is not larger than the maximum + // container index. + var vbot = Math.max(0, vcr_bot); + var idxEnd_1 = Math.ceil(Math.min(cidx + ccnt - 1, cidx + (vbot - elemRange.top) / avg, this_1._total)); + // Initial resize while the grid is hidden will give NaN + // This is an important optimisation, as it is involved in not + // loading all rows, so we override in that case so + // there are more than the 2-3 that fit in the min height. + if (isNaN(idxStart_1) && isSpacer) + idxStart_1 = cidx - 1; + if (isNaN(idxEnd_1) && isSpacer && this_1._scrollHeight > 0 && elemRange.bottom == 0) { + idxEnd_1 = Math.min(ccnt, cidx + Math.ceil((this_1._viewRange.bottom - container._top) / (this_1._orgAvgHeight || 0))); + } + // Call the data callback + if (this_1._callback) { + var self_1 = this_1; + egw.debug("log", "Dataview grid flag for update: ", { start: idxStart_1, end: idxEnd_1 }); + window.setTimeout(function () { + // If row template changes, self._callback might disappear + if (typeof self_1._callback != "undefined") { + self_1._callback.call(self_1._context, idxStart_1, idxEnd_1); + } + }, 0); + } + } + } + }; + var this_1 = this; + for (var i = 0; i < this._map.length; i++) { + _loop_1(i); + } + }; + /** + * Invalidate iterates over the "mapping" array. It calculates which + * containers have to be removed and where new containers should be added. + */ + et2_dataview_grid.prototype._doInvalidate = function (_super) { + if (!this.doInvalidate) + return; + // Update the pixel positions + this._recalculateElementPosition(); + // Call the callback + if (this._invalidateCallback) { + var range = this.getVisibleIndexRange(et2_range(this.scrollarea.scrollTop(), this._scrollHeight)); + this._invalidateCallback.call(this._invalidateContext, range); + } + // Get the visible mapping indices and recalculate index and pixel + // position of the containers. + var mapVis = this._calculateVisibleMappingIndices(); + // Delete all invisible elements -- if anything changed, we have to + // recalculate the pixel positions again + if (this._cleanupOutOfRangeElements(mapVis)) { + this._recalculateElementPosition(); + } + // Update the view range of all visible elements that implement the + // corresponding interface and request elements for all visible spacers + this._updateContainers(); + // Call the inherited invalidate function, broadcast the invalidation + // through the container tree. + if (this._parent && _super) { + _super._doInvalidate(); + } + }; + /** + * Translates the given grid index into the element index of the map. If the + * given index is completely out of the range, "false" is returned. + */ + et2_dataview_grid.prototype._calculateMapIndex = function (_index) { + var top = 0; + var bot = this._map.length - 1; + while (top <= bot) { + var idx = Math.floor((top + bot) / 2); + var elem = this._map[idx]; + var realIdx = elem.getIndex(); + var realCnt = elem.getCount(); + if (_index >= realIdx && _index < realIdx + realCnt) { + return idx; + } + else if (_index < realIdx) { + bot = idx - 1; + } + else { + top = idx + 1; + } + } + return false; + }; + et2_dataview_grid.prototype._insertContainerAtSpacer = function (_index, _mapIndex, _mapElem, _container, _avg) { + // Set the index of the new container + _container.setIndex(_index); + // Calculate at which position the spacer has to be splitted + var splitIdx = _index - _mapElem.getIndex(); + // Get the count of elements that remain at the top of the splitter + var cntTop = splitIdx; + // Get the count of elements that remain at the bottom of the splitter + // -- it has to be one element less than before + var cntBottom = _mapElem.getCount() - splitIdx - 1; + // Split the containers if cntTop and cntBottom are larger than zero + if (cntTop > 0 && cntBottom > 0) { + // Set the new count of the currently existing container, preserving + // its height as it was + _mapElem.setCount(cntTop); + // Add the new element after the old container + _container.insertIntoTree(_mapElem.getLastNode()); + // Create a new spacer and add it after the newly inserted container + var newSpacer = new et2_dataview_view_spacer_1.et2_dataview_spacer(this, this._rowProvider); + newSpacer.setCount(cntBottom, _avg); + newSpacer.setIndex(_index + 1); + newSpacer.insertIntoTree(_container.getLastNode()); + // Insert the container and the new spacer into the map + this._map.splice(_mapIndex + 1, 0, _container, newSpacer); + } + else if (cntTop === 0 && cntBottom > 0) { + // Simply adjust the size of the old spacer and insert the new + // container in front of it + _container.insertIntoTree(_mapElem.getFirstNode(), true); + _mapElem.setIndex(_index + 1); + _mapElem.setCount(cntBottom, _avg); + this._map.splice(_mapIndex, 0, _container); + } + else if (cntTop > 0 && cntBottom === 0) { + // Simply add the new container to the end of the old container and + // adjust the count of the old spacer to the remaining count. + _container.insertIntoTree(_mapElem.getLastNode()); + _mapElem.setCount(cntTop); + this._map.splice(_mapIndex + 1, 0, _container); + } + else // if (cntTop === 0 && cntBottom === 0) + { + // Append the new container to the current container and then + // destroy the old container + _container.insertIntoTree(_mapElem.getLastNode()); + _mapElem.destroy(); + this._map.splice(_mapIndex, 1, _container); + } + }; + et2_dataview_grid.prototype._insertContainerAtElement = function (_index, _mapIndex, _mapElem, _container, _avg) { + // In a first step, simply insert the element at the specified position, + // in front of the element _mapElem. + _container.setIndex(_index); + _container.insertIntoTree(_mapElem.getFirstNode(), true); + this._map.splice(_mapIndex, 0, _container); + // Search for the next spacer and increment the indices of all other + // elements until there + var _newIndex = _index + 1; + for (var i = _mapIndex + 1; i < this._map.length; i++) { + // Update the index of the element + this._map[i].setIndex(_newIndex++); + // We've found a spacer -- decrement its element count and abort + if (this._map[i] instanceof et2_dataview_view_spacer_1.et2_dataview_spacer) { + this._decrementSpacerCount(i, _avg); + return; + } + } + // We've found no spacer so far, remove the last element from the map in + // order to obtain the "totalCount" (especially the last element is no + // spacer, so the following code cannot remove a spacer) + this._map.pop().destroy(); + }; + /** + * Inserts the given container at the given index. + */ + et2_dataview_grid.prototype._doInsertContainer = function (_index, _mapIndex, _container, _avg) { + // Check whether the given element at the map index is a spacer. If + // yes, we have to split the spacer at that position. + var mapElem = this._map[_mapIndex]; + if (mapElem instanceof et2_dataview_view_spacer_1.et2_dataview_spacer) { + this._insertContainerAtSpacer(_index, _mapIndex, mapElem, _container, _avg); + } + else { + this._insertContainerAtElement(_index, _mapIndex, mapElem, _container, _avg); + } + }; + /** + * Replaces the container at the given index with a spacer. The function + * tries to extend any spacer lying above or below the given _mapIndex. + * This code does not destroy the given container, but maintains its map + * index. + * + * @param _mapIndex is the index of _mapElem in the _map array. + * @param _mapElem is the container which should be replaced. + */ + et2_dataview_grid.prototype._replaceContainerWithSpacer = function (_mapIndex, _mapElem) { + var newAvg; + var spacer; + var totalHeight; + var totalCount; + // Check whether a spacer can be extended above or below the given + // _mapIndex + var spacerAbove = null; + var spacerBelow = null; + if (_mapIndex > 0 + && this._map[_mapIndex - 1] instanceof et2_dataview_view_spacer_1.et2_dataview_spacer) { + spacerAbove = this._map[_mapIndex - 1]; + } + if (_mapIndex < this._map.length - 1 + && this._map[_mapIndex + 1] instanceof et2_dataview_view_spacer_1.et2_dataview_spacer) { + spacerBelow = this._map[_mapIndex + 1]; + } + if (!spacerAbove && !spacerBelow) { + // No spacer can be extended -- simply create a new one + spacer = new et2_dataview_view_spacer_1.et2_dataview_spacer(this, this._rowProvider); + spacer.setIndex(_mapElem.getIndex()); + spacer.addAvgHeight(_mapElem.getHeight()); + spacer.setCount(1, _mapElem.getHeight()); + // Insert the new spacer at the correct place into the DOM tree and + // the mapping + spacer.insertIntoTree(_mapElem.getLastNode()); + this._map.splice(_mapIndex + 1, 0, spacer); + } + else if (spacerAbove && spacerBelow) { + // We're going to consolidate the upper and the lower spacer. To do + // that we'll calculate a new count of elements and a new average + // height, so that the upper container can get the height of all + // three elements together + totalHeight = spacerAbove.getHeight() + spacerBelow.getHeight() + + _mapElem.getHeight(); + totalCount = spacerAbove.getCount() + spacerBelow.getCount() + + 1; + newAvg = totalHeight / totalCount; + // Update the upper spacer + spacerAbove.addAvgHeight(_mapElem.getHeight()); + spacerAbove.setCount(totalCount, newAvg); + // Delete the lower spacer and remove it from the mapping + spacerBelow.destroy(); + this._map.splice(_mapIndex + 1, 1); + } + else { + // One of the two spacers is available + spacer = spacerAbove || spacerBelow; + // Calculate the new count and the new average height of that spacer + totalCount = spacer.getCount() + 1; + totalHeight = spacer.getHeight() + _mapElem.getHeight(); + newAvg = totalHeight / totalCount; + // Set the new container height + spacer.setIndex(Math.min(spacer.getIndex(), _mapElem.getIndex())); + spacer.addAvgHeight(_mapElem.getHeight()); + spacer.setCount(totalCount, newAvg); + } + }; + /** + * Checks whether there is another spacer below the given map index and if + * yes, consolidates the two. + */ + et2_dataview_grid.prototype._consolidateSpacers = function (_mapIndex) { + if (_mapIndex < this._map.length - 1 + && this._map[_mapIndex] instanceof et2_dataview_view_spacer_1.et2_dataview_spacer + && this._map[_mapIndex + 1] instanceof et2_dataview_view_spacer_1.et2_dataview_spacer) { + var spacerAbove = this._map[_mapIndex]; + var spacerBelow = this._map[_mapIndex + 1]; + // Calculate the new height/count of both containers + var totalHeight = spacerAbove.getHeight() + spacerBelow.getHeight(); + var totalCount = spacerAbove.getCount() + spacerBelow.getCount(); + var newAvg = totalCount / totalHeight; + // Extend the new spacer + spacerAbove.setCount(totalCount, newAvg); + // Delete the old spacer + spacerBelow.destroy(); + this._map.splice(_mapIndex + 1, 1); + } + }; + /** + * Decrements the count of the spacer at the given _mapIndex by one. If the + * given spacer has no more elements, it will be removed from the mapping. + * Note that this function does not update the indices of the following + * elements, this function is only used internally by the + * _insertContainerAtElement function and the _doDeleteContainer function + * where appropriate adjustments to the map data structure are done. + * + * @param _mapIndex is the index of the spacer in the "map" data structure. + * @param _avg is the new average height of the container, may be + * "undefined" in which case the height of the spacer rows is kept as it + * was. + */ + et2_dataview_grid.prototype._decrementSpacerCount = function (_mapIndex, _avg) { + var cnt = this._map[_mapIndex].getCount() - 1; + if (cnt > 0) { + this._map[_mapIndex].setCount(cnt, _avg); + } + else { + this._map[_mapIndex].destroy(); + this._map.splice(_mapIndex, 1); + } + }; + /** + * Deletes the container at the given index. + */ + et2_dataview_grid.prototype._doDeleteContainer = function (_mapIndex, _replaceWithSpacer) { + // _replaceWithSpacer defaults to false + _replaceWithSpacer = _replaceWithSpacer ? _replaceWithSpacer : false; + // Fetch the element at the given map index + var mapElem = this._map[_mapIndex]; + // Indicates whether an element has really been removed -- if yes, the + // bottom spacer will be extended + var removedElement = false; + // Check whether the map element is a spacer -- if yes, we have to do + // some special treatment + if (mapElem instanceof et2_dataview_view_spacer_1.et2_dataview_spacer) { + // Do nothing if the "_replaceWithSpacer" flag is true as the + // element already is a spacer + if (!_replaceWithSpacer) { + this._decrementSpacerCount(_mapIndex); + removedElement = true; + } + } + else { + if (_replaceWithSpacer) { + this._replaceContainerWithSpacer(_mapIndex, mapElem); + } + else { + removedElement = true; + } + // Remove the complete (current) container, decrement the _mapIndex + this._map[_mapIndex].destroy(); + this._map.splice(_mapIndex, 1); + _mapIndex--; + // The delete operation may have created two joining spacers -- this + // is highly suboptimal, so we'll consolidate those two spacers + this._consolidateSpacers(_mapIndex); + } + // Update the indices of all elements after the current one, if we've + // really removed an element + if (removedElement) { + for (var i = _mapIndex + 1; i < this._map.length; i++) { + this._map[i].setIndex(this._map[i].getIndex() - 1); + } + // Extend the last spacer as we have to maintain the spacer count + this._appendEmptyRows(1); + } + }; + /** + * The appendEmptyRows function is used internally to append empty rows to + * the end of the table. This functionality is needed in order to maintain + * the "total count" in the _doDeleteContainer function and to increase the + * "total count" in the "setCount" function. + * + * @param _count specifies the count of empty rows that will be added to the + * end of the table. + */ + et2_dataview_grid.prototype._appendEmptyRows = function (_count) { + // Special case -- the last element in the "_map" is no spacer -- this + // means, that the "managedRange" is currently at the bottom of the list + // -- so we have to insert a new spacer + var spacer = null; + var lastIndex = this._map.length - 1; + if (this._map.length === 0 || + !(this._map[lastIndex] instanceof et2_dataview_view_spacer_1.et2_dataview_spacer)) { + // Create a new spacer + spacer = new et2_dataview_view_spacer_1.et2_dataview_spacer(this, this._rowProvider); + // Insert the spacer -- we have a special case if there currently is + // no element inside the mapping + if (this._map.length === 0) { + // Add a dummy element to the grid + var dummy = jQuery(document.createElement("tr")); + this.innerTbody.append(dummy); + // Append the spacer to the grid + spacer.setIndex(0); + spacer.insertIntoTree(dummy, false); + // Remove the dummy element + dummy.remove(); + } + else { + // Insert the new spacer after the last element + spacer.setIndex(this._map[lastIndex].getIndex() + 1); + spacer.insertIntoTree(this._map[lastIndex].getLastNode()); + } + // Add the new spacer to the mapping + this._map.push(spacer); + } + else { + // Get the spacer at the bottom of the mapping + spacer = this._map[lastIndex]; + } + // Update the spacer count + spacer.setCount(_count + spacer.getCount(), this.getAverageHeight()); + }; + /** + * The _decreaseTotal function is used to decrease the total row count in + * the grid. It tries to remove the given count of rows from the spacer + * located at the bottom of the grid, if this is not possible, it starts + * removing complete rows. + * + * @param _delta specifies how many rows should be removed. + */ + et2_dataview_grid.prototype._decreaseTotal = function (_delta) { + // Iterate over the current mapping, starting at the bottom and delete + // rows. _delta is decreased for each removed row. Abort when delta is + // zero or the map is empty + while (_delta > 0 && this._map.length > 0) { + var cont = this._map[this._map.length - 1]; + // Remove as many containers as possible from spacers + if (cont instanceof et2_dataview_view_spacer_1.et2_dataview_spacer) { + var diff = cont.getCount() - _delta; + if (diff > 0) { + // We're done as the spacer still has entries left + _delta = 0; + cont.setCount(diff, this.getAverageHeight()); + break; + } + else { + // Decrease _delta by the count of rows the spacer had + _delta -= diff + _delta; + } + } + else { + // We're going to remove a single row: remove it + _delta -= 1; + } + // Destroy the container if there are no rows left + cont.destroy(); + this._map.pop(); + } + // Check whether _delta is really zero + if (_delta > 0) { + this.egw.debug('error', "Error while decreasing the total count - requested to remove more rows than available."); + } + }; + /** + * Creates the grid DOM-Nodes + */ + et2_dataview_grid.prototype._createNodes = function () { + this.tr = jQuery(document.createElement("tr")); + this.outerCell = jQuery(document.createElement("td")) + .addClass("frame") + .attr("colspan", this._rowProvider.getColumnCount() + + (this._parentGrid ? 0 : 1)) + .appendTo(this.tr); + // Create the scrollarea div if this is the outer grid + this.scrollarea = null; + if (this._parentGrid == null) { + this.scrollarea = jQuery(document.createElement("div")) + .addClass("egwGridView_scrollarea") + .scroll(this, function (e) { + // Clear any older scroll timeout + if (e.data._scrollTimeout) { + window.clearTimeout(e.data._scrollTimeout); + } + // Clear any existing "invalidate" timeout (as the + // "setViewRange" function triggered by the scroll event + // forces an "invalidate"). + if (e.data._invalidateTimeout) { + window.clearTimeout(e.data._invalidateTimeout); + e.data._invalidateTimeout = null; + } + // Set a new timeout which calls the setViewArea + // function + e.data._scrollTimeout = window.setTimeout(jQuery.proxy(function () { + var newRange = et2_range(this.scrollarea.scrollTop() - et2_dataview_grid.ET2_GRID_VIEW_EXT, this._scrollHeight + et2_dataview_grid.ET2_GRID_VIEW_EXT * 2); + if (!et2_rangeEqual(newRange, this._viewRange)) { + this.setViewRange(newRange); + } + }, e.data), et2_dataview_grid.ET2_GRID_SCROLL_TIMEOUT); + }) + .height(this._scrollHeight) + .appendTo(this.outerCell); + } + // Create the inner table + var table = jQuery(document.createElement("table")) + .addClass("egwGridView_grid") + .appendTo(this.scrollarea ? this.scrollarea : this.outerCell); + this.innerTbody = jQuery(document.createElement("tbody")) + .appendTo(table); + // Set the tr as container element + this.appendNode(jQuery(this.tr[0])); + }; + /** + * Determines how many pixels the view range of the gridview is extended inside + * the scroll callback. + */ + et2_dataview_grid.ET2_GRID_VIEW_EXT = 50; + /** + * Determines the timeout after which the scroll-event is processed. + */ + et2_dataview_grid.ET2_GRID_SCROLL_TIMEOUT = 50; + /** + * Determines the timeout after which the invalidate-request gets processed. + */ + et2_dataview_grid.ET2_GRID_INVALIDATE_TIMEOUT = 25; + /** + * Determines how many elements are kept displayed outside of the current view + * range until they get removed. + */ + et2_dataview_grid.ET2_GRID_HOLD_COUNT = 50; + return et2_dataview_grid; +}(et2_dataview_view_container_1.et2_dataview_container)); +exports.et2_dataview_grid = et2_dataview_grid; +//# sourceMappingURL=et2_dataview_view_grid.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_dataview_view_grid.ts b/api/js/etemplate/et2_dataview_view_grid.ts new file mode 100644 index 0000000000..efb22c6149 --- /dev/null +++ b/api/js/etemplate/et2_dataview_view_grid.ts @@ -0,0 +1,1457 @@ +/** + * EGroupware eTemplate2 - Class which contains the "grid" base class + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage dataview + * @link http://www.egroupware.org + * @author Andreas Stöckel + * @copyright Stylite 2011 + * @version $Id$ + * + +/*egw:uses + /vendor/bower-asset/jquery/dist/jquery.js; + et2_core_common; + + et2_dataview_interfaces; + et2_dataview_view_container; + et2_dataview_view_spacer; +*/ + +import {et2_dataview_IViewRange} from "./et2_dataview_interfaces"; +import {et2_dataview_container} from "./et2_dataview_view_container"; +import {et2_dataview_spacer} from "./et2_dataview_view_spacer"; +import {et2_dataview_rowProvider} from "./et2_dataview_view_rowProvider"; + +export class et2_dataview_grid extends et2_dataview_container implements et2_dataview_IViewRange +{ + /** + * Determines how many pixels the view range of the gridview is extended inside + * the scroll callback. + */ + public static readonly ET2_GRID_VIEW_EXT = 50; + + /** + * Determines the timeout after which the scroll-event is processed. + */ + public static readonly ET2_GRID_SCROLL_TIMEOUT = 50; + + /** + * Determines the timeout after which the invalidate-request gets processed. + */ + public static readonly ET2_GRID_INVALIDATE_TIMEOUT = 25; + + /** + * Determines how many elements are kept displayed outside of the current view + * range until they get removed. + */ + public static readonly ET2_GRID_HOLD_COUNT = 50; + + + + egw: any; + + private _orgAvgHeight: number | boolean; + private _rowProvider: et2_dataview_rowProvider; + private _scrollHeight: number; + private _scrollTimeout: null; + + private _parentGrid: any; + + private _callback: Function; + private _context: object; + + private _invalidateTimeout: number; + private _invalidateCallback: Function; + private _invalidateContext: null; + private doInvalidate : boolean; + + private _map: any[]; + private _viewRange: { top: any; bottom: any }; + private _total: number; + private _avgHeight: number | boolean; + private _avgCount: number; + + private scrollarea: any; + private innerTbody: any; + private outerCell: JQuery; + + + /** + * Creates the grid. + * + * @param _parent is the parent grid class - if null, this means that this + * is the outer grid which manages the scrollarea. If not null, all other + * parameters are ignored and copied from the given grid instance. + * @param _parentGrid + * @param _egw + * @param _rowProvider + * @param _avgHeight is the starting average height of the column rows. + * @memberOf et2_dataview_grid + */ + constructor (_parent, _parentGrid, _egw, _rowProvider, _avgHeight : number) + { + // Call the inherited constructor + super(_parent); + + // If the parent is given, copy all other parameters from it + if (_parentGrid != null) + { + this.egw = _parent.egw; + this._orgAvgHeight = false; + this._rowProvider = _parentGrid._rowProvider; + } + else + { + // Otherwise copy the given parameters + this.egw = _egw; + this._orgAvgHeight = _avgHeight; + this._rowProvider = _rowProvider; + + // As this grid instance has no parent, we need a scroll container + this._scrollHeight = 0; + this._scrollTimeout = null; + } + + this._parentGrid = _parentGrid; + + this._scrollTimeout = null; + + this._invalidateTimeout = null; + + this._invalidateCallback = null; + this._invalidateContext = null; + + // Flag for stopping invalidate while working + this.doInvalidate = true; + + // _map contains a mapping between the grid indices and the elements + // associated to it. The first element in the array always refers to the + // element starting at index zero (being a spacer if the grid currently + // displays another range). + this._map = []; + + // _viewRange contains the current pixel-range of the grid which is + // visible. + this._viewRange = et2_range(0, 0); + + // Holds the maximum count of elements + this._total = 0; + + // Holds data used for storing the current average height data + this._avgHeight = false; + this._avgCount = -1; + + // Build the outer grid nodes + this._createNodes(); + } + + destroy () + { + // Destroy all containers + this.setTotalCount(0); + + // Stop the scroll timeout + if (this._scrollTimeout) + { + window.clearTimeout(this._scrollTimeout); + } + + // Stop the invalidate timeout + if (this._invalidateTimeout) + { + window.clearTimeout(this._invalidateTimeout); + } + + super.destroy(); + } + + clear () + { + // Store the old total count and rescue the current average height in + // form of the "original average height" + const oldTotalCount = this._total; + this._orgAvgHeight = this.getAverageHeight(); + + // Set the total count to zero + this.setTotalCount(0); + + // Reset the total count value + this.setTotalCount(oldTotalCount); + } + + /** + * Throws all elements away which are outside the current view range + */ + cleanup () + { + // Update the pixel positions + this._recalculateElementPosition(); + + // Get the visible mapping indices and recalculate index and pixel + // position of the containers. + const mapVis = this._calculateVisibleMappingIndices(); + + // Delete all invisible elements -- if anything changed, we have to + // recalculate the pixel positions again + this._cleanupOutOfRangeElements(mapVis, 0); + } + + /** + * The insertRow function can be called to insert the given container(s) at + * the given row index. If there currently is another container at that + * given position, the new container(s) will be inserted above the old + * container. Yet the "total count" of the grid will be preserved by + * removing the correct count of elements from the next possible spacer. If + * no spacer is found, the last containers will be removed. This causes + * inserting new containers at the end of a grid to be immediately removed + * again. + * + * @param _index is the row index at which the given container(s) should be + * inserted. + * @param _container is eiter a single et2_dataview_container instance + * which should be inserted at the given position. Or an array of + * et2_dataview_container instances. If you want to remove the container + * don't do that manually by calling its "destroy" function but use the + * deleteRow function. + */ + insertRow (_index, _container) + { + // Calculate the map element the given index refers to + const idx = this._calculateMapIndex(_index); + + if (idx !== false) + { + // Wrap the container inside an array + if (_container instanceof et2_dataview_container) + { + _container = [_container]; + } + + // Fetch the average height + const avg = this.getAverageHeight(); + + // Call the internal _doInsertContainer function + for (let i = 0; i < _container.length; i++) + { + this._doInsertContainer(_index, idx, _container[i], avg); + } + + // Schedule an "invalidate" event + this.invalidate(); + } + } + + /** + * The deleteRow function can be used to remove the element at the given + * index. + * + * @param _index is the index from which should be deleted. If the given + * index is outside the so called "managedRange" nothing will happen, as the + * container has already been destroyed by the grid instance. + */ + deleteRow (_index) + { + // Calculate the map element the given index refers to + const idx = this._calculateMapIndex(_index); + + if (idx !== false) + { + this._doDeleteContainer(idx, false); + + // Schedule an "invalidate" event + this.invalidate(); + } + } + + /** + * The given callback gets called whenever the scroll position changed or + * the visible element range changed. The element indices are passed to the + * function as et2_range. + */ + setInvalidateCallback (_callback, _context) + { + this._invalidateCallback = _callback; + this._invalidateContext = _context; + } + + /** + * The setDataCallback function is used to set the callback that will be + * called when the grid requires new data. + * + * @param _callback is the callback function which gets called when the grid + * needs some new rows. + * @param _context is the context in which the callback function gets + * called. + */ + setDataCallback (_callback : Function, _context : object) + { + this._callback = _callback; + this._context = _context; + } + + /** + * The updateTotalCount function can be used to update the total count of + * rows that are displayed inside the grid. Changing the count always causes + * the spacer at the bottom (if it exists) to be + * + * @param _count specifies how many entries the grid can show. + */ + setTotalCount (_count : number) + { + // Abort if the total count has not changed + if (_count === this._total) + return; + + // Calculate how many elements have to be removed/added + const delta = Math.max(0, _count) - this._total; + + if (delta > 0) + { + this._appendEmptyRows(delta); + } + else + { + this._decreaseTotal(-delta); + } + + this._total = Math.max(0, _count); + + // Schedule an invalidate + this.invalidate(); + } + + /** + * Returns the current "total" count. + */ + getTotalCount() : number + { + return this._total; + } + + /** + * The setViewRange function updates the range in which rows are shown. + */ + setViewRange (_range) + { + // Set the new view range + this._viewRange = _range; + + // Immediately call the "invalidate" function + this._doInvalidate(); + } + + /** + * Return the indices of the currently visible rows. + */ + getVisibleIndexRange (_viewRange) + { + + function getElemIdx(_elem, _px) + { + if (_elem instanceof et2_dataview_spacer) + { + return _elem.getIndex() + + Math.floor((_px - _elem.getTop()) / this.getAverageHeight()); + } + + return _elem.getIndex(); + } + + let idxTop = 0; + let idxBottom = 0; + let vr; + + if (_viewRange) + { + vr = _viewRange; + } + else + { + // Calculate the "correct" view range by removing ET2_GRID_VIEW_EXT + vr = et2_bounds( + this._viewRange.top + et2_dataview_grid.ET2_GRID_VIEW_EXT, + this._viewRange.bottom - et2_dataview_grid.ET2_GRID_VIEW_EXT); + } + + // Get the elements at the top and the bottom of the view + let topElem = null; + let botElem = null; + for (let i = 0; i < this._map.length; i++) + { + if (!topElem && this._map[i].getBottom() > vr.top) + { + topElem = this._map[i]; + } + + if (this._map[i].getTop() > vr.bottom) + { + botElem = this._map[i]; + break; + } + } + + if (!botElem) + { + botElem = this._map[this._map.length - 1]; + } + + if (topElem) + { + idxTop = getElemIdx.call(this, topElem, vr.top); + idxBottom = getElemIdx.call(this, botElem, vr.bottom); + } + + // Return the calculated index top and bottom + return et2_bounds(idxTop, idxBottom); + } + + /** + * Returns index range of all currently managed rows. + */ + getIndexRange () + { + let idxTop = false; + let idxBottom = false; + + for (let i = 0; i < this._map.length; i++) + { + if (!(this._map[i] instanceof et2_dataview_spacer)) + { + const idx = this._map[i].getIndex(); + + if (idxTop === false) + { + idxTop = idx; + } + + idxBottom = idx; + } + } + + return et2_bounds(idxTop, idxBottom); + } + + /** + * Updates the scrollheight + */ + setScrollHeight (_height) + { + this._scrollHeight = _height; + + // Update the height of the outer container + if (this.scrollarea) + { + this.scrollarea.height(_height); + } + + // Update the viewing range + this.setViewRange(et2_range(this._viewRange.top, this._scrollHeight)); + } + + /** + * Returns the average row height data, overrides the corresponding function + * of the et2_dataview_container. + */ + getAvgHeightData () + { + + if (this._avgHeight === false) + { + let avgCount = 0; + let avgSum = 0; + + for (let i = 0; i < this._map.length; i++) + { + const data = this._map[i].getAvgHeightData(); + + if (data !== null) + { + avgSum += data.avgHeight * data.avgCount; + avgCount += data.avgCount; + } + } + + // Calculate the average height, but only if we have a height + if (avgCount > 0 && avgSum > 0) + { + this._avgHeight = avgSum / avgCount; + this._avgCount = avgCount; + } + } + + // Return the calculated average height if it is available + if (this._avgHeight !== false) + { + return { + "avgCount": this._avgCount, + "avgHeight": this._avgHeight + }; + } + + // Otherwise return the parent average height + if (this._parent) + { + return this._parent.getAvgHeightData(); + } + + // Otherwise return the original average height given in the constructor + if (this._orgAvgHeight !== false) + { + return { + "avgCount": 1, + "avgHeight": this._orgAvgHeight + }; + } + return null; + } + + /** + * Returns the average row height in pixels. + */ + getAverageHeight () + { + const data = this.getAvgHeightData(); + return data ? data.avgHeight : 19; + } + + /** + * Returns the row provider. + */ + getRowProvider () + { + return this._rowProvider; + } + + /** + * Called whenever the size of this or another element in the container tree + * changes. + */ + invalidate() + { + + // Clear any existing "invalidate" timeout + if (this._invalidateTimeout) + { + window.clearTimeout(this._invalidateTimeout); + } + + if(!this.doInvalidate) + { + return; + } + + const self = this; + const _super = super.invalidate(); + this._invalidateTimeout = window.setTimeout(function() { + egw.debug("log","Dataview grid timed invalidate"); + // Clear the "_avgHeight" + self._avgHeight = false; + self._avgCount = -1; + self._invalidateTimeout = null; + self._doInvalidate(_super); + }, et2_dataview_grid.ET2_GRID_INVALIDATE_TIMEOUT); + } + + /** + * Makes the given index visible: TODO: Propagate this to the parent grid. + */ + makeIndexVisible(_idx) + { + // Get the element range + const elemRange = this._getElementRange(_idx); + + // Abort if the index was out of range + if (!elemRange) + { + return false; + } + + // Calculate the current visible range + const visibleRange = et2_bounds( + this._viewRange.top + et2_dataview_grid.ET2_GRID_VIEW_EXT, + this._viewRange.bottom - et2_dataview_grid.ET2_GRID_VIEW_EXT + ); + + // Check whether the element is currently completely visible -- if yes, + // do nothing + if (visibleRange.top < elemRange.top + && visibleRange.bottom > elemRange.bottom) + { + return true; + } + + if (elemRange.top < visibleRange.top) + { + this.scrollarea.scrollTop(elemRange.top); + } + else + { + const h = elemRange.bottom - elemRange.top; + this.scrollarea.scrollTop(elemRange.top - this._scrollHeight + h); + } + + } + + + /* ---- PRIVATE FUNCTIONS ---- */ + +/* _inspectStructuralIntegrity: function() { + var idx = 0; + for (var i = 0; i < this._map.length; i++) + { + if (this._map[i].getIndex() != idx) + { + throw "Index missmatch!"; + } + idx += this._map[i].getCount(); + } + + if (idx !== this._total) + { + throw "Total count missmatch!"; + } + },*/ + + /** + * Translates the given index to a range, returns false if the index is + * out of range. + */ + _getElementRange( _idx : number) + { + // Recalculate the element positions + this._recalculateElementPosition(); + + // Translate the given index to the map index + const mapIdx = this._calculateMapIndex(_idx); + + // Do nothing if the given index is out of range + if (mapIdx === false) + { + return false; + } + + // Get the map element + const elem = this._map[mapIdx]; + + // Get the element range + if (elem instanceof et2_dataview_spacer) + { + const avg = this.getAverageHeight(); + return et2_range(elem.getTop() + avg * (elem.getIndex() - _idx), + avg); + } + + return elem.getRange(); + } + + /** + * Recalculates the position of the currently managed containers. This + * routine only updates the pixel position of the elements -- the index of + * the elements is guaranteed to be maintained correctly by all high level + * functions of the grid, as the index position is needed to be correct for + * the "deleteRow" and "insertRow" functions, and we cannot effort to call + * this calculation method after every change in the grid mapping. + */ + _recalculateElementPosition() + { + for (let i = 0; i < this._map.length; i++) + { + if (i == 0) + { + this._map[i].setTop(0); + } + else + { + this._map[i].setTop(this._map[i - 1].getBottom()); + } + } + } + + /** + * The "_calculateVisibleMappingIndices" function calculates the indices of + * the _map array, which refer to containers that are currently (partially) + * visible. This function is used internally by "_doInvalidate". + */ + _calculateVisibleMappingIndices() : {top: number, bottom: number} + { + // First update the "top" and "bottom", and "index" values of all + // managed elements, and at the same time calculate the mapping indices + // of the elements which are inside the current view range. + const mapVis = {"top": -1, "bottom": -1}; + + for (let i = 0; i < this._map.length; i++) + { + // Update the top of the "map visible index" -- set it to the first + // element index, where the bottom line is beneath the top line + // of the view range. + if (mapVis.top === -1 + && this._map[i].getBottom() > this._viewRange.top) + { + mapVis.top = i; + } + + // Update the bottom of the "map visible index" -- set it to the + // first element index, where the top line is beneath the bottom + // line of the view range. + if (mapVis.bottom === -1 + && this._map[i].getTop() > this._viewRange.bottom) + { + mapVis.bottom = i; + break; + } + } + + return mapVis; + } + + /** + * Deletes all elements which are "out of the view range". This function is + * internally used by "_doInvalidate". How many elements that are out of the + * view range get preserved fully depends on the _holdCount parameter + * variable. + * + * @param _mapVis contains the _map indices of the just visible containers. + * @param _holdCount contains the number of elements that should be kept, + * if not given, this parameter defaults to ET2_GRID_HOLD_COUNT + */ + _cleanupOutOfRangeElements( _mapVis : {top : number, bottom: number}, _holdCount? : number) + { + + // Iterates over the map from and to the given indices and pushes all + // elements onto the given array, which are more than _holdCount + // elements remote from the start. + function searchElements(_arr, _start, _stop, _dir) + { + let dist = 0; + for (let i = _start; _dir > 0 ? i <= _stop : i >= _stop; i += _dir) + { + if (dist > _holdCount) + { + _arr.push(i); + } + else + { + dist += this._map[i].getCount(); + } + } + } + + // Set _holdCount to ET2_GRID_HOLD_COUNT if the parameters is not given + _holdCount = typeof _holdCount === "undefined" ? et2_dataview_grid.ET2_GRID_HOLD_COUNT : + _holdCount; + + // Collect all elements that will be deleted at the top and at the + // bottom of the grid + const deleteTop = []; + const deleteBottom = []; + + if (_mapVis.top !== -1) + { + searchElements.call(this, deleteTop, _mapVis.top, 0, -1); + } + + if (_mapVis.bottom !== -1) + { + searchElements.call(this, deleteBottom, _mapVis.bottom, + this._map.length - 1, 1); + } + + // The offset variable specifies how many elements have been deleted + // from the map -- this variable is needed as deleting elements from the + // map shifts the map indices. We iterate in oposite direction over the + // elements, as this allows the _doDeleteContainer/ container function + // to extend the (possibly) existing spacer at the top of the grid + let offs = 0; + for (var i = deleteTop.length - 1; i >= 0; i--) + { + // Delete the container and calculate the new offset + const mapLength = this._map.length; + this._doDeleteContainer(deleteTop[i] - offs, true); + offs += mapLength - this._map.length; + } + + for (var i = deleteBottom.length - 1; i >= 0; i--) + { + this._doDeleteContainer(deleteBottom[i] - offs, true); + } + + return deleteBottom.length + deleteTop.length > 0; + } + + /** + * The _updateContainers function is used internally by "_doInvalidate" in + * order to call the "setViewRange" function of all containers the implement + * that interfaces (needed for nested grids), and to request new elements + * for all currently visible spacers. + */ + _updateContainers() + { + for (let i = 0; i < this._map.length; i++) + { + const container = this._map[i]; + + // Check which type the container object has + const isSpacer = container instanceof et2_dataview_spacer; + const hasIViewRange = !isSpacer + && implements_et2_dataview_IViewRange(container); + + // If the container has one of those special types, calculate the + // view range and use that to update the view range of the element + // or to request new elements for the spacer + if (isSpacer || hasIViewRange) + { + // Calculate the relative view range and check whether + // the element is really visible + const elemRange = container.getRange(); + + // Abort if the element is not inside the visible range + if (!et2_rangeIntersect(this._viewRange, elemRange)) + { + continue; + } + + if (hasIViewRange) + { + // Update the view range of the container + container.setViewRange(et2_bounds( + this._viewRange.top - elemRange.top, + this._viewRange.bottom - elemRange.top)); + } + else // This container is a spacer + { + // Obtain the average element height + const avg = container._rowHeight; + + // Get the visible container range (vcr) + const vcr_top = Math.max(this._viewRange.top, elemRange.top); + const vcr_bot = Math.min(this._viewRange.bottom, elemRange.bottom); + + // Calculate the indices of the elements which will be + // requested + const cidx = container.getIndex(); + const ccnt = container.getCount(); + + // Calculate the start index -- prevent vtop from getting + // negative (and so idxStart being smaller than cidx) and + // ensure that idxStart is not larger than the maximum + // container index. + const vtop = Math.max(0, vcr_top); + let idxStart = Math.floor( + Math.min(cidx + ccnt - 1, + cidx + (vtop - elemRange.top) / avg, + this._total + )); + + // Calculate the end index -- prevent vtop from getting + // negative (and so idxEnd being smaller than cidx) and + // ensure that idxEnd is not larger than the maximum + // container index. + const vbot = Math.max(0, vcr_bot); + let idxEnd = Math.ceil( + Math.min(cidx + ccnt - 1, + cidx + (vbot - elemRange.top) / avg, + this._total + )); + + // Initial resize while the grid is hidden will give NaN + // This is an important optimisation, as it is involved in not + // loading all rows, so we override in that case so + // there are more than the 2-3 that fit in the min height. + if(isNaN(idxStart) && isSpacer) idxStart = cidx-1; + if(isNaN(idxEnd) && isSpacer && this._scrollHeight > 0 && elemRange.bottom == 0) + { + idxEnd = Math.min(ccnt,cidx + Math.ceil( + (this._viewRange.bottom - container._top) / (this._orgAvgHeight || 0) + )); + } + + // Call the data callback + if (this._callback) + { + const self = this; + egw.debug("log","Dataview grid flag for update: ", {start:idxStart,end:idxEnd}); + window.setTimeout(function() { + // If row template changes, self._callback might disappear + if(typeof self._callback != "undefined") + { + self._callback.call(self._context, idxStart, idxEnd); + } + }, 0); + } + } + } + } + } + + /** + * Invalidate iterates over the "mapping" array. It calculates which + * containers have to be removed and where new containers should be added. + */ + _doInvalidate( _super?) + { + if(!this.doInvalidate) return; + + // Update the pixel positions + this._recalculateElementPosition(); + + // Call the callback + if (this._invalidateCallback) + { + const range = this.getVisibleIndexRange( + et2_range(this.scrollarea.scrollTop(), this._scrollHeight)); + this._invalidateCallback.call(this._invalidateContext, range); + } + + // Get the visible mapping indices and recalculate index and pixel + // position of the containers. + const mapVis = this._calculateVisibleMappingIndices(); + + // Delete all invisible elements -- if anything changed, we have to + // recalculate the pixel positions again + if (this._cleanupOutOfRangeElements(mapVis)) + { + this._recalculateElementPosition(); + } + + // Update the view range of all visible elements that implement the + // corresponding interface and request elements for all visible spacers + this._updateContainers(); + + // Call the inherited invalidate function, broadcast the invalidation + // through the container tree. + if (this._parent && _super) + { + _super._doInvalidate() + } + } + + /** + * Translates the given grid index into the element index of the map. If the + * given index is completely out of the range, "false" is returned. + */ + _calculateMapIndex( _index) + { + + let top = 0; + let bot = this._map.length - 1; + + while (top <= bot) + { + const idx = Math.floor((top + bot) / 2); + const elem = this._map[idx]; + + const realIdx = elem.getIndex(); + const realCnt = elem.getCount(); + + if (_index >= realIdx && _index < realIdx + realCnt) + { + return idx; + } + else if (_index < realIdx) + { + bot = idx - 1; + } + else + { + top = idx + 1; + } + } + + return false; + } + + _insertContainerAtSpacer(_index, _mapIndex, _mapElem, _container,_avg) + { + // Set the index of the new container + _container.setIndex(_index); + + // Calculate at which position the spacer has to be splitted + const splitIdx = _index - _mapElem.getIndex(); + + // Get the count of elements that remain at the top of the splitter + const cntTop = splitIdx; + + // Get the count of elements that remain at the bottom of the splitter + // -- it has to be one element less than before + const cntBottom = _mapElem.getCount() - splitIdx - 1; + + // Split the containers if cntTop and cntBottom are larger than zero + if (cntTop > 0 && cntBottom > 0) + { + // Set the new count of the currently existing container, preserving + // its height as it was + _mapElem.setCount(cntTop); + + // Add the new element after the old container + _container.insertIntoTree(_mapElem.getLastNode()); + + // Create a new spacer and add it after the newly inserted container + const newSpacer = new et2_dataview_spacer(this, + this._rowProvider); + newSpacer.setCount(cntBottom, _avg); + newSpacer.setIndex(_index + 1); + newSpacer.insertIntoTree(_container.getLastNode()); + + // Insert the container and the new spacer into the map + this._map.splice(_mapIndex + 1, 0, _container, newSpacer); + } + else if (cntTop === 0 && cntBottom > 0) + { + // Simply adjust the size of the old spacer and insert the new + // container in front of it + _container.insertIntoTree(_mapElem.getFirstNode(), true); + _mapElem.setIndex(_index + 1); + _mapElem.setCount(cntBottom, _avg); + + this._map.splice(_mapIndex, 0, _container); + } + else if (cntTop > 0 && cntBottom === 0) + { + // Simply add the new container to the end of the old container and + // adjust the count of the old spacer to the remaining count. + _container.insertIntoTree(_mapElem.getLastNode()); + _mapElem.setCount(cntTop); + + this._map.splice(_mapIndex + 1, 0, _container); + } + else // if (cntTop === 0 && cntBottom === 0) + { + // Append the new container to the current container and then + // destroy the old container + _container.insertIntoTree(_mapElem.getLastNode()); + _mapElem.destroy(); + + this._map.splice(_mapIndex, 1, _container); + } + } + + _insertContainerAtElement(_index, _mapIndex, _mapElem, _container, _avg) + { + // In a first step, simply insert the element at the specified position, + // in front of the element _mapElem. + _container.setIndex(_index); + _container.insertIntoTree(_mapElem.getFirstNode(), true); + this._map.splice(_mapIndex, 0, _container); + + // Search for the next spacer and increment the indices of all other + // elements until there + let _newIndex = _index + 1; + for (let i = _mapIndex + 1; i < this._map.length; i++) + { + // Update the index of the element + this._map[i].setIndex(_newIndex++); + + // We've found a spacer -- decrement its element count and abort + if (this._map[i] instanceof et2_dataview_spacer) + { + this._decrementSpacerCount(i, _avg); + return; + } + } + + // We've found no spacer so far, remove the last element from the map in + // order to obtain the "totalCount" (especially the last element is no + // spacer, so the following code cannot remove a spacer) + this._map.pop().destroy(); + } + + /** + * Inserts the given container at the given index. + */ + _doInsertContainer( _index, _mapIndex, _container, _avg) + { + // Check whether the given element at the map index is a spacer. If + // yes, we have to split the spacer at that position. + const mapElem = this._map[_mapIndex]; + + if (mapElem instanceof et2_dataview_spacer) + { + this._insertContainerAtSpacer(_index, _mapIndex, mapElem, + _container, _avg); + } + else + { + this._insertContainerAtElement(_index, _mapIndex, mapElem, + _container, _avg); + } + } + + /** + * Replaces the container at the given index with a spacer. The function + * tries to extend any spacer lying above or below the given _mapIndex. + * This code does not destroy the given container, but maintains its map + * index. + * + * @param _mapIndex is the index of _mapElem in the _map array. + * @param _mapElem is the container which should be replaced. + */ + _replaceContainerWithSpacer( _mapIndex : number, _mapElem) + { + let newAvg; + let spacer; + let totalHeight; + let totalCount; + // Check whether a spacer can be extended above or below the given + // _mapIndex + let spacerAbove = null; + let spacerBelow = null; + + if (_mapIndex > 0 + && this._map[_mapIndex - 1] instanceof et2_dataview_spacer) + { + spacerAbove = this._map[_mapIndex - 1]; + } + + if (_mapIndex < this._map.length - 1 + && this._map[_mapIndex + 1] instanceof et2_dataview_spacer) + { + spacerBelow = this._map[_mapIndex + 1]; + } + + if (!spacerAbove && !spacerBelow) + { + // No spacer can be extended -- simply create a new one + spacer = new et2_dataview_spacer(this, this._rowProvider); + spacer.setIndex(_mapElem.getIndex()); + spacer.addAvgHeight(_mapElem.getHeight()); + spacer.setCount(1, _mapElem.getHeight()); + + // Insert the new spacer at the correct place into the DOM tree and + // the mapping + spacer.insertIntoTree(_mapElem.getLastNode()); + this._map.splice(_mapIndex + 1, 0, spacer); + } + else if (spacerAbove && spacerBelow) + { + // We're going to consolidate the upper and the lower spacer. To do + // that we'll calculate a new count of elements and a new average + // height, so that the upper container can get the height of all + // three elements together + totalHeight = spacerAbove.getHeight() + spacerBelow.getHeight() + + _mapElem.getHeight(); + totalCount = spacerAbove.getCount() + spacerBelow.getCount() + + 1; + newAvg = totalHeight / totalCount; + + // Update the upper spacer + spacerAbove.addAvgHeight(_mapElem.getHeight()); + spacerAbove.setCount(totalCount, newAvg); + + // Delete the lower spacer and remove it from the mapping + spacerBelow.destroy(); + this._map.splice(_mapIndex + 1, 1); + } + else + { + // One of the two spacers is available + spacer = spacerAbove || spacerBelow; + + // Calculate the new count and the new average height of that spacer + totalCount = spacer.getCount() + 1; + totalHeight = spacer.getHeight() + _mapElem.getHeight(); + newAvg = totalHeight / totalCount; + + // Set the new container height + spacer.setIndex(Math.min(spacer.getIndex(), _mapElem.getIndex())); + spacer.addAvgHeight(_mapElem.getHeight()); + spacer.setCount(totalCount, newAvg); + } + } + + /** + * Checks whether there is another spacer below the given map index and if + * yes, consolidates the two. + */ + _consolidateSpacers( _mapIndex) + { + if (_mapIndex < this._map.length - 1 + && this._map[_mapIndex] instanceof et2_dataview_spacer + && this._map[_mapIndex + 1] instanceof et2_dataview_spacer) + { + const spacerAbove = this._map[_mapIndex]; + const spacerBelow = this._map[_mapIndex + 1]; + + // Calculate the new height/count of both containers + const totalHeight = spacerAbove.getHeight() + spacerBelow.getHeight(); + const totalCount = spacerAbove.getCount() + spacerBelow.getCount(); + const newAvg = totalCount / totalHeight; + + // Extend the new spacer + spacerAbove.setCount(totalCount, newAvg); + + // Delete the old spacer + spacerBelow.destroy(); + this._map.splice(_mapIndex + 1, 1); + } + } + + /** + * Decrements the count of the spacer at the given _mapIndex by one. If the + * given spacer has no more elements, it will be removed from the mapping. + * Note that this function does not update the indices of the following + * elements, this function is only used internally by the + * _insertContainerAtElement function and the _doDeleteContainer function + * where appropriate adjustments to the map data structure are done. + * + * @param _mapIndex is the index of the spacer in the "map" data structure. + * @param _avg is the new average height of the container, may be + * "undefined" in which case the height of the spacer rows is kept as it + * was. + */ + _decrementSpacerCount( _mapIndex : number, _avg? : number) + { + const cnt = this._map[_mapIndex].getCount() - 1; + if (cnt > 0) + { + this._map[_mapIndex].setCount(cnt, _avg); + } + else + { + this._map[_mapIndex].destroy(); + this._map.splice(_mapIndex, 1); + } + } + + /** + * Deletes the container at the given index. + */ + _doDeleteContainer( _mapIndex, _replaceWithSpacer) + { + // _replaceWithSpacer defaults to false + _replaceWithSpacer = _replaceWithSpacer ? _replaceWithSpacer : false; + + // Fetch the element at the given map index + const mapElem = this._map[_mapIndex]; + + // Indicates whether an element has really been removed -- if yes, the + // bottom spacer will be extended + let removedElement = false; + + // Check whether the map element is a spacer -- if yes, we have to do + // some special treatment + if (mapElem instanceof et2_dataview_spacer) + { + // Do nothing if the "_replaceWithSpacer" flag is true as the + // element already is a spacer + if (!_replaceWithSpacer) + { + this._decrementSpacerCount(_mapIndex); + removedElement = true; + } + } + else + { + if (_replaceWithSpacer) + { + this._replaceContainerWithSpacer(_mapIndex, mapElem); + } + else + { + removedElement = true; + } + + // Remove the complete (current) container, decrement the _mapIndex + this._map[_mapIndex].destroy(); + this._map.splice(_mapIndex, 1); + _mapIndex--; + + // The delete operation may have created two joining spacers -- this + // is highly suboptimal, so we'll consolidate those two spacers + this._consolidateSpacers(_mapIndex); + } + + // Update the indices of all elements after the current one, if we've + // really removed an element + if (removedElement) + { + for (let i = _mapIndex + 1; i < this._map.length; i++) + { + this._map[i].setIndex(this._map[i].getIndex() - 1); + } + + // Extend the last spacer as we have to maintain the spacer count + this._appendEmptyRows(1); + } + } + + /** + * The appendEmptyRows function is used internally to append empty rows to + * the end of the table. This functionality is needed in order to maintain + * the "total count" in the _doDeleteContainer function and to increase the + * "total count" in the "setCount" function. + * + * @param _count specifies the count of empty rows that will be added to the + * end of the table. + */ + _appendEmptyRows( _count : number) + { + // Special case -- the last element in the "_map" is no spacer -- this + // means, that the "managedRange" is currently at the bottom of the list + // -- so we have to insert a new spacer + let spacer = null; + const lastIndex = this._map.length - 1; + if (this._map.length === 0 || + !(this._map[lastIndex] instanceof et2_dataview_spacer)) + { + // Create a new spacer + spacer = new et2_dataview_spacer(this, this._rowProvider); + + // Insert the spacer -- we have a special case if there currently is + // no element inside the mapping + if (this._map.length === 0) + { + // Add a dummy element to the grid + const dummy = jQuery(document.createElement("tr")); + this.innerTbody.append(dummy); + + // Append the spacer to the grid + spacer.setIndex(0); + spacer.insertIntoTree(dummy, false); + + // Remove the dummy element + dummy.remove(); + } + else + { + // Insert the new spacer after the last element + spacer.setIndex(this._map[lastIndex].getIndex() + 1); + spacer.insertIntoTree(this._map[lastIndex].getLastNode()); + } + + // Add the new spacer to the mapping + this._map.push(spacer); + } + else + { + // Get the spacer at the bottom of the mapping + spacer = this._map[lastIndex]; + } + + // Update the spacer count + spacer.setCount(_count + spacer.getCount(), this.getAverageHeight()); + } + + /** + * The _decreaseTotal function is used to decrease the total row count in + * the grid. It tries to remove the given count of rows from the spacer + * located at the bottom of the grid, if this is not possible, it starts + * removing complete rows. + * + * @param _delta specifies how many rows should be removed. + */ + _decreaseTotal( _delta : number) + { + // Iterate over the current mapping, starting at the bottom and delete + // rows. _delta is decreased for each removed row. Abort when delta is + // zero or the map is empty + while (_delta > 0 && this._map.length > 0) + { + const cont = this._map[this._map.length - 1]; + + // Remove as many containers as possible from spacers + if (cont instanceof et2_dataview_spacer) + { + const diff = cont.getCount() - _delta; + + if (diff > 0) + { + // We're done as the spacer still has entries left + _delta = 0; + cont.setCount(diff, this.getAverageHeight()); + break; + } + else + { + // Decrease _delta by the count of rows the spacer had + _delta -= diff + _delta; + } + } + else + { + // We're going to remove a single row: remove it + _delta -= 1; + } + + // Destroy the container if there are no rows left + cont.destroy(); + this._map.pop(); + } + + // Check whether _delta is really zero + if (_delta > 0) + { + this.egw.debug('error', "Error while decreasing the total count - requested to remove more rows than available."); + } + } + + /** + * Creates the grid DOM-Nodes + */ + _createNodes() + { + + this.tr = jQuery(document.createElement("tr")); + + this.outerCell = jQuery(document.createElement("td")) + .addClass("frame") + .attr("colspan", this._rowProvider.getColumnCount() + + (this._parentGrid ? 0 : 1)) + .appendTo(this.tr); + + // Create the scrollarea div if this is the outer grid + this.scrollarea = null; + if (this._parentGrid == null) + { + this.scrollarea = jQuery(document.createElement("div")) + .addClass("egwGridView_scrollarea") + .scroll(this, function(e) { + + // Clear any older scroll timeout + if (e.data._scrollTimeout) + { + window.clearTimeout(e.data._scrollTimeout); + } + + // Clear any existing "invalidate" timeout (as the + // "setViewRange" function triggered by the scroll event + // forces an "invalidate"). + if (e.data._invalidateTimeout) + { + window.clearTimeout(e.data._invalidateTimeout); + e.data._invalidateTimeout = null; + } + + // Set a new timeout which calls the setViewArea + // function + e.data._scrollTimeout = window.setTimeout(jQuery.proxy(function() { + const newRange = et2_range( + this.scrollarea.scrollTop() - et2_dataview_grid.ET2_GRID_VIEW_EXT, + this._scrollHeight + et2_dataview_grid.ET2_GRID_VIEW_EXT * 2 + ); + + if (!et2_rangeEqual(newRange, this._viewRange)) + { + this.setViewRange(newRange); + } + },e.data), et2_dataview_grid.ET2_GRID_SCROLL_TIMEOUT); + }) + .height(this._scrollHeight) + .appendTo(this.outerCell); + } + + // Create the inner table + const table = jQuery(document.createElement("table")) + .addClass("egwGridView_grid") + .appendTo(this.scrollarea ? this.scrollarea : this.outerCell); + + this.innerTbody = jQuery(document.createElement("tbody")) + .appendTo(table); + + // Set the tr as container element + this.appendNode(jQuery(this.tr[0])); + } + +} + diff --git a/api/js/etemplate/et2_dataview_view_resizeable.js b/api/js/etemplate/et2_dataview_view_resizeable.js index 3a7e28870b..f05f2ae732 100644 --- a/api/js/etemplate/et2_dataview_view_resizeable.js +++ b/api/js/etemplate/et2_dataview_view_resizeable.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - Functions which allow resizing of table headers * @@ -9,222 +10,174 @@ * @copyright Stylite 2011 * @version $Id$ */ - +Object.defineProperty(exports, "__esModule", { value: true }); /** * This set of functions is currently only supporting resizing in ew-direction */ - -(function() -{ - "use strict"; - - // Define some constants - var RESIZE_BORDER = 12; - var RESIZE_MIN_WIDTH = 25; - var RESIZE_ADD = 2; // Used to ensure mouse is under the resize element after resizing has finished - - // In resize region returns whether the mouse is currently in the - // "resizeRegion" - function inResizeRegion(_x, _elem) - { - var ol = _x - _elem.offset().left; - return (ol > (_elem.outerWidth(true) - RESIZE_BORDER)); - } - - var helper = null; - var overlay = null; - var didResize = false; - var resizeWidth = 0; - - function startResize(_outerElem, _elem, _callback, _column) - { - if (overlay == null || helper == null) - { - // Prevent text selection - // FireFox handles highlight prevention (text selection) different than other browsers - if (typeof _elem[0].style.MozUserSelect !="undefined") - { - _elem[0].style.MozUserSelect = "none"; - } - else - { - _elem[0].onselectstart = function() { - return false; - }; - } - - // Indicate resizing is in progress - jQuery(_outerElem).addClass('egwResizing'); - - // Reset the "didResize" flag - didResize = false; - - // Create the resize helper - var left = _elem.offset().left; - helper = jQuery(document.createElement("div")) - .addClass("egwResizeHelper") - .appendTo("body") - .css("top", _elem.offset().top + "px") - .css("left", left + "px") - .css("height", _outerElem.outerHeight(true) + "px"); - - // Create the overlay which will be catching the mouse movements - overlay = jQuery(document.createElement("div")) - .addClass("egwResizeOverlay") - - .bind("mousemove", function(e) { - didResize = true; - resizeWidth = Math.max(e.pageX - left + RESIZE_ADD, - _column && _column.minWidth ? _column.minWidth : RESIZE_MIN_WIDTH - ); - helper.css("width", resizeWidth + "px"); - }) - - .bind("mouseup", function() { - stopResize(_outerElem); - - // Reset text selection - _elem[0].onselectstart = null; - - // Call the callback if the user actually performed a resize - if (didResize) - { - _callback(resizeWidth); - } - }) - .appendTo("body"); - } - } - - function stopResize(_outerElem) - { - - jQuery(_outerElem).removeClass('egwResizing'); - if (helper != null) - { - helper.remove(); - helper = null; - } - - if (overlay != null) - { - overlay.remove(); - overlay = null; - } - } - - this.et2_dataview_makeResizeable = function(_elem, _callback, _context) - { - // Get the table surrounding the given element - this element is used to - // align the helper properly - var outerTable = _elem.closest("table"); - - // Bind the "mousemove" event in the "resize" namespace - _elem.bind("mousemove.resize", function(e) { - var stopResize = false; - // Stop switch to resize cursor if the mouse position - // is more intended for scrollbar not the resize edge - // 8pixel is an arbitary number for scrolbar area - if (e.target.clientHeight < e.target.scrollHeight && e.target.offsetWidth - e.offsetX <= 8) - { - stopResize = true; - } - _elem.css("cursor", inResizeRegion(e.pageX, _elem) && !stopResize? "ew-resize" : "auto"); - }); - - // Bind the "mousedown" event in the "resize" namespace - _elem.bind("mousedown.resize", function(e) { - var stopResize = false; - // Stop resize if the mouse position is more intended - // for scrollbar not the resize edge - // 8pixel is an arbitary number for scrolbar area - if (e.target.clientHeight < e.target.scrollHeight && e.target.offsetWidth - e.offsetX <= 8) - { - stopResize = true; - } - // Do not triger startResize if clicked element is select-tag, as it may causes conflict in some browsers - if (inResizeRegion(e.pageX, _elem) && e.target.tagName != 'SELECT' && !stopResize) - { - // Start the resizing - startResize(outerTable, _elem, function(_w) { - _callback.call(_context, _w); - }, _context); - } - - }); - - // Bind double click for auto-size - _elem.dblclick(function(e) { - // Just show message for relative width columns - if(_context && _context.relativeWidth) - { - return egw.message(egw.lang('You tried to automatically size a flex column, which always takes the rest of the space','info')); - } - // Find column class - it's usually the first one - var col_class = ''; - for(var i = 0; i < this.classList.length; i++) - { - if(this.classList[i].indexOf('gridCont') === 0) - { - col_class = this.classList[i]; - break; - } - } - - // Find widest part, including header - var column = jQuery(this); - column.children().css('width','auto'); - var max_width = column.children().children().innerWidth(); - var padding = column.outerWidth(true) - max_width; - - var resize = jQuery(this).closest('.egwGridView_outer') - .find('tbody td.'+col_class+'> div:first-child') - .add(column.children()) - // Set column width to auto to allow space for everything to flow - .css('width','auto'); - resize.children() - .css({'white-space':'nowrap'}) - .each(function() - { - var col = jQuery(this); - // Find visible (text) children and force them to not wrap - var children = col.find('span:visible, time:visible, label:visible') - .css({'white-space':'nowrap'}) - this.offsetWidth; - children.each(function() - { - var child = jQuery(this); - this.offsetWidth; - if(child.outerWidth() > max_width) - { - max_width = child.outerWidth(); - } - window.getComputedStyle(this).width; - }); - this.offsetWidth; - if(col.innerWidth() > max_width) - { - max_width = col.innerWidth(); - } - - // Reset children - children.css('white-space',''); - children.css('display',''); - } - ) - .css({'white-space':''}); - - // Reset column - column.children().css('width',''); - resize.css('width',''); - _callback.call(_context, max_width+padding); - }); - }; - - this.et2_dataview_resetResizeable = function(_elem) - { - // Remove all events in the ".resize" namespace from the element - _elem.unbind(".resize"); - }; -}).call(window); - +var et2_dataview_view_resizable = /** @class */ (function () { + function et2_dataview_view_resizable() { + } + // In resize region returns whether the mouse is currently in the + // "resizeRegion" + et2_dataview_view_resizable.inResizeRegion = function (_x, _elem) { + var ol = _x - _elem.offset().left; + return (ol > (_elem.outerWidth(true) - et2_dataview_view_resizable.RESIZE_BORDER)); + }; + et2_dataview_view_resizable.startResize = function (_outerElem, _elem, _callback, _column) { + if (this.overlay == null || this.helper == null) { + // Prevent text selection + // FireFox handles highlight prevention (text selection) different than other browsers + if (typeof _elem[0].style.MozUserSelect != "undefined") { + _elem[0].style.MozUserSelect = "none"; + } + else { + _elem[0].onselectstart = function () { + return false; + }; + } + // Indicate resizing is in progress + jQuery(_outerElem).addClass('egwResizing'); + // Reset the "didResize" flag + this.didResize = false; + // Create the resize helper + var left = _elem.offset().left; + this.helper = jQuery(document.createElement("div")) + .addClass("egwResizeHelper") + .appendTo("body") + .css("top", _elem.offset().top + "px") + .css("left", left + "px") + .css("height", _outerElem.outerHeight(true) + "px"); + // Create the overlay which will be catching the mouse movements + this.overlay = jQuery(document.createElement("div")) + .addClass("egwResizeOverlay") + .bind("mousemove", function (e) { + this.didResize = true; + this.resizeWidth = Math.max(e.pageX - left + et2_dataview_view_resizable.RESIZE_ADD, _column && _column.minWidth ? _column.minWidth : et2_dataview_view_resizable.RESIZE_MIN_WIDTH); + this.helper.css("width", this.resizeWidth + "px"); + }.bind(this)) + .bind("mouseup", function () { + this.stopResize(_outerElem); + // Reset text selection + _elem[0].onselectstart = null; + // Call the callback if the user actually performed a resize + if (this.didResize) { + _callback(this.resizeWidth); + } + }.bind(this)) + .appendTo("body"); + } + }; + et2_dataview_view_resizable.stopResize = function (_outerElem) { + jQuery(_outerElem).removeClass('egwResizing'); + if (this.helper != null) { + this.helper.remove(); + this.helper = null; + } + if (this.overlay != null) { + this.overlay.remove(); + this.overlay = null; + } + }; + // Define some constants + et2_dataview_view_resizable.RESIZE_BORDER = 12; + et2_dataview_view_resizable.RESIZE_MIN_WIDTH = 25; + et2_dataview_view_resizable.RESIZE_ADD = 2; // Used to ensure mouse is under the resize element after resizing has finished + et2_dataview_view_resizable.helper = null; + et2_dataview_view_resizable.overlay = null; + et2_dataview_view_resizable.didResize = false; + et2_dataview_view_resizable.resizeWidth = 0; + et2_dataview_view_resizable.makeResizeable = function (_elem, _callback, _context) { + // Get the table surrounding the given element - this element is used to + // align the helper properly + var outerTable = _elem.closest("table"); + // Bind the "mousemove" event in the "resize" namespace + _elem.bind("mousemove.resize", function (e) { + var stopResize = false; + // Stop switch to resize cursor if the mouse position + // is more intended for scrollbar not the resize edge + // 8pixel is an arbitary number for scrolbar area + if (e.target.clientHeight < e.target.scrollHeight && e.target.offsetWidth - e.offsetX <= 8) { + stopResize = true; + } + _elem.css("cursor", et2_dataview_view_resizable.inResizeRegion(e.pageX, _elem) && !stopResize ? "ew-resize" : "auto"); + }); + // Bind the "mousedown" event in the "resize" namespace + _elem.bind("mousedown.resize", function (e) { + var stopResize = false; + // Stop resize if the mouse position is more intended + // for scrollbar not the resize edge + // 8pixel is an arbitary number for scrolbar area + if (e.target.clientHeight < e.target.scrollHeight && e.target.offsetWidth - e.offsetX <= 8) { + stopResize = true; + } + // Do not triger startResize if clicked element is select-tag, as it may causes conflict in some browsers + if (et2_dataview_view_resizable.inResizeRegion(e.pageX, _elem) && e.target.tagName != 'SELECT' && !stopResize) { + // Start the resizing + et2_dataview_view_resizable.startResize(outerTable, _elem, function (_w) { + _callback.call(_context, _w); + }, _context); + } + }); + // Bind double click for auto-size + _elem.dblclick(function (e) { + // Just show message for relative width columns + if (_context && _context.relativeWidth) { + return egw.message(egw.lang('You tried to automatically size a flex column, which always takes the rest of the space', 'info')); + } + // Find column class - it's usually the first one + var col_class = ''; + for (var i = 0; i < this.classList.length; i++) { + if (this.classList[i].indexOf('gridCont') === 0) { + col_class = this.classList[i]; + break; + } + } + // Find widest part, including header + var column = jQuery(this); + column.children().css('width', 'auto'); + var max_width = column.children().children().innerWidth(); + var padding = column.outerWidth(true) - max_width; + var resize = jQuery(this).closest('.egwGridView_outer') + .find('tbody td.' + col_class + '> div:first-child') + .add(column.children()) + // Set column width to auto to allow space for everything to flow + .css('width', 'auto'); + resize.children() + .css({ 'white-space': 'nowrap' }) + .each(function () { + var col = jQuery(this); + // Find visible (text) children and force them to not wrap + var children = col.find('span:visible, time:visible, label:visible') + .css({ 'white-space': 'nowrap' }); + this.offsetWidth; + children.each(function () { + var child = jQuery(this); + this.offsetWidth; + if (child.outerWidth() > max_width) { + max_width = child.outerWidth(); + } + window.getComputedStyle(this).width; + }); + this.offsetWidth; + if (col.innerWidth() > max_width) { + max_width = col.innerWidth(); + } + // Reset children + children.css('white-space', ''); + children.css('display', ''); + }) + .css({ 'white-space': '' }); + // Reset column + column.children().css('width', ''); + resize.css('width', ''); + _callback.call(_context, max_width + padding); + }); + }; + et2_dataview_view_resizable.et2_dataview_resetResizeable = function (_elem) { + // Remove all events in the ".resize" namespace from the element + _elem.unbind(".resize"); + }; + return et2_dataview_view_resizable; +}()); +exports.et2_dataview_view_resizable = et2_dataview_view_resizable; +//# sourceMappingURL=et2_dataview_view_resizeable.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_dataview_view_resizeable.ts b/api/js/etemplate/et2_dataview_view_resizeable.ts new file mode 100644 index 0000000000..97813ac782 --- /dev/null +++ b/api/js/etemplate/et2_dataview_view_resizeable.ts @@ -0,0 +1,230 @@ +/** + * EGroupware eTemplate2 - Functions which allow resizing of table headers + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage dataview + * @link http://www.egroupware.org + * @author Andreas Stöckel + * @copyright Stylite 2011 + * @version $Id$ + */ + +/** + * This set of functions is currently only supporting resizing in ew-direction + */ + +export class et2_dataview_view_resizable +{ + + // Define some constants + public static readonly RESIZE_BORDER = 12; + public static readonly RESIZE_MIN_WIDTH = 25; + public static readonly RESIZE_ADD = 2; // Used to ensure mouse is under the resize element after resizing has finished + + + public static helper : JQuery = null; + public static overlay : JQuery = null; + public static didResize = false; + public static resizeWidth = 0; + + // In resize region returns whether the mouse is currently in the + // "resizeRegion" + public static inResizeRegion(_x, _elem) + { + var ol = _x - _elem.offset().left; + return (ol > (_elem.outerWidth(true) - et2_dataview_view_resizable.RESIZE_BORDER)); + } + + public static startResize(_outerElem, _elem, _callback, _column) + { + if (this.overlay == null || this.helper == null) + { + // Prevent text selection + // FireFox handles highlight prevention (text selection) different than other browsers + if (typeof _elem[0].style.MozUserSelect !="undefined") + { + _elem[0].style.MozUserSelect = "none"; + } + else + { + _elem[0].onselectstart = function() { + return false; + }; + } + + // Indicate resizing is in progress + jQuery(_outerElem).addClass('egwResizing'); + + // Reset the "didResize" flag + this.didResize = false; + + // Create the resize helper + var left = _elem.offset().left; + this.helper = jQuery(document.createElement("div")) + .addClass("egwResizeHelper") + .appendTo("body") + .css("top", _elem.offset().top + "px") + .css("left", left + "px") + .css("height", _outerElem.outerHeight(true) + "px"); + + // Create the overlay which will be catching the mouse movements + this.overlay = jQuery(document.createElement("div")) + .addClass("egwResizeOverlay") + + .bind("mousemove", function(e) { + this.didResize = true; + this.resizeWidth = Math.max(e.pageX - left + et2_dataview_view_resizable.RESIZE_ADD, + _column && _column.minWidth ? _column.minWidth : et2_dataview_view_resizable.RESIZE_MIN_WIDTH + ); + this.helper.css("width", this.resizeWidth + "px"); + }.bind(this)) + + .bind("mouseup", function() { + this.stopResize(_outerElem); + + // Reset text selection + _elem[0].onselectstart = null; + + // Call the callback if the user actually performed a resize + if (this.didResize) + { + _callback(this.resizeWidth); + } + }.bind(this)) + .appendTo("body"); + } + } + + public static stopResize(_outerElem) + { + + jQuery(_outerElem).removeClass('egwResizing'); + if (this.helper != null) + { + this.helper.remove(); + this.helper = null; + } + + if (this.overlay != null) + { + this.overlay.remove(); + this.overlay = null; + } + } + + public static makeResizeable = function(_elem, _callback, _context) + { + // Get the table surrounding the given element - this element is used to + // align the helper properly + var outerTable = _elem.closest("table"); + + // Bind the "mousemove" event in the "resize" namespace + _elem.bind("mousemove.resize", function(e) { + var stopResize = false; + // Stop switch to resize cursor if the mouse position + // is more intended for scrollbar not the resize edge + // 8pixel is an arbitary number for scrolbar area + if (e.target.clientHeight < e.target.scrollHeight && e.target.offsetWidth - e.offsetX <= 8) + { + stopResize = true; + } + _elem.css("cursor", et2_dataview_view_resizable.inResizeRegion(e.pageX, _elem) && !stopResize? "ew-resize" : "auto"); + }); + + // Bind the "mousedown" event in the "resize" namespace + _elem.bind("mousedown.resize", function(e) { + var stopResize = false; + // Stop resize if the mouse position is more intended + // for scrollbar not the resize edge + // 8pixel is an arbitary number for scrolbar area + if (e.target.clientHeight < e.target.scrollHeight && e.target.offsetWidth - e.offsetX <= 8) + { + stopResize = true; + } + // Do not triger startResize if clicked element is select-tag, as it may causes conflict in some browsers + if (et2_dataview_view_resizable.inResizeRegion(e.pageX, _elem) && e.target.tagName != 'SELECT' && !stopResize) + { + // Start the resizing + et2_dataview_view_resizable.startResize(outerTable, _elem, function(_w) { + _callback.call(_context, _w); + }, _context); + } + + }); + + // Bind double click for auto-size + _elem.dblclick(function(e) { + // Just show message for relative width columns + if(_context && _context.relativeWidth) + { + return egw.message(egw.lang('You tried to automatically size a flex column, which always takes the rest of the space','info')); + } + // Find column class - it's usually the first one + var col_class = ''; + for(var i = 0; i < this.classList.length; i++) + { + if(this.classList[i].indexOf('gridCont') === 0) + { + col_class = this.classList[i]; + break; + } + } + + // Find widest part, including header + var column = jQuery(this); + column.children().css('width','auto'); + var max_width = column.children().children().innerWidth(); + var padding = column.outerWidth(true) - max_width; + + var resize = jQuery(this).closest('.egwGridView_outer') + .find('tbody td.'+col_class+'> div:first-child') + .add(column.children()) + // Set column width to auto to allow space for everything to flow + .css('width','auto'); + resize.children() + .css({'white-space':'nowrap'}) + .each(function() + { + var col = jQuery(this); + // Find visible (text) children and force them to not wrap + var children = col.find('span:visible, time:visible, label:visible') + .css({'white-space':'nowrap'}); + this.offsetWidth; + children.each(function() + { + var child = jQuery(this); + this.offsetWidth; + if(child.outerWidth() > max_width) + { + max_width = child.outerWidth(); + } + window.getComputedStyle(this).width; + }); + this.offsetWidth; + if(col.innerWidth() > max_width) + { + max_width = col.innerWidth(); + } + + // Reset children + children.css('white-space',''); + children.css('display',''); + } + ) + .css({'white-space':''}); + + // Reset column + column.children().css('width',''); + resize.css('width',''); + _callback.call(_context, max_width+padding); + }); + }; + + public static et2_dataview_resetResizeable = function(_elem) + { + // Remove all events in the ".resize" namespace from the element + _elem.unbind(".resize"); + } +} + diff --git a/api/js/etemplate/et2_dataview_view_row.js b/api/js/etemplate/et2_dataview_view_row.js index adfb69f670..d669dfed2c 100644 --- a/api/js/etemplate/et2_dataview_view_row.js +++ b/api/js/etemplate/et2_dataview_view_row.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - dataview * @@ -8,188 +9,152 @@ * @author Andreas Stöckel * @copyright Stylite 2011-2012 * @version $Id$ - */ + * /*egw:uses - egw_action.egw_action; + egw_action.egw_action; - et2_dataview_view_container; + et2_dataview_view_container; */ - -/** - * @augments et2_dataview_container - */ -var et2_dataview_row = (function(){ "use strict"; return et2_dataview_container.extend(et2_dataview_IViewRange, -{ - /** - * Creates the row container. Use the "setRow" function to load the actual - * row content. - * - * @param _parent is the row parent container. - * @memberOf et2_dataview_row - */ - init: function(_parent) { - // Call the inherited constructor - this._super(_parent); - - // Create the outer "tr" tag and append it to the container - this.tr = jQuery(document.createElement("tr")); - this.appendNode(this.tr); - - // Grid row which gets expanded when clicking on the corresponding - // button - this.expansionContainer = null; - this.expansionVisible = false; - - // Toggle button which is used to show and hide the expansionContainer - this.expansionButton = null; - }, - - destroy: function () { - - if (this.expansionContainer != null) - { - this.expansionContainer.free(); - } - - this._super(); - }, - - clear: function() { - this.tr.empty(); - }, - - makeExpandable: function (_expandable, _callback, _context) { - if (_expandable) - { - // Create the tr and the button if this has not been done yet - if (!this.expansionButton) - { - this.expansionButton = jQuery(document.createElement("span")); - this.expansionButton.addClass("arrow closed"); - } - - // Update context - var self = this; - this.expansionButton.off("click").on("click", function (e) { - self._handleExpansionButtonClick(_callback, _context); - e.stopImmediatePropagation(); - }); - - jQuery("td:first", this.tr).prepend(this.expansionButton); - } - else - { - // If the row is made non-expandable, remove the created DOM-Nodes - if (this.expansionButton) - { - this.expansionButton.remove(); - } - - if (this.expansionContainer) - { - this.expansionContainer.free(); - } - - this.expansionButton = null; - this.expansionContainer = null; - } - }, - - removeFromTree: function () { - if (this.expansionContainer) - { - this.expansionContainer.removeFromTree(); - } - - this.expansionContainer = null; - this.expansionButton = null; - - this._super(); - }, - - getDOMNode: function () { - return this.tr[0]; - }, - - getJNode: function () { - return this.tr; - }, - - getHeight: function () { - var h = this._super(); - - if (this.expansionContainer && this.expansionVisible) - { - h += this.expansionContainer.getHeight(); - } - - return h; - }, - - getAvgHeightData: function() { - // Only take the height of the own tr into account - //var oldVisible = this.expansionVisible; - this.expansionVisible = false; - - var res = { - "avgHeight": this.getHeight(), - "avgCount": 1 - }; - - this.expansionVisible = true; - - return res; - }, - - - /** -- PRIVATE FUNCTIONS -- **/ - - - _handleExpansionButtonClick: function (_callback, _context) { - // Create the "expansionContainer" if it does not exist yet - if (!this.expansionContainer) - { - this.expansionContainer = _callback.call(_context); - this.expansionContainer.insertIntoTree(this.tr); - this.expansionVisible = false; - } - - // Toggle the visibility of the expansion tr - this.expansionVisible = !this.expansionVisible; - jQuery(this.expansionContainer._nodes[0]).toggle(this.expansionVisible); - - // Set the class of the arrow - if (this.expansionVisible) - { - this.expansionButton.addClass("opened"); - this.expansionButton.removeClass("closed"); - } - else - { - this.expansionButton.addClass("closed"); - this.expansionButton.removeClass("opened"); - } - - this.invalidate(); - }, - - - /** -- Implementation of et2_dataview_IViewRange -- **/ - - - setViewRange: function (_range) { - if (this.expansionContainer && this.expansionVisible - && this.expansionContainer.implements(et2_dataview_IViewRange)) - { - // Substract the height of the own row from the container - var oh = jQuery(this._nodes[0]).height(); - _range.top -= oh; - - // Proxy the setViewRange call to the expansion container - this.expansionContainer.setViewRange(_range); - } - } - -});}).call(this); - +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 }); +var et2_dataview_row = /** @class */ (function (_super) { + __extends(et2_dataview_row, _super); + /** + * Creates the row container. Use the "setRow" function to load the actual + * row content. + * + * @param _parent is the row parent container. + */ + function et2_dataview_row(_parent) { + var _this = + // Call the inherited constructor + _super.call(this, _parent) || this; + // Create the outer "tr" tag and append it to the container + _this.tr = jQuery(document.createElement("tr")); + _this.appendNode(_this.tr); + // Grid row which gets expanded when clicking on the corresponding + // button + _this.expansionContainer = null; + _this.expansionVisible = false; + // Toggle button which is used to show and hide the expansionContainer + _this.expansionButton = null; + return _this; + } + et2_dataview_row.prototype.destroy = function () { + if (this.expansionContainer != null) { + this.expansionContainer.destroy(); + } + _super.prototype.destroy.call(this); + }; + et2_dataview_row.prototype.clear = function () { + this.tr.empty(); + }; + et2_dataview_row.prototype.makeExpandable = function (_expandable, _callback, _context) { + if (_expandable) { + // Create the tr and the button if this has not been done yet + if (!this.expansionButton) { + this.expansionButton = jQuery(document.createElement("span")); + this.expansionButton.addClass("arrow closed"); + } + // Update context + var self = this; + this.expansionButton.off("click").on("click", function (e) { + self._handleExpansionButtonClick(_callback, _context); + e.stopImmediatePropagation(); + }); + jQuery("td:first", this.tr).prepend(this.expansionButton); + } + else { + // If the row is made non-expandable, remove the created DOM-Nodes + if (this.expansionButton) { + this.expansionButton.remove(); + } + if (this.expansionContainer) { + this.expansionContainer.destroy(); + } + this.expansionButton = null; + this.expansionContainer = null; + } + }; + et2_dataview_row.prototype.removeFromTree = function () { + if (this.expansionContainer) { + this.expansionContainer.removeFromTree(); + } + this.expansionContainer = null; + this.expansionButton = null; + _super.prototype.removeFromTree.call(this); + }; + et2_dataview_row.prototype.getDOMNode = function () { + return this.tr[0]; + }; + et2_dataview_row.prototype.getJNode = function () { + return this.tr; + }; + et2_dataview_row.prototype.getHeight = function () { + var h = _super.prototype.getHeight.call(this); + if (this.expansionContainer && this.expansionVisible) { + h += this.expansionContainer.getHeight(); + } + return h; + }; + et2_dataview_row.prototype.getAvgHeightData = function () { + // Only take the height of the own tr into account + //var oldVisible = this.expansionVisible; + this.expansionVisible = false; + var res = { + "avgHeight": this.getHeight(), + "avgCount": 1 + }; + this.expansionVisible = true; + return res; + }; + /** -- PRIVATE FUNCTIONS -- **/ + et2_dataview_row.prototype._handleExpansionButtonClick = function (_callback, _context) { + // Create the "expansionContainer" if it does not exist yet + if (!this.expansionContainer) { + this.expansionContainer = _callback.call(_context); + this.expansionContainer.insertIntoTree(this.tr); + this.expansionVisible = false; + } + // Toggle the visibility of the expansion tr + this.expansionVisible = !this.expansionVisible; + jQuery(this.expansionContainer._nodes[0]).toggle(this.expansionVisible); + // Set the class of the arrow + if (this.expansionVisible) { + this.expansionButton.addClass("opened"); + this.expansionButton.removeClass("closed"); + } + else { + this.expansionButton.addClass("closed"); + this.expansionButton.removeClass("opened"); + } + this.invalidate(); + }; + /** -- Implementation of et2_dataview_IViewRange -- **/ + et2_dataview_row.prototype.setViewRange = function (_range) { + if (this.expansionContainer && this.expansionVisible + && this.expansionContainer.implements(et2_dataview_IViewRange)) { + // Substract the height of the own row from the container + var oh = jQuery(this._nodes[0]).height(); + _range.top -= oh; + // Proxy the setViewRange call to the expansion container + this.expansionContainer.setViewRange(_range); + } + }; + return et2_dataview_row; +}(et2_dataview_container)); +exports.et2_dataview_row = et2_dataview_row; +//# sourceMappingURL=et2_dataview_view_row.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_dataview_view_row.ts b/api/js/etemplate/et2_dataview_view_row.ts new file mode 100644 index 0000000000..185457f047 --- /dev/null +++ b/api/js/etemplate/et2_dataview_view_row.ts @@ -0,0 +1,203 @@ +/** + * EGroupware eTemplate2 - dataview + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage dataview + * @link http://www.egroupware.org + * @author Andreas Stöckel + * @copyright Stylite 2011-2012 + * @version $Id$ + * + +/*egw:uses + egw_action.egw_action; + + et2_dataview_view_container; +*/ + +import {et2_dataview_IViewRange} from "./et2_dataview_interfaces"; + +export class et2_dataview_row extends et2_dataview_container implements et2_dataview_IViewRange +{ + /** + * Creates the row container. Use the "setRow" function to load the actual + * row content. + * + * @param _parent is the row parent container. + */ + constructor( _parent) + { + // Call the inherited constructor + super(_parent); + + // Create the outer "tr" tag and append it to the container + this.tr = jQuery(document.createElement("tr")); + this.appendNode(this.tr); + + // Grid row which gets expanded when clicking on the corresponding + // button + this.expansionContainer = null; + this.expansionVisible = false; + + // Toggle button which is used to show and hide the expansionContainer + this.expansionButton = null; + } + + destroy( ) + { + + if (this.expansionContainer != null) + { + this.expansionContainer.destroy(); + } + + super.destroy(); + } + + clear( ) + { + this.tr.empty(); + } + + makeExpandable( _expandable, _callback, _context) + { + if (_expandable) + { + // Create the tr and the button if this has not been done yet + if (!this.expansionButton) + { + this.expansionButton = jQuery(document.createElement("span")); + this.expansionButton.addClass("arrow closed"); + } + + // Update context + var self = this; + this.expansionButton.off("click").on("click", function (e) { + self._handleExpansionButtonClick(_callback, _context); + e.stopImmediatePropagation(); + }); + + jQuery("td:first", this.tr).prepend(this.expansionButton); + } + else + { + // If the row is made non-expandable, remove the created DOM-Nodes + if (this.expansionButton) + { + this.expansionButton.remove(); + } + + if (this.expansionContainer) + { + this.expansionContainer.destroy(); + } + + this.expansionButton = null; + this.expansionContainer = null; + } + } + + removeFromTree( ) + { + if (this.expansionContainer) + { + this.expansionContainer.removeFromTree(); + } + + this.expansionContainer = null; + this.expansionButton = null; + + super.removeFromTree(); + } + + getDOMNode( ) + { + return this.tr[0]; + } + + getJNode( ) + { + return this.tr; + } + + getHeight( ) + { + var h = super.getHeight(); + + if (this.expansionContainer && this.expansionVisible) + { + h += this.expansionContainer.getHeight(); + } + + return h; + } + + getAvgHeightData( ) + { + // Only take the height of the own tr into account + //var oldVisible = this.expansionVisible; + this.expansionVisible = false; + + var res = { + "avgHeight": this.getHeight(), + "avgCount": 1 + }; + + this.expansionVisible = true; + + return res; + } + + + /** -- PRIVATE FUNCTIONS -- **/ + + + _handleExpansionButtonClick( _callback, _context) + { + // Create the "expansionContainer" if it does not exist yet + if (!this.expansionContainer) + { + this.expansionContainer = _callback.call(_context); + this.expansionContainer.insertIntoTree(this.tr); + this.expansionVisible = false; + } + + // Toggle the visibility of the expansion tr + this.expansionVisible = !this.expansionVisible; + jQuery(this.expansionContainer._nodes[0]).toggle(this.expansionVisible); + + // Set the class of the arrow + if (this.expansionVisible) + { + this.expansionButton.addClass("opened"); + this.expansionButton.removeClass("closed"); + } + else + { + this.expansionButton.addClass("closed"); + this.expansionButton.removeClass("opened"); + } + + this.invalidate(); + } + + + /** -- Implementation of et2_dataview_IViewRange -- **/ + + + setViewRange( _range) + { + if (this.expansionContainer && this.expansionVisible + && this.expansionContainer.implements(et2_dataview_IViewRange)) + { + // Substract the height of the own row from the container + var oh = jQuery(this._nodes[0]).height(); + _range.top -= oh; + + // Proxy the setViewRange call to the expansion container + this.expansionContainer.setViewRange(_range); + } + } + +} \ No newline at end of file diff --git a/api/js/etemplate/et2_dataview_view_rowProvider.js b/api/js/etemplate/et2_dataview_view_rowProvider.js index e50e1336b7..b793cba537 100644 --- a/api/js/etemplate/et2_dataview_view_rowProvider.js +++ b/api/js/etemplate/et2_dataview_view_rowProvider.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - Class which contains a factory method for rows * @@ -9,121 +10,103 @@ * @copyright Stylite 2011 * @version $Id$ */ - +Object.defineProperty(exports, "__esModule", { value: true }); /*egw:uses - /vendor/bower-asset/jquery/dist/jquery.js; - et2_core_inheritance; - et2_core_interfaces; - et2_core_arrayMgr; - et2_core_widget; + /vendor/bower-asset/jquery/dist/jquery.js; + et2_core_inheritance; + et2_core_interfaces; + et2_core_arrayMgr; + et2_core_widget; */ - /** * The row provider contains prototypes (full clonable dom-trees) * for all registered row types. - * - * @augments Class */ -var et2_dataview_rowProvider = (function(){ "use strict"; return Class.extend( -{ - /** - * - * @param _outerId - * @param _columnIds - * @memberOf et2_dataview_rowProvider - */ - init: function(_outerId, _columnIds) { - // Copy the given parameters - this._outerId = _outerId; - this._columnIds = _columnIds; - this._prototypes = {}; - - this._template = null; - this._mgrs = null; - this._rootWidget = null; - - // Create the default row "prototypes" - this._createFullRowPrototype(); - this._createDefaultPrototype(); - this._createEmptyPrototype(); - this._createLoadingPrototype(); - }, - - getColumnCount: function() { - return this._columnIds.length; - }, - - /** - * Returns a clone of the prototype with the given name. If the generator - * callback function is given, this function is called if the prototype - * does not yet registered. - * - * @param {string} _name - * @param {function} _generator - * @param {object} _context - */ - getPrototype: function(_name, _generator, _context) { - if (typeof this._prototypes[_name] == "undefined") - { - if (typeof _generator != "undefined") - { - this._prototypes[_name] = _generator.call(_context, this._outerId, - this._columnIds); - } - else - { - return null; - } - } - - return this._prototypes[_name].clone(); - }, - - - /* ---- PRIVATE FUNCTIONS ---- */ - - - _createFullRowPrototype: function() { - var tr = jQuery(document.createElement("tr")); - var td = jQuery(document.createElement("td")) - .addClass(this._outerId + "_td_fullRow") - .attr("colspan", this._columnIds.length) - .appendTo(tr); - var div = jQuery(document.createElement("div")) - .addClass(this._outerId + "_div_fullRow") - .appendTo(td); - - this._prototypes["fullRow"] = tr; - }, - - _createDefaultPrototype: function() { - var tr = jQuery(document.createElement("tr")); - - // Append a td for each column - for (var i = 0; i < this._columnIds.length; i++) - { - var td = jQuery(document.createElement("td")) - .addClass(this._outerId + "_td_" + this._columnIds[i]) - .appendTo(tr); - var div = jQuery(document.createElement("div")) - .addClass(this._outerId + "_div_" + this._columnIds[i]) - .addClass("innerContainer") - .appendTo(td); - } - - this._prototypes["default"] = tr; - }, - - _createEmptyPrototype: function() { - this._prototypes["empty"] = jQuery(document.createElement("tr")); - }, - - _createLoadingPrototype: function() { - var fullRow = this.getPrototype("fullRow"); - jQuery("div", fullRow).addClass("loading"); - - this._prototypes["loading"] = fullRow; - } - -});}).call(this); - +var et2_dataview_rowProvider = /** @class */ (function () { + /** + * + * @param _outerId + * @param _columnIds + */ + function et2_dataview_rowProvider(_outerId, _columnIds) { + // Copy the given parameters + this._outerId = _outerId; + this._columnIds = _columnIds; + this._prototypes = {}; + this._template = null; + this._mgrs = null; + this._rootWidget = null; + // Create the default row "prototypes" + this._createFullRowPrototype(); + this._createDefaultPrototype(); + this._createEmptyPrototype(); + this._createLoadingPrototype(); + } + et2_dataview_rowProvider.prototype.destroy = function () { + this._template = null; + this._mgrs = null; + this._rootWidget = null; + this._prototypes = {}; + this._columnIds = []; + }; + et2_dataview_rowProvider.prototype.getColumnCount = function () { + return this._columnIds.length; + }; + /** + * Returns a clone of the prototype with the given name. If the generator + * callback function is given, this function is called if the prototype + * does not yet registered. + * + * @param {string} _name + * @param {function} _generator + * @param {object} _context + */ + et2_dataview_rowProvider.prototype.getPrototype = function (_name, _generator, _context) { + if (typeof this._prototypes[_name] == "undefined") { + if (typeof _generator != "undefined") { + this._prototypes[_name] = _generator.call(_context, this._outerId, this._columnIds); + } + else { + return null; + } + } + return this._prototypes[_name].clone(); + }; + /* ---- PRIVATE FUNCTIONS ---- */ + et2_dataview_rowProvider.prototype._createFullRowPrototype = function () { + var tr = jQuery(document.createElement("tr")); + var td = jQuery(document.createElement("td")) + .addClass(this._outerId + "_td_fullRow") + .attr("colspan", this._columnIds.length) + .appendTo(tr); + var div = jQuery(document.createElement("div")) + .addClass(this._outerId + "_div_fullRow") + .appendTo(td); + this._prototypes["fullRow"] = tr; + }; + et2_dataview_rowProvider.prototype._createDefaultPrototype = function () { + var tr = jQuery(document.createElement("tr")); + // Append a td for each column + for (var i = 0; i < this._columnIds.length; i++) { + var td = jQuery(document.createElement("td")) + .addClass(this._outerId + "_td_" + this._columnIds[i]) + .appendTo(tr); + var div = jQuery(document.createElement("div")) + .addClass(this._outerId + "_div_" + this._columnIds[i]) + .addClass("innerContainer") + .appendTo(td); + } + this._prototypes["default"] = tr; + }; + et2_dataview_rowProvider.prototype._createEmptyPrototype = function () { + this._prototypes["empty"] = jQuery(document.createElement("tr")); + }; + et2_dataview_rowProvider.prototype._createLoadingPrototype = function () { + var fullRow = this.getPrototype("fullRow"); + jQuery("div", fullRow).addClass("loading"); + this._prototypes["loading"] = fullRow; + }; + return et2_dataview_rowProvider; +}()); +exports.et2_dataview_rowProvider = et2_dataview_rowProvider; +//# sourceMappingURL=et2_dataview_view_rowProvider.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_dataview_view_rowProvider.ts b/api/js/etemplate/et2_dataview_view_rowProvider.ts new file mode 100644 index 0000000000..d5d182a79d --- /dev/null +++ b/api/js/etemplate/et2_dataview_view_rowProvider.ts @@ -0,0 +1,148 @@ +/** + * EGroupware eTemplate2 - Class which contains a factory method for rows + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage dataview + * @link http://www.egroupware.org + * @author Andreas Stöckel + * @copyright Stylite 2011 + * @version $Id$ + */ + +/*egw:uses + /vendor/bower-asset/jquery/dist/jquery.js; + et2_core_inheritance; + et2_core_interfaces; + et2_core_arrayMgr; + et2_core_widget; +*/ + +/** + * The row provider contains prototypes (full clonable dom-trees) + * for all registered row types. + */ +export class et2_dataview_rowProvider +{ + private _outerId: any; + private _columnIds: any; + private _prototypes: {}; + private _template: null; + private _mgrs: null; + private _rootWidget: null; + /** + * + * @param _outerId + * @param _columnIds + */ + constructor( _outerId, _columnIds) + { + // Copy the given parameters + this._outerId = _outerId; + this._columnIds = _columnIds; + this._prototypes = {}; + + this._template = null; + this._mgrs = null; + this._rootWidget = null; + + // Create the default row "prototypes" + this._createFullRowPrototype(); + this._createDefaultPrototype(); + this._createEmptyPrototype(); + this._createLoadingPrototype(); + } + + public destroy() + { + this._template = null; + this._mgrs = null; + this._rootWidget = null; + this._prototypes = {}; + this._columnIds = []; + } + + public getColumnCount() + { + return this._columnIds.length; + } + + /** + * Returns a clone of the prototype with the given name. If the generator + * callback function is given, this function is called if the prototype + * does not yet registered. + * + * @param {string} _name + * @param {function} _generator + * @param {object} _context + */ + getPrototype( _name : string, _generator? : Function, _context? : any) + { + if (typeof this._prototypes[_name] == "undefined") + { + if (typeof _generator != "undefined") + { + this._prototypes[_name] = _generator.call(_context, this._outerId, + this._columnIds); + } + else + { + return null; + } + } + + return this._prototypes[_name].clone(); + } + + + /* ---- PRIVATE FUNCTIONS ---- */ + + + _createFullRowPrototype( ) + { + var tr = jQuery(document.createElement("tr")); + var td = jQuery(document.createElement("td")) + .addClass(this._outerId + "_td_fullRow") + .attr("colspan", this._columnIds.length) + .appendTo(tr); + var div = jQuery(document.createElement("div")) + .addClass(this._outerId + "_div_fullRow") + .appendTo(td); + + this._prototypes["fullRow"] = tr; + } + + _createDefaultPrototype( ) + { + var tr = jQuery(document.createElement("tr")); + + // Append a td for each column + for (var i = 0; i < this._columnIds.length; i++) + { + var td = jQuery(document.createElement("td")) + .addClass(this._outerId + "_td_" + this._columnIds[i]) + .appendTo(tr); + var div = jQuery(document.createElement("div")) + .addClass(this._outerId + "_div_" + this._columnIds[i]) + .addClass("innerContainer") + .appendTo(td); + } + + this._prototypes["default"] = tr; + } + + _createEmptyPrototype( ) + { + this._prototypes["empty"] = jQuery(document.createElement("tr")); + } + + _createLoadingPrototype( ) + { + var fullRow = this.getPrototype("fullRow"); + jQuery("div", fullRow).addClass("loading"); + + this._prototypes["loading"] = fullRow; + } + +} + diff --git a/api/js/etemplate/et2_dataview_view_spacer.js b/api/js/etemplate/et2_dataview_view_spacer.js index 2dc1cdf588..f92d344cb6 100644 --- a/api/js/etemplate/et2_dataview_view_spacer.js +++ b/api/js/etemplate/et2_dataview_view_spacer.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - Class which contains the spacer container * @@ -9,97 +10,94 @@ * @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 - /vendor/bower-asset/jquery/dist/jquery.js; - et2_dataview_view_container; + /vendor/bower-asset/jquery/dist/jquery.js; + et2_dataview_view_container; */ - /** * @augments et2_dataview_container */ -var et2_dataview_spacer = (function(){ "use strict"; return et2_dataview_container.extend( -{ - /** - * Constructor - * - * @param _parent - * @param _rowProvider - * @memberOf et2_dataview_spacer - */ - init: function (_parent, _rowProvider) { - // Call the inherited container constructor - this._super(_parent); - - // Initialize the row count and the row height - this._count = 0; - this._rowHeight = 19; - this._avgSum = 0; - this._avgCount = 0; - - // Get the spacer row and append it to the container - this.spacerNode = _rowProvider.getPrototype("spacer", - this._createSpacerPrototype, this); - this._phDiv = jQuery("td", this.spacerNode); - this.appendNode(this.spacerNode); - }, - - setCount: function (_count, _rowHeight) { - // Set the new count and _rowHeight if given - this._count = _count; - if (typeof _rowHeight !== "undefined") - { - this._rowHeight = _rowHeight; - } - - // Update the element height - this._phDiv.height(this._count * this._rowHeight); - - // Call the invalidate function - this.invalidate(); - }, - - getCount: function () { - return this._count; - }, - - getHeight: function () { - // Set the calculated height, so that "invalidate" will work correctly - this._height = this._count * this._rowHeight; - - return this._height; - }, - - getAvgHeightData: function () { - if (this._avgCount > 0) - { - return { - "avgHeight": this._avgSum / this._avgCount, - "avgCount": this._avgCount - }; - } - - return null; - }, - - addAvgHeight: function (_height) { - this._avgSum += _height; - this._avgCount++; - }, - - /* ---- PRIVATE FUNCTIONS ---- */ - - _createSpacerPrototype: function (_outerId, _columnIds) { - var tr = jQuery(document.createElement("tr")); - - var td = jQuery(document.createElement("td")) - .addClass("egwGridView_spacer") - .addClass(_outerId + "_spacer_fullRow") - .attr("colspan", _columnIds.length) - .appendTo(tr); - - return tr; - } - -});}).call(this); - +var et2_dataview_spacer = /** @class */ (function (_super) { + __extends(et2_dataview_spacer, _super); + /** + * Constructor + * + * @param _parent + * @param _rowProvider + * @memberOf et2_dataview_spacer + */ + function et2_dataview_spacer(_parent, _rowProvider) { + var _this = + // Call the inherited container constructor + _super.call(this, _parent) || this; + // Initialize the row count and the row height + _this._count = 0; + _this._rowHeight = 19; + _this._avgSum = 0; + _this._avgCount = 0; + // Get the spacer row and append it to the container + _this.spacerNode = _rowProvider.getPrototype("spacer", _this._createSpacerPrototype, _this); + _this._phDiv = jQuery("td", _this.spacerNode); + _this.appendNode(_this.spacerNode); + return _this; + } + et2_dataview_spacer.prototype.setCount = function (_count, _rowHeight) { + // Set the new count and _rowHeight if given + this._count = _count; + if (typeof _rowHeight !== "undefined") { + this._rowHeight = _rowHeight; + } + // Update the element height + this._phDiv.height(this._count * this._rowHeight); + // Call the invalidate function + this.invalidate(); + }; + et2_dataview_spacer.prototype.getCount = function () { + return this._count; + }; + et2_dataview_spacer.prototype.getHeight = function () { + // Set the calculated height, so that "invalidate" will work correctly + this._height = this._count * this._rowHeight; + return this._height; + }; + et2_dataview_spacer.prototype.getAvgHeightData = function () { + if (this._avgCount > 0) { + return { + "avgHeight": this._avgSum / this._avgCount, + "avgCount": this._avgCount + }; + } + return null; + }; + et2_dataview_spacer.prototype.addAvgHeight = function (_height) { + this._avgSum += _height; + this._avgCount++; + }; + /* ---- PRIVATE FUNCTIONS ---- */ + et2_dataview_spacer.prototype._createSpacerPrototype = function (_outerId, _columnIds) { + var tr = jQuery(document.createElement("tr")); + var td = jQuery(document.createElement("td")) + .addClass("egwGridView_spacer") + .addClass(_outerId + "_spacer_fullRow") + .attr("colspan", _columnIds.length) + .appendTo(tr); + return tr; + }; + return et2_dataview_spacer; +}(et2_dataview_container)); +exports.et2_dataview_spacer = et2_dataview_spacer; +//# sourceMappingURL=et2_dataview_view_spacer.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_dataview_view_spacer.ts b/api/js/etemplate/et2_dataview_view_spacer.ts new file mode 100644 index 0000000000..a08b043fea --- /dev/null +++ b/api/js/etemplate/et2_dataview_view_spacer.ts @@ -0,0 +1,112 @@ +/** + * EGroupware eTemplate2 - Class which contains the spacer container + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage dataview + * @link http://www.egroupware.org + * @author Andreas Stöckel + * @copyright Stylite 2011 + * @version $Id$ + */ + +/*egw:uses + /vendor/bower-asset/jquery/dist/jquery.js; + et2_dataview_view_container; +*/ + +/** + * @augments et2_dataview_container + */ +export class et2_dataview_spacer extends et2_dataview_container +{ + /** + * Constructor + * + * @param _parent + * @param _rowProvider + * @memberOf et2_dataview_spacer + */ + constructor( _parent, _rowProvider) + { + // Call the inherited container constructor + super(_parent); + + // Initialize the row count and the row height + this._count = 0; + this._rowHeight = 19; + this._avgSum = 0; + this._avgCount = 0; + + // Get the spacer row and append it to the container + this.spacerNode = _rowProvider.getPrototype("spacer", + this._createSpacerPrototype, this); + this._phDiv = jQuery("td", this.spacerNode); + this.appendNode(this.spacerNode); + } + + setCount( _count, _rowHeight) + { + // Set the new count and _rowHeight if given + this._count = _count; + if (typeof _rowHeight !== "undefined") + { + this._rowHeight = _rowHeight; + } + + // Update the element height + this._phDiv.height(this._count * this._rowHeight); + + // Call the invalidate function + this.invalidate(); + } + + getCount( ) + { + return this._count; + } + + getHeight( ) + { + // Set the calculated height, so that "invalidate" will work correctly + this._height = this._count * this._rowHeight; + + return this._height; + } + + getAvgHeightData( ) + { + if (this._avgCount > 0) + { + return { + "avgHeight": this._avgSum / this._avgCount, + "avgCount": this._avgCount + }; + } + + return null; + } + + addAvgHeight( _height) + { + this._avgSum += _height; + this._avgCount++; + } + + /* ---- PRIVATE FUNCTIONS ---- */ + + _createSpacerPrototype( _outerId, _columnIds) + { + var tr = jQuery(document.createElement("tr")); + + var td = jQuery(document.createElement("td")) + .addClass("egwGridView_spacer") + .addClass(_outerId + "_spacer_fullRow") + .attr("colspan", _columnIds.length) + .appendTo(tr); + + return tr; + } + +} + diff --git a/api/js/etemplate/et2_dataview_view_tile.js b/api/js/etemplate/et2_dataview_view_tile.js index 2360759e06..41154e3160 100644 --- a/api/js/etemplate/et2_dataview_view_tile.js +++ b/api/js/etemplate/et2_dataview_view_tile.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - dataview code * @@ -9,96 +10,99 @@ * @copyright Nathan Gray 2014 * @version $Id: et2_dataview_view_container_1.js 46338 2014-03-20 09:40:37Z ralfbecker $ */ - +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 - /vendor/bower-asset/jquery/dist/jquery.js; - et2_dataview_interfaces; + /vendor/bower-asset/jquery/dist/jquery.js; + et2_dataview_interfaces; */ - /** * Displays tiles or thumbnails (squares) instead of full rows. * * It's important that the template specifies a fixed width and height (via CSS) * so that the rows and columns work out properly. * - * @augments et2_dataview_container */ -var et2_dataview_tile = (function(){ "use strict"; return et2_dataview_row.extend([], -{ - columns: 4, - - /** - * Creates the row container. Use the "setRow" function to load the actual - * row content. - * - * @param _parent is the row parent container. - * @memberOf et2_dataview_row - */ - init: function(_parent) { - // Call the inherited constructor - this._super(_parent); - - // Make sure the needed class is there to get the CSS - this.tr.addClass('tile'); - }, - - makeExpandable: function (_expandable, _callback, _context) { - // Nope. It mostly works, it's just weird. - }, - - getAvgHeightData: function() { - var res = { - "avgHeight": this.getHeight() / this.columns, - "avgCount": this.columns - }; - return res; - }, - - /** - * Returns the height for the tile. - * - * This is where we do the magic. If a new row should start, we return the proper - * height. If this should be another tile in the same row, we say it has 0 height. - * @returns {Number} - */ - getHeight: function() { - if(this._index % this.columns == 0) - { - return this._super(); - } - else - { - return 0; - } - }, - - /** - * Broadcasts an invalidation through the container tree. Marks the own - * height as invalid. - */ - invalidate: function() { - if(this._inTree && this.tr) - { - var template_width = jQuery('.innerContainer',this.tr).children().outerWidth(true); - if(template_width) - { - - this.tr.css('width', template_width + (this.tr.outerWidth(true) - this.tr.width())); - } - } - this._recalculate_columns(); - this._super(); - }, - - /** - * Recalculate how many columns we can fit in a row. - * While browser takes care of the actual layout, we need this for proper - * pagination. - */ - _recalculate_columns: function() { - if(this._inTree && this.tr && this.tr.parent()) - { - this.columns = Math.max(1,parseInt(this.tr.parent().innerWidth() / this.tr.outerWidth(true))); - } - } -});}).call(this); +var et2_dataview_tile = /** @class */ (function (_super) { + __extends(et2_dataview_tile, _super); + /** + * Creates the row container. Use the "setRow" function to load the actual + * row content. + * + * @param _parent is the row parent container. + * @memberOf et2_dataview_row + */ + function et2_dataview_tile(_parent) { + var _this = + // Call the inherited constructor + _super.call(this, _parent) || this; + _this.columns = 4; + // Make sure the needed class is there to get the CSS + _this.tr.addClass('tile'); + return _this; + } + et2_dataview_tile.prototype.makeExpandable = function (_expandable, _callback, _context) { + // Nope. It mostly works, it's just weird. + }; + et2_dataview_tile.prototype.getAvgHeightData = function () { + var res = { + "avgHeight": this.getHeight() / this.columns, + "avgCount": this.columns + }; + return res; + }; + /** + * Returns the height for the tile. + * + * This is where we do the magic. If a new row should start, we return the proper + * height. If this should be another tile in the same row, we say it has 0 height. + * @returns {Number} + */ + et2_dataview_tile.prototype.getHeight = function () { + if (this._index % this.columns == 0) { + return _super.prototype.getHeight.call(this); + } + else { + return 0; + } + }; + /** + * Broadcasts an invalidation through the container tree. Marks the own + * height as invalid. + */ + et2_dataview_tile.prototype.invalidate = function () { + if (this._inTree && this.tr) { + var template_width = jQuery('.innerContainer', this.tr).children().outerWidth(true); + if (template_width) { + this.tr.css('width', template_width + (this.tr.outerWidth(true) - this.tr.width())); + } + } + this._recalculate_columns(); + _super.prototype.invalidate.call(this); + }; + /** + * Recalculate how many columns we can fit in a row. + * While browser takes care of the actual layout, we need this for proper + * pagination. + */ + et2_dataview_tile.prototype._recalculate_columns = function () { + if (this._inTree && this.tr && this.tr.parent()) { + this.columns = Math.max(1, parseInt(this.tr.parent().innerWidth() / this.tr.outerWidth(true))); + } + }; + return et2_dataview_tile; +}(et2_dataview_row)); +exports.et2_dataview_tile = et2_dataview_tile; +//# sourceMappingURL=et2_dataview_view_tile.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_dataview_view_tile.ts b/api/js/etemplate/et2_dataview_view_tile.ts new file mode 100644 index 0000000000..32d25a40fc --- /dev/null +++ b/api/js/etemplate/et2_dataview_view_tile.ts @@ -0,0 +1,107 @@ +/** + * EGroupware eTemplate2 - dataview code + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage dataview + * @link http://www.egroupware.org + * @author Nathan Gray + * @copyright Nathan Gray 2014 + * @version $Id: et2_dataview_view_container_1.js 46338 2014-03-20 09:40:37Z ralfbecker $ + */ + +/*egw:uses + /vendor/bower-asset/jquery/dist/jquery.js; + et2_dataview_interfaces; +*/ + +/** + * Displays tiles or thumbnails (squares) instead of full rows. + * + * It's important that the template specifies a fixed width and height (via CSS) + * so that the rows and columns work out properly. + * + */ +export class et2_dataview_tile extends et2_dataview_row { + columns: number = 4; + + /** + * Creates the row container. Use the "setRow" function to load the actual + * row content. + * + * @param _parent is the row parent container. + * @memberOf et2_dataview_row + */ + constructor(_parent) + { + // Call the inherited constructor + super(_parent); + + // Make sure the needed class is there to get the CSS + this.tr.addClass('tile'); + } + + makeExpandable(_expandable, _callback, _context) + { + // Nope. It mostly works, it's just weird. + } + + getAvgHeightData() + { + var res = { + "avgHeight": this.getHeight() / this.columns, + "avgCount": this.columns + }; + return res; + } + + /** + * Returns the height for the tile. + * + * This is where we do the magic. If a new row should start, we return the proper + * height. If this should be another tile in the same row, we say it has 0 height. + * @returns {Number} + */ + getHeight() + { + if (this._index % this.columns == 0) + { + return super.getHeight(); + } + else + { + return 0; + } + } + + /** + * Broadcasts an invalidation through the container tree. Marks the own + * height as invalid. + */ + invalidate() + { + if (this._inTree && this.tr) + { + var template_width = jQuery('.innerContainer', this.tr).children().outerWidth(true); + if (template_width) + { + this.tr.css('width', template_width + (this.tr.outerWidth(true) - this.tr.width())); + } + } + this._recalculate_columns(); + super.invalidate(); + } + + /** + * Recalculate how many columns we can fit in a row. + * While browser takes care of the actual layout, we need this for proper + * pagination. + */ + _recalculate_columns() + { + if (this._inTree && this.tr && this.tr.parent()) + { + this.columns = Math.max(1, parseInt(this.tr.parent().innerWidth() / this.tr.outerWidth(true))); + } + } +} diff --git a/api/js/etemplate/et2_extension_customfields.js b/api/js/etemplate/et2_extension_customfields.js index 9ea0b874f1..3d962749f6 100644 --- a/api/js/etemplate/et2_extension_customfields.js +++ b/api/js/etemplate/et2_extension_customfields.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - JS Custom fields object * @@ -9,721 +10,601 @@ * @copyright Nathan Gray 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 - lib/tooltip; - /vendor/bower-asset/jquery/dist/jquery.js; - et2_core_xml; - et2_core_DOMWidget; - et2_core_inputWidget; + lib/tooltip; + /vendor/bower-asset/jquery/dist/jquery.js; + et2_core_xml; + et2_core_DOMWidget; + et2_core_inputWidget; */ - -/** - * @augments et2_dataview - */ -var et2_customfields_list = (function(){ "use strict"; return et2_valueWidget.extend([et2_IDetachedDOM, et2_IInput], -{ - attributes: { - 'customfields': { - 'name': 'Custom fields', - 'description': 'Auto filled', - 'type': 'any' - }, - 'fields': { - 'name': 'Custom fields', - 'description': 'Auto filled', - 'type': 'any' - }, - 'value': { - 'name': 'Custom fields', - 'description': 'Auto filled', - 'type': "any" - }, - 'type_filter': { - 'name': 'Field filter', - "default": "", - "type": "any", // String or array - "description": "Filter displayed custom fields by their 'type2' attribute" - }, - 'private': { - ignore: true, - type: 'boolean' - }, - 'sub_app': { - 'name': 'sub app name', - 'type': "string", - 'description': "Name of sub application" - }, - // Allow onchange so you can put handlers on the sub-widgets - 'onchange': { - "name": "onchange", - "type": "string", - "default": et2_no_init, - "description": "JS code which is executed when the value changes." - } - }, - - legacyOptions: ["type_filter","private", "fields"], // Field restriction & private done server-side - - prefix: '#', - - DEFAULT_ID: "custom_fields", - - /** - * Constructor - * - * @memberOf et2_customfields_list - */ - init: function() { - // Some apps (infolog edit) don't give ID, so assign one to get settings - if(!arguments[1].id) arguments[1].id = this.DEFAULT_ID; - - this._super.apply(this, arguments); - - // Allows server side to override prefix - not an attribute though - if(typeof this.options.prefix != 'undefined') this.prefix = this.options.prefix; - - // Create the table body and the table - this.tbody = jQuery(document.createElement("tbody")); - this.table = jQuery(document.createElement("table")) - .addClass("et2_grid et2_customfield_list"); - this.table.append(this.tbody); - - this.rows = {}; - this.widgets = {}; - this.detachedNodes = []; - if(!this.options.fields) this.options.fields = {}; - if(typeof this.options.fields === 'string') - { - var fields = this.options.fields.split(','); - this.options.fields = {}; - for(var i = 0; i < fields.length; i++) - { - this.options.fields[fields[i]] = true; - } - } - - if(this.options.type_filter && typeof this.options.type_filter == "string") - { - this.options.type_filter = this.options.type_filter.split(","); - } - if(this.options.type_filter) - { - var already_filtered = !jQuery.isEmptyObject(this.options.fields); - for(var field_name in this.options.customfields) - { - // Already excluded? - if(already_filtered && !this.options.fields[field_name]) continue; - - if(!this.options.customfields[field_name].type2 || this.options.customfields[field_name].type2.length == 0 || - this.options.customfields[field_name].type2 == '0') - { - // No restrictions - this.options.fields[field_name] = true; - continue; - } - var types = typeof this.options.customfields[field_name].type2 == 'string' ? this.options.customfields[field_name].type2.split(",") : this.options.customfields[field_name].type2; - this.options.fields[field_name] = false; - for(var i = 0; i < types.length; i++) - { - if(jQuery.inArray(types[i],this.options.type_filter) > -1) - { - this.options.fields[field_name] = true; - continue; - } - } - } - } - - this.setDOMNode(this.table[0]); - }, - - destroy: function() { - this._super.apply(this, arguments); - this.rows = {}; - this.widgets = {}; - this.detachedNodes = []; - this.tbody = null; - }, - - /** - * What does this do? I don't know, but when everything is done the second - * time, this makes it work. Otherwise, all custom fields are lost. - */ - assign: function(_obj) { - this.loadFields(); - }, - - getDOMNode: function(_sender) { - - // Check whether the _sender object exists inside the management array - if(this.rows && _sender.id && this.rows[_sender.id]) - { - return this.rows[_sender.id]; - } - - return this._super.apply(this, arguments); - }, - - /** - * Initialize widgets for custom fields - */ - loadFields: function() { - if(!this.options || !this.options.customfields) return; - - // Already set up - avoid duplicates in nextmatch - if(this._type == 'customfields-list' && !this.isInTree()) return; - if(!jQuery.isEmptyObject(this.widgets)) return; - - // Check for global setting changes (visibility) - var global_data = this.getArrayMgr("modifications").getRoot().getEntry('~custom_fields~'); - if(global_data && global_data.fields && !this.options.fields) this.options.fields = global_data.fields; - - // For checking app entries - var apps = this.egw().link_app_list(); - - // Create the table rows - for(var field_name in this.options.customfields) - { - // Skip fields if we're filtering - if(this._type != 'customfields-list' && !jQuery.isEmptyObject(this.options.fields) && !this.options.fields[field_name]) continue; - - var field = this.options.customfields[field_name]; - - var id = this.prefix+field_name; - - // Need curlies around ID for nm row expansion - if(this.id == '$row') - { - id = "{" + this.id + "}" + "["+this.prefix + field_name+"]"; - } - else if (this.id != this.DEFAULT_ID) - { - // Prefix this ID to avoid potential ID collisions - id = this.id + "["+id+"]"; - } - - // Avoid creating field twice - if(!this.rows[id]) - { - - var row = jQuery(document.createElement("tr")) - .appendTo(this.tbody) - .addClass(this.id+'_'+id); - var cf = jQuery(document.createElement("td")) - .appendTo(row); - if(!field.type) field.type = 'text";' - var setup_function = '_setup_'+(apps[field.type] ? 'link_entry' : field.type.replace("-","_")); - - var attrs = { - 'id': id, - 'statustext': field.help, - 'needed': field.needed, - 'readonly': this.getArrayMgr("readonlys").isReadOnly(id, null, this.options.readonly), - 'value': this.options.value[this.prefix+field_name] - }; - // Can't have a required readonly, it will warn & be removed later, so avoid the warning - if(attrs.readonly === true) delete attrs.needed; - - if(this.options.onchange) - { - attrs.onchange = this.options.onchange; - } - - if(this[setup_function]) { - var no_skip = this[setup_function].call(this, field_name, field, attrs); - if(!no_skip) continue; - } - - if(this._type == 'customfields-list') { - // No label, cust widget - attrs.readonly = true; - // Widget tooltips don't work in nextmatch because of the creation / binding separation - // Set title to field label so browser will show something - // Add field label & help as data attribute to row, so it can be stylied with CSS (title should be disabled) - row.attr('title', field.label); - row.attr('data-label', field.label); - row.attr('data-field', field_name); - row.attr('data-help', field.help); - this.detachedNodes.push(row[0]); - } - else - { - // Label in first column, widget in 2nd - cf.text(field.label + ""); - cf = jQuery(document.createElement("td")) - .appendTo(row); - } - this.rows[id] = cf[0]; - - // Set any additional attributes set in options, but not for widgets that pass actual options - if(['select','radio','radiogroup','checkbox','button'].indexOf(field.type) == -1 && !jQuery.isEmptyObject(field.values)) - { - var w = et2_registry[attrs.type ? attrs.type : field.type]; - for(var attr_name in field.values) - { - if (typeof w.prototype.attributes[attr_name] != "undefined") - attrs[attr_name] = field.values[attr_name]; - } - } - // Create widget - var widget = this.widgets[field_name] = et2_createWidget(attrs.type ? attrs.type : field.type, attrs, this); - } - - // Field is not to be shown - if(!this.options.fields || jQuery.isEmptyObject(this.options.fields) || this.options.fields[field_name] == true) - { - jQuery(this.rows[field_name]).show(); - } - else - { - jQuery(this.rows[field_name]).hide(); - } - - } - }, - - /** - * Read needed info on available custom fields from various places it's stored. - */ - transformAttributes: function(_attrs) { - this._super.apply(this, arguments); - - // Add in settings that are objects - - // Customized settings for this widget (unlikely) - var data = this.getArrayMgr("modifications").getEntry(this.id); - // Check for global settings - var global_data = this.getArrayMgr("modifications").getRoot().getEntry('~custom_fields~', true); - if(global_data) - { - for(var key in data) - { - // Don't overwrite fields with global values - if(global_data[key] && key !== 'fields') - { - data[key] = jQuery.extend(true, {}, data[key], global_data[key]); - } - } - } - for(var key in data) - { - _attrs[key] = data[key]; - } - for(var key in global_data) - { - if(typeof global_data[key] != 'undefined' && ! _attrs[key]) _attrs[key] = global_data[key]; - } - - if (this.id) - { - // Set the value for this element - var contentMgr = this.getArrayMgr("content"); - if (contentMgr != null) { - var val = contentMgr.getEntry(this.id); - _attrs["value"] = {}; - if (val !== null) - { - if(this.id.indexOf(this.prefix) === 0 && typeof data.fields != 'undefined' && data.fields[this.id.replace(this.prefix,'')] === true) - { - _attrs['value'][this.id] = val; - } - else - { - // Only set the values that match desired custom fields - for(var key in val) - { - if(key.indexOf(this.prefix) == 0) { - _attrs["value"][key] = val[key]; - } - } - } - //_attrs["value"] = val; - } - else - { - // Check for custom fields directly in record - for(var key in _attrs.customfields) - { - _attrs["value"][this.prefix + key] = contentMgr.getEntry(this.prefix + key); - } - } - } - } - }, - - loadFromXML: function(_node) { - this.loadFields(); - - // Load the nodes as usual - this._super.apply(this, arguments); - }, - - set_value: function(_value) { - if(!this.options.customfields) return; - for(var field_name in this.options.customfields) - { - // Skip fields if we're filtering - if(!jQuery.isEmptyObject(this.options.fields) && !this.options.fields[field_name]) continue; - - // Make sure widget is created, and has the needed function - if(!this.widgets[field_name] || !this.widgets[field_name].set_value) continue; - var value = _value[this.prefix + field_name] ? _value[this.prefix + field_name] : null; - - // Check if ID was missing - if(value == null && this.id == this.DEFAULT_ID && this.getArrayMgr("content").getEntry(this.prefix + field_name)) - { - value = this.getArrayMgr("content").getEntry(this.prefix + field_name); - } - - switch(this.options.customfields[field_name].type) - { - case 'date': - // Date custom fields are always in Y-m-d, which seldom matches user's preference - // which fails when sent to date widget. This is only used for nm rows, when possible - // this is fixed server side - if(value && isNaN(value)) - { - value = jQuery.datepicker.parseDate("yy-mm-dd",value); - } - break; - } - this.widgets[field_name].set_value(value); - } - }, - - /** - * et2_IInput so the custom field can be it's own widget. - */ - getValue: function() { - // Not using an ID means we have to grab all the widget values, and put them where server knows to look - if(this.id != this.DEFAULT_ID) - { - return null; - } - var value = {}; - for(var field_name in this.widgets) - { - if(this.widgets[field_name].getValue && !this.widgets[field_name].options.readonly) - { - value[this.prefix + field_name] = this.widgets[field_name].getValue(); - } - } - return value; - }, - - isDirty: function() { - var dirty = true; - for(var field_name in this.widgets) - { - if(this.widgets[field_name].isDirty) - { - dirty = dirty && this.widgets[field_name].isDirty(); - } - } - return dirty; - }, - - resetDirty: function() { - for(var field_name in this.widgets) - { - if(this.widgets[field_name].resetDirty) - { - this.widgets[field_name].resetDirty(); - } - } - }, - - isValid: function() { - // Individual customfields will handle themselves - return true; - }, - - /** - * Adapt provided attributes to match options for widget - * - * rows > 1 --> textarea, with rows=rows and cols=len - * !rows --> input, with size=len - * rows = 1 --> input, with size=len, maxlength=len - */ - _setup_text: function(field_name, field, attrs) { - // No label on the widget itself - delete(attrs.label); - - field.type = 'textbox'; - attrs.rows = field.rows > 1 ? field.rows : null; - - if(field.len) - { - attrs.size = field.len; - if (field.rows == 1) attrs.maxlength = field.len; - } - return true; - }, - _setup_ajax_select: function(field_name, field, attrs) { - var attributes = ['get_rows','get_title','id_field','template']; - if(field.values) - { - for(var i = 0; i < attributes.length; i++) - { - if(typeof field.values[attributes[i]] !== 'undefined') - { - attrs[attributes[i]] = field.values[attributes[i]]; - } - } - } - return true; - }, - _setup_float: function(field_name, field, attrs) { - // No label on the widget itself - delete(attrs.label); - - field.type = 'float'; - - if(field.len) - { - attrs.size = field.len; - } - return true; - }, - _setup_select: function(field_name, field, attrs) { - // No label on the widget itself - delete(attrs.label); - - attrs.rows = field.rows; - // select_options are now send from server-side incl. ones defined via a file in EGroupware root - attrs.tags = field.tags; - - return true; - }, - _setup_select_account: function(field_name, field, attrs) { - attrs.empty_label = egw.lang('Select'); - if(field.account_type) - { - attrs.account_type = field.account_type; - } - return this._setup_select(field_name, field, attrs); - }, - - _setup_date: function(field_name, field, attrs) { - attrs.data_format = field.values && field.values.format ? field.values.format : 'Y-m-d'; - return true; - }, - _setup_date_time: function(field_name, field, attrs) { - attrs.data_format = field.values && field.values.format ? field.values.format : 'Y-m-d H:i:s'; - return true; - }, - _setup_htmlarea: function(field_name, field, attrs) { - attrs.config = field.config ? field.config : {}; - attrs.config.toolbarStartupExpanded = false; - if(field.len) - { - attrs.config.width = field.len+'px'; - } - attrs.config.height = (((field.rows > 0 && field.rows !='undefined') ? field.rows : 5) *16) +'px'; - - // We have to push the config modifications into the modifications array, or they'll - // be overwritten by the site config from the server - var data = this.getArrayMgr("modifications").getEntry(this.prefix+field_name); - if(data) jQuery.extend(data.config, attrs.config); - - return true; - }, - _setup_radio: function(field_name, field, attrs) { - // 'Empty' label will be first - delete(attrs.label); - - if(field.values && field.values['']) - { - attrs.label = field.values['']; - delete field.values['']; - } - - field.type = 'radiogroup'; - attrs.options = field.values; - return true; - }, - - _setup_checkbox: function(field_name, field, attrs) { - // Read-only checkbox is just text - if(attrs.readonly) - { - attrs.ro_true = field.label; - } - return true; - }, - - /** - * People set button attributes as - * label: javascript - */ - _setup_button: function(field_name, field, attrs) { - // No label on the widget itself - delete(attrs.label); - - attrs.label = field.label; - - if (this._type == 'customfields-list') - { - // No buttons in a list, it causes problems with detached nodes - return false; - } - // Simple case, one widget for a custom field - if(!field.values || typeof field.values != 'object' || Object.keys(field.values).length == 1) - { - for(var key in field.values) - { - attrs.label = key; - attrs.onclick = field.values[key]; - } - if (!attrs.label) - { - attrs.label = 'No "label=onclick" in values!'; - attrs.onclick = function(){ return false; }; - } - return !attrs.readonly; - } - else - { - // Complicated case, a single custom field you get multiple widgets - // Handle it all here, since this is the exception - var row = jQuery('tr',this.tbody).last(); - var cf = jQuery('td',row); - // Label in first column, widget in 2nd - cf.text(field.label + ""); - cf = jQuery(document.createElement("td")) - .appendTo(row); - - for(var key in field.values) - { - var button_attrs = jQuery.extend({},attrs); - button_attrs.label = key; - button_attrs.onclick = field.values[key]; - button_attrs.id = attrs.id + '_' + key; - - // This controls where the button is placed in the DOM - this.rows[button_attrs.id] = cf[0]; - - // Do not store in the widgets list, one name for multiple widgets would cause problems - /*this.widgets[field_name] = */ et2_createWidget(attrs.type ? attrs.type : field.type, button_attrs, this); - } - return false; - } - }, - _setup_link_entry: function(field_name, field, attrs) { - if(field.type === 'filemanager') - { - return this._setup_filemanager(field_name, field, attrs); - } - // No label on the widget itself - delete(attrs.label); - - attrs.type = "link-entry"; - attrs.only_app = field.type; - return true; - }, - - _setup_filemanager: function(field_name, field, attrs) { - attrs.type = 'vfs-upload'; - delete(attrs.label); - - if (this._type == 'customfields-list') - { - // No special UI needed? - return true; - } - else - { - // Complicated case, a single custom field you get multiple widgets - // Handle it all here, since this is the exception - var row = jQuery('tr',this.tbody).last(); - var cf = jQuery('td',row); - - // Label in first column, widget in 2nd - cf.text(field.label + ""); - cf = jQuery(document.createElement("td")) - .appendTo(row); - - // Create upload widget - var widget = this.widgets[field_name] = et2_createWidget(attrs.type ? attrs.type : field.type, attrs, this); - - // This controls where the widget is placed in the DOM - this.rows[attrs.id] = cf[0]; - jQuery(widget.getDOMNode(widget)).css('vertical-align','top'); - - // Add a link to existing VFS file - var select_attrs = jQuery.extend({}, - attrs, - // Filemanager select - { - label: '', - mode: widget.options.multiple ? 'open-multiple' : 'open', - method: 'EGroupware\\Api\\Etemplate\\Widget\\Link::ajax_link_existing', - method_id: attrs.path, - button_label: egw.lang('Link') - },{type: 'vfs-select'}); - select_attrs.id = attrs.id + '_vfs_select'; - - // This controls where the button is placed in the DOM - this.rows[select_attrs.id] = cf[0]; - - // Do not store in the widgets list, one name for multiple widgets would cause problems - widget = et2_createWidget(select_attrs.type, select_attrs, this); - jQuery(widget.getDOMNode(widget)).css('vertical-align','top').prependTo(cf); - } - return false; - }, - - /** - * Set which fields are visible, by name - * - * Note: no # prefix on the name - * - */ - set_visible: function(_fields) { - for(var name in _fields) - { - if(this.rows[this.prefix + name]) - { - if(_fields[name]) - { - jQuery(this.rows[this.prefix+name]).show(); - } - else - { - jQuery(this.rows[this.prefix+name]).hide(); - } - } - this.options.fields[name] = _fields[name]; - } - }, - - /** - * Code for implementing et2_IDetachedDOM - */ - getDetachedAttributes: function(_attrs) - { - _attrs.push("value", "class"); - }, - - getDetachedNodes: function() - { - return this.detachedNodes ? this.detachedNodes : []; - }, - - setDetachedAttributes: function(_nodes, _values) - { - // Individual widgets are detected and handled by the grid, but the interface is needed for this to happen - - // Show the row if there's a value, hide it if there is no value - for(var i = 0; i < _nodes.length; i++) - { - // toggle() needs a boolean to do what we want - var key = _nodes[i].getAttribute('data-field'); - jQuery(_nodes[i]).toggle(_values.fields[key] && _values.value[this.prefix + key]?true:false); - } - } -});}).call(this); - -et2_register_widget(et2_customfields_list, ["customfields", "customfields-list"]); - +var et2_core_widget_1 = require("./et2_core_widget"); +var et2_core_inheritance_1 = require("./et2_core_inheritance"); +var et2_core_valueWidget_1 = require("./et2_core_valueWidget"); +var et2_customfields_list = /** @class */ (function (_super) { + __extends(et2_customfields_list, _super); + function et2_customfields_list(_parent, _attrs, _child) { + var _this = _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_customfields_list._attributes, _child || {})) || this; + _this.legacyOptions = ["type_filter", "private", "fields"]; // Field restriction & private done server-side + _this.rows = {}; + _this.widgets = {}; + _this.detachedNodes = []; + // Some apps (infolog edit) don't give ID, so assign one to get settings + if (!_this.id) { + _this.id = _attrs.id = et2_customfields_list.DEFAULT_ID; + // 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); + } + // Create the table body and the table + _this.tbody = jQuery(document.createElement("tbody")); + _this.table = jQuery(document.createElement("table")) + .addClass("et2_grid et2_customfield_list"); + _this.table.append(_this.tbody); + if (!_this.options.fields) + _this.options.fields = {}; + if (typeof _this.options.fields === 'string') { + var fields = _this.options.fields.split(','); + _this.options.fields = {}; + for (var i = 0; i < fields.length; i++) { + _this.options.fields[fields[i]] = true; + } + } + if (_this.options.type_filter && typeof _this.options.type_filter == "string") { + _this.options.type_filter = _this.options.type_filter.split(","); + } + if (_this.options.type_filter) { + var already_filtered = !jQuery.isEmptyObject(_this.options.fields); + for (var field_name in _this.options.customfields) { + // Already excluded? + if (already_filtered && !_this.options.fields[field_name]) + continue; + if (!_this.options.customfields[field_name].type2 || _this.options.customfields[field_name].type2.length == 0 || + _this.options.customfields[field_name].type2 == '0') { + // No restrictions + _this.options.fields[field_name] = true; + continue; + } + var types = typeof _this.options.customfields[field_name].type2 == 'string' ? _this.options.customfields[field_name].type2.split(",") : _this.options.customfields[field_name].type2; + _this.options.fields[field_name] = false; + for (var i = 0; i < types.length; i++) { + if (jQuery.inArray(types[i], _this.options.type_filter) > -1) { + _this.options.fields[field_name] = true; + } + } + } + } + _this.setDOMNode(_this.table[0]); + return _this; + } + et2_customfields_list.prototype.destroy = function () { + _super.prototype.destroy.call(this); + this.rows = {}; + this.widgets = {}; + this.detachedNodes = []; + this.tbody = null; + }; + /** + * What does this do? I don't know, but when everything is done the second + * time, this makes it work. Otherwise, all custom fields are lost. + */ + et2_customfields_list.prototype.assign = function (_obj) { + this.loadFields(); + }; + et2_customfields_list.prototype.getDOMNode = function (_sender) { + // Check whether the _sender object exists inside the management array + if (this.rows && _sender.id && this.rows[_sender.id]) { + return this.rows[_sender.id]; + } + return _super.prototype.getDOMNode.call(this, _sender); + }; + /** + * Initialize widgets for custom fields + */ + et2_customfields_list.prototype.loadFields = function () { + if (!this.options || !this.options.customfields) + return; + // Already set up - avoid duplicates in nextmatch + if (this.getType() == 'customfields-list' && !this.isInTree()) + return; + if (!jQuery.isEmptyObject(this.widgets)) + return; + // Check for global setting changes (visibility) + var global_data = this.getArrayMgr("modifications").getRoot().getEntry('~custom_fields~'); + if (global_data && global_data.fields && !this.options.fields) + this.options.fields = global_data.fields; + // For checking app entries + var apps = this.egw().link_app_list(); + // Create the table rows + for (var field_name in this.options.customfields) { + // Skip fields if we're filtering + if (this.getType() != 'customfields-list' && !jQuery.isEmptyObject(this.options.fields) && !this.options.fields[field_name]) + continue; + var field = this.options.customfields[field_name]; + var id = et2_customfields_list.PREFIX + field_name; + // Need curlies around ID for nm row expansion + if (this.id == '$row') { + id = "{" + this.id + "}" + "[" + et2_customfields_list.PREFIX + field_name + "]"; + } + else if (this.id != et2_customfields_list.DEFAULT_ID) { + // Prefix this ID to avoid potential ID collisions + id = this.id + "[" + id + "]"; + } + // Avoid creating field twice + if (!this.rows[id]) { + var row = jQuery(document.createElement("tr")) + .appendTo(this.tbody) + .addClass(this.id + '_' + id); + var cf = jQuery(document.createElement("td")) + .appendTo(row); + if (!field.type) + field.type = 'text";'; + var setup_function = '_setup_' + (apps[field.type] ? 'link_entry' : field.type.replace("-", "_")); + var attrs = { + 'id': id, + 'statustext': field.help, + 'needed': field.needed, + 'readonly': this.getArrayMgr("readonlys").isReadOnly(id, null, this.options.readonly), + 'value': this.options.value[et2_customfields_list.PREFIX + field_name] + }; + // Can't have a required readonly, it will warn & be removed later, so avoid the warning + if (attrs.readonly === true) + delete attrs.needed; + if (this.options.onchange) { + attrs.onchange = this.options.onchange; + } + if (this[setup_function]) { + var no_skip = this[setup_function].call(this, field_name, field, attrs); + if (!no_skip) + continue; + } + if (this.getType() == 'customfields-list') { + // No label, cust widget + attrs.readonly = true; + // Widget tooltips don't work in nextmatch because of the creation / binding separation + // Set title to field label so browser will show something + // Add field label & help as data attribute to row, so it can be stylied with CSS (title should be disabled) + row.attr('title', field.label); + row.attr('data-label', field.label); + row.attr('data-field', field_name); + row.attr('data-help', field.help); + this.detachedNodes.push(row[0]); + } + else { + // Label in first column, widget in 2nd + cf.text(field.label + ""); + cf = jQuery(document.createElement("td")) + .appendTo(row); + } + this.rows[id] = cf[0]; + // Set any additional attributes set in options, but not for widgets that pass actual options + if (['select', 'radio', 'radiogroup', 'checkbox', 'button'].indexOf(field.type) == -1 && !jQuery.isEmptyObject(field.values)) { + var w = et2_registry[attrs.type ? attrs.type : field.type]; + for (var attr_name in field.values) { + if (typeof w.prototype.attributes[attr_name] != "undefined") + attrs[attr_name] = field.values[attr_name]; + } + } + // Create widget + var widget = this.widgets[field_name] = et2_createWidget(attrs.type ? attrs.type : field.type, attrs, this); + } + // Field is not to be shown + if (!this.options.fields || jQuery.isEmptyObject(this.options.fields) || this.options.fields[field_name] == true) { + jQuery(this.rows[field_name]).show(); + } + else { + jQuery(this.rows[field_name]).hide(); + } + } + }; + /** + * Read needed info on available custom fields from various places it's stored. + */ + et2_customfields_list.prototype.transformAttributes = function (_attrs) { + _super.prototype.transformAttributes.call(this, _attrs); + // Add in settings that are objects + // Customized settings for this widget (unlikely) + var data = this.getArrayMgr("modifications").getEntry(this.id); + // Check for global settings + var global_data = this.getArrayMgr("modifications").getRoot().getEntry('~custom_fields~', true); + if (global_data) { + for (var key_1 in data) { + // Don't overwrite fields with global values + if (global_data[key_1] && key_1 !== 'fields') { + data[key_1] = jQuery.extend(true, {}, data[key_1], global_data[key_1]); + } + } + } + for (var key in data) { + _attrs[key] = data[key]; + } + for (var key_2 in global_data) { + if (typeof global_data[key_2] != 'undefined' && !_attrs[key_2]) + _attrs[key_2] = global_data[key_2]; + } + if (this.id) { + // Set the value for this element + var contentMgr = this.getArrayMgr("content"); + if (contentMgr != null) { + var val = contentMgr.getEntry(this.id); + _attrs["value"] = {}; + if (val !== null) { + if (this.id.indexOf(et2_customfields_list.PREFIX) === 0 && typeof data.fields != 'undefined' && data.fields[this.id.replace(et2_customfields_list.PREFIX, '')] === true) { + _attrs['value'][this.id] = val; + } + else { + // Only set the values that match desired custom fields + for (var key_3 in val) { + if (key_3.indexOf(et2_customfields_list.PREFIX) == 0) { + _attrs["value"][key_3] = val[key_3]; + } + } + } + //_attrs["value"] = val; + } + else { + // Check for custom fields directly in record + for (var key in _attrs.customfields) { + _attrs["value"][et2_customfields_list.PREFIX + key] = contentMgr.getEntry(et2_customfields_list.PREFIX + key); + } + } + } + } + }; + et2_customfields_list.prototype.loadFromXML = function (_node) { + this.loadFields(); + // Load the nodes as usual + _super.prototype.loadFromXML.call(this, _node); + }; + et2_customfields_list.prototype.set_value = function (_value) { + if (!this.options.customfields) + return; + for (var field_name in this.options.customfields) { + // Skip fields if we're filtering + if (!jQuery.isEmptyObject(this.options.fields) && !this.options.fields[field_name]) + continue; + // Make sure widget is created, and has the needed function + if (!this.widgets[field_name] || !this.widgets[field_name].set_value) + continue; + var value = _value[et2_customfields_list.PREFIX + field_name] ? _value[et2_customfields_list.PREFIX + field_name] : null; + // Check if ID was missing + if (value == null && this.id == et2_customfields_list.DEFAULT_ID && this.getArrayMgr("content").getEntry(et2_customfields_list.PREFIX + field_name)) { + value = this.getArrayMgr("content").getEntry(et2_customfields_list.PREFIX + field_name); + } + switch (this.options.customfields[field_name].type) { + case 'date': + // Date custom fields are always in Y-m-d, which seldom matches user's preference + // which fails when sent to date widget. This is only used for nm rows, when possible + // this is fixed server side + if (value && isNaN(value)) { + value = jQuery.datepicker.parseDate("yy-mm-dd", value); + } + break; + } + this.widgets[field_name].set_value(value); + } + }; + /** + * et2_IInput so the custom field can be it's own widget. + */ + et2_customfields_list.prototype.getValue = function () { + // Not using an ID means we have to grab all the widget values, and put them where server knows to look + if (this.id != et2_customfields_list.DEFAULT_ID) { + return null; + } + var value = {}; + for (var field_name in this.widgets) { + if (this.widgets[field_name].getValue && !this.widgets[field_name].options.readonly) { + value[et2_customfields_list.PREFIX + field_name] = this.widgets[field_name].getValue(); + } + } + return value; + }; + et2_customfields_list.prototype.isDirty = function () { + var dirty = true; + for (var field_name in this.widgets) { + if (this.widgets[field_name].isDirty) { + dirty = dirty && this.widgets[field_name].isDirty(); + } + } + return dirty; + }; + et2_customfields_list.prototype.resetDirty = function () { + for (var field_name in this.widgets) { + if (this.widgets[field_name].resetDirty) { + this.widgets[field_name].resetDirty(); + } + } + }; + et2_customfields_list.prototype.isValid = function () { + // Individual customfields will handle themselves + return true; + }; + /** + * Adapt provided attributes to match options for widget + * + * rows > 1 --> textarea, with rows=rows and cols=len + * !rows --> input, with size=len + * rows = 1 --> input, with size=len, maxlength=len + */ + et2_customfields_list.prototype._setup_text = function (field_name, field, attrs) { + // No label on the widget itself + delete (attrs.label); + field.type = 'textbox'; + attrs.rows = field.rows > 1 ? field.rows : null; + if (field.len) { + attrs.size = field.len; + if (field.rows == 1) + attrs.maxlength = field.len; + } + return true; + }; + et2_customfields_list.prototype._setup_ajax_select = function (field_name, field, attrs) { + var attributes = ['get_rows', 'get_title', 'id_field', 'template']; + if (field.values) { + for (var i = 0; i < attributes.length; i++) { + if (typeof field.values[attributes[i]] !== 'undefined') { + attrs[attributes[i]] = field.values[attributes[i]]; + } + } + } + return true; + }; + et2_customfields_list.prototype._setup_float = function (field_name, field, attrs) { + // No label on the widget itself + delete (attrs.label); + field.type = 'float'; + if (field.len) { + attrs.size = field.len; + } + return true; + }; + et2_customfields_list.prototype._setup_select = function (field_name, field, attrs) { + // No label on the widget itself + delete (attrs.label); + attrs.rows = field.rows; + // select_options are now send from server-side incl. ones defined via a file in EGroupware root + attrs.tags = field.tags; + return true; + }; + et2_customfields_list.prototype._setup_select_account = function (field_name, field, attrs) { + attrs.empty_label = egw.lang('Select'); + if (field.account_type) { + attrs.account_type = field.account_type; + } + return this._setup_select(field_name, field, attrs); + }; + et2_customfields_list.prototype._setup_date = function (field_name, field, attrs) { + attrs.data_format = field.values && field.values.format ? field.values.format : 'Y-m-d'; + return true; + }; + et2_customfields_list.prototype._setup_date_time = function (field_name, field, attrs) { + attrs.data_format = field.values && field.values.format ? field.values.format : 'Y-m-d H:i:s'; + return true; + }; + et2_customfields_list.prototype._setup_htmlarea = function (field_name, field, attrs) { + attrs.config = field.config ? field.config : {}; + attrs.config.toolbarStartupExpanded = false; + if (field.len) { + attrs.config.width = field.len + 'px'; + } + attrs.config.height = (((field.rows > 0 && field.rows != 'undefined') ? field.rows : 5) * 16) + 'px'; + // We have to push the config modifications into the modifications array, or they'll + // be overwritten by the site config from the server + var data = this.getArrayMgr("modifications").getEntry(et2_customfields_list.PREFIX + field_name); + if (data) + jQuery.extend(data.config, attrs.config); + return true; + }; + et2_customfields_list.prototype._setup_radio = function (field_name, field, attrs) { + // 'Empty' label will be first + delete (attrs.label); + if (field.values && field.values['']) { + attrs.label = field.values['']; + delete field.values['']; + } + field.type = 'radiogroup'; + attrs.options = field.values; + return true; + }; + et2_customfields_list.prototype._setup_checkbox = function (field_name, field, attrs) { + // Read-only checkbox is just text + if (attrs.readonly) { + attrs.ro_true = field.label; + } + return true; + }; + /** + * People set button attributes as + * label: javascript + */ + et2_customfields_list.prototype._setup_button = function (field_name, field, attrs) { + // No label on the widget itself + delete (attrs.label); + attrs.label = field.label; + if (this.getType() == 'customfields-list') { + // No buttons in a list, it causes problems with detached nodes + return false; + } + // Simple case, one widget for a custom field + if (!field.values || typeof field.values != 'object' || Object.keys(field.values).length == 1) { + for (var key_4 in field.values) { + attrs.label = key_4; + attrs.onclick = field.values[key_4]; + } + if (!attrs.label) { + attrs.label = 'No "label=onclick" in values!'; + attrs.onclick = function () { return false; }; + } + return !attrs.readonly; + } + else { + // Complicated case, a single custom field you get multiple widgets + // Handle it all here, since this is the exception + var row = jQuery('tr', this.tbody).last(); + var cf = jQuery('td', row); + // Label in first column, widget in 2nd + cf.text(field.label + ""); + cf = jQuery(document.createElement("td")) + .appendTo(row); + for (var key in field.values) { + var button_attrs = jQuery.extend({}, attrs); + button_attrs.label = key; + button_attrs.onclick = field.values[key]; + button_attrs.id = attrs.id + '_' + key; + // This controls where the button is placed in the DOM + this.rows[button_attrs.id] = cf[0]; + // Do not store in the widgets list, one name for multiple widgets would cause problems + /*this.widgets[field_name] = */ et2_createWidget(attrs.type ? attrs.type : field.type, button_attrs, this); + } + return false; + } + }; + et2_customfields_list.prototype._setup_link_entry = function (field_name, field, attrs) { + if (field.type === 'filemanager') { + return this._setup_filemanager(field_name, field, attrs); + } + // No label on the widget itself + delete (attrs.label); + attrs.type = "link-entry"; + attrs.only_app = field.type; + return true; + }; + et2_customfields_list.prototype._setup_filemanager = function (field_name, field, attrs) { + attrs.type = 'vfs-upload'; + delete (attrs.label); + if (this.getType() == 'customfields-list') { + // No special UI needed? + return true; + } + else { + // Complicated case, a single custom field you get multiple widgets + // Handle it all here, since this is the exception + var row = jQuery('tr', this.tbody).last(); + var cf = jQuery('td', row); + // Label in first column, widget in 2nd + cf.text(field.label + ""); + cf = jQuery(document.createElement("td")) + .appendTo(row); + // Create upload widget + var widget = this.widgets[field_name] = et2_createWidget(attrs.type ? attrs.type : field.type, attrs, this); + // This controls where the widget is placed in the DOM + this.rows[attrs.id] = cf[0]; + jQuery(widget.getDOMNode(widget)).css('vertical-align', 'top'); + // Add a link to existing VFS file + var select_attrs = jQuery.extend({}, attrs, + // Filemanager select + { + label: '', + mode: widget.options.multiple ? 'open-multiple' : 'open', + method: 'EGroupware\\Api\\Etemplate\\Widget\\Link::ajax_link_existing', + method_id: attrs.path, + button_label: egw.lang('Link') + }, { type: 'vfs-select' }); + select_attrs.id = attrs.id + '_vfs_select'; + // This controls where the button is placed in the DOM + this.rows[select_attrs.id] = cf[0]; + // Do not store in the widgets list, one name for multiple widgets would cause problems + widget = et2_createWidget(select_attrs.type, select_attrs, this); + jQuery(widget.getDOMNode(widget)).css('vertical-align', 'top').prependTo(cf); + } + return false; + }; + /** + * Set which fields are visible, by name + * + * Note: no # prefix on the name + * + */ + et2_customfields_list.prototype.set_visible = function (_fields) { + for (var name_1 in _fields) { + if (this.rows[et2_customfields_list.PREFIX + name_1]) { + if (_fields[name_1]) { + jQuery(this.rows[et2_customfields_list.PREFIX + name_1]).show(); + } + else { + jQuery(this.rows[et2_customfields_list.PREFIX + name_1]).hide(); + } + } + this.options.fields[name_1] = _fields[name_1]; + } + }; + /** + * Code for implementing et2_IDetachedDOM + */ + et2_customfields_list.prototype.getDetachedAttributes = function (_attrs) { + _attrs.push("value", "class"); + }; + et2_customfields_list.prototype.getDetachedNodes = function () { + return this.detachedNodes ? this.detachedNodes : []; + }; + et2_customfields_list.prototype.setDetachedAttributes = function (_nodes, _values) { + // Individual widgets are detected and handled by the grid, but the interface is needed for this to happen + // Show the row if there's a value, hide it if there is no value + for (var i = 0; i < _nodes.length; i++) { + // toggle() needs a boolean to do what we want + var key = _nodes[i].getAttribute('data-field'); + jQuery(_nodes[i]).toggle(_values.fields[key] && _values.value[et2_customfields_list.PREFIX + key] ? true : false); + } + }; + et2_customfields_list._attributes = { + 'customfields': { + 'name': 'Custom fields', + 'description': 'Auto filled', + 'type': 'any' + }, + 'fields': { + 'name': 'Custom fields', + 'description': 'Auto filled', + 'type': 'any' + }, + 'value': { + 'name': 'Custom fields', + 'description': 'Auto filled', + 'type': "any" + }, + 'type_filter': { + 'name': 'Field filter', + "default": "", + "type": "any", + "description": "Filter displayed custom fields by their 'type2' attribute" + }, + 'private': { + ignore: true, + type: 'boolean' + }, + 'sub_app': { + 'name': 'sub app name', + 'type': "string", + 'description': "Name of sub application" + }, + // Allow onchange so you can put handlers on the sub-widgets + 'onchange': { + "name": "onchange", + "type": "string", + "default": et2_no_init, + "description": "JS code which is executed when the value changes." + } + }; + et2_customfields_list.PREFIX = '#'; + et2_customfields_list.DEFAULT_ID = "custom_fields"; + return et2_customfields_list; +}(et2_core_valueWidget_1.et2_valueWidget)); +exports.et2_customfields_list = et2_customfields_list; +et2_core_widget_1.et2_register_widget(et2_customfields_list, ["customfields", "customfields-list"]); +//# sourceMappingURL=et2_extension_customfields.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_extension_customfields.ts b/api/js/etemplate/et2_extension_customfields.ts new file mode 100644 index 0000000000..a7087a37b4 --- /dev/null +++ b/api/js/etemplate/et2_extension_customfields.ts @@ -0,0 +1,758 @@ +/** + * EGroupware eTemplate2 - JS Custom fields object + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage api + * @link http://www.egroupware.org + * @author Nathan Gray + * @copyright Nathan Gray 2011 + * @version $Id$ + */ + +/*egw:uses + lib/tooltip; + /vendor/bower-asset/jquery/dist/jquery.js; + et2_core_xml; + et2_core_DOMWidget; + et2_core_inputWidget; +*/ + +import {et2_register_widget, WidgetConfig} from "./et2_core_widget"; +import {ClassWithAttributes} from "./et2_core_inheritance"; +import {et2_valueWidget} from "./et2_core_valueWidget"; + +export class et2_customfields_list extends et2_valueWidget implements et2_IDetachedDOM, et2_IInput +{ + static readonly _attributes = { + 'customfields': { + 'name': 'Custom fields', + 'description': 'Auto filled', + 'type': 'any' + }, + 'fields': { + 'name': 'Custom fields', + 'description': 'Auto filled', + 'type': 'any' + }, + 'value': { + 'name': 'Custom fields', + 'description': 'Auto filled', + 'type': "any" + }, + 'type_filter': { + 'name': 'Field filter', + "default": "", + "type": "any", // String or array + "description": "Filter displayed custom fields by their 'type2' attribute" + }, + 'private': { + ignore: true, + type: 'boolean' + }, + 'sub_app': { + 'name': 'sub app name', + 'type': "string", + 'description': "Name of sub application" + }, + // Allow onchange so you can put handlers on the sub-widgets + 'onchange': { + "name": "onchange", + "type": "string", + "default": et2_no_init, + "description": "JS code which is executed when the value changes." + } + }; + + legacyOptions = ["type_filter","private", "fields"]; // Field restriction & private done server-side + + public static readonly PREFIX = '#'; + + public static readonly DEFAULT_ID = "custom_fields"; + private tbody: JQuery; + private table: JQuery; + private rows = {}; + private widgets = {}; + private detachedNodes = []; + + constructor(_parent?, _attrs? : WidgetConfig, _child? : object) + { + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_customfields_list._attributes, _child || {})); + + // Some apps (infolog edit) don't give ID, so assign one to get settings + if(!this.id) + { + this.id = _attrs.id = et2_customfields_list.DEFAULT_ID; + + // 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); + } + + // Create the table body and the table + this.tbody = jQuery(document.createElement("tbody")); + this.table = jQuery(document.createElement("table")) + .addClass("et2_grid et2_customfield_list"); + this.table.append(this.tbody); + + if(!this.options.fields) this.options.fields = {}; + if(typeof this.options.fields === 'string') + { + const fields = this.options.fields.split(','); + this.options.fields = {}; + for(var i = 0; i < fields.length; i++) + { + this.options.fields[fields[i]] = true; + } + } + + if(this.options.type_filter && typeof this.options.type_filter == "string") + { + this.options.type_filter = this.options.type_filter.split(","); + } + if(this.options.type_filter) + { + const already_filtered = !jQuery.isEmptyObject(this.options.fields); + for(let field_name in this.options.customfields) + { + // Already excluded? + if(already_filtered && !this.options.fields[field_name]) continue; + + if(!this.options.customfields[field_name].type2 || this.options.customfields[field_name].type2.length == 0 || + this.options.customfields[field_name].type2 == '0') + { + // No restrictions + this.options.fields[field_name] = true; + continue; + } + const types = typeof this.options.customfields[field_name].type2 == 'string' ? this.options.customfields[field_name].type2.split(",") : this.options.customfields[field_name].type2; + this.options.fields[field_name] = false; + for(var i = 0; i < types.length; i++) + { + if(jQuery.inArray(types[i],this.options.type_filter) > -1) + { + this.options.fields[field_name] = true; + + } + } + } + } + + this.setDOMNode(this.table[0]); + } + + destroy( ) + { + super.destroy(); + this.rows = {}; + this.widgets = {}; + this.detachedNodes = []; + this.tbody = null; + } + + /** + * What does this do? I don't know, but when everything is done the second + * time, this makes it work. Otherwise, all custom fields are lost. + */ + assign( _obj) + { + this.loadFields(); + } + + getDOMNode( _sender) + { + + // Check whether the _sender object exists inside the management array + if(this.rows && _sender.id && this.rows[_sender.id]) + { + return this.rows[_sender.id]; + } + + return super.getDOMNode(_sender); + } + + /** + * Initialize widgets for custom fields + */ + loadFields( ) + { + if(!this.options || !this.options.customfields) return; + + // Already set up - avoid duplicates in nextmatch + if(this.getType() == 'customfields-list' && !this.isInTree()) return; + if(!jQuery.isEmptyObject(this.widgets)) return; + + // Check for global setting changes (visibility) + const global_data = this.getArrayMgr("modifications").getRoot().getEntry('~custom_fields~'); + if(global_data && global_data.fields && !this.options.fields) this.options.fields = global_data.fields; + + // For checking app entries + const apps = this.egw().link_app_list(); + + // Create the table rows + for(let field_name in this.options.customfields) + { + // Skip fields if we're filtering + if(this.getType() != 'customfields-list' && !jQuery.isEmptyObject(this.options.fields) && !this.options.fields[field_name]) continue; + + const field = this.options.customfields[field_name]; + + let id = et2_customfields_list.PREFIX + field_name; + + // Need curlies around ID for nm row expansion + if(this.id == '$row') + { + id = "{" + this.id + "}" + "["+et2_customfields_list.PREFIX + field_name+"]"; + } + else if (this.id != et2_customfields_list.DEFAULT_ID) + { + // Prefix this ID to avoid potential ID collisions + id = this.id + "["+id+"]"; + } + + // Avoid creating field twice + if(!this.rows[id]) + { + + const row = jQuery(document.createElement("tr")) + .appendTo(this.tbody) + .addClass(this.id + '_' + id); + let cf = jQuery(document.createElement("td")) + .appendTo(row); + if(!field.type) field.type = 'text";'; + const setup_function = '_setup_' + (apps[field.type] ? 'link_entry' : field.type.replace("-", "_")); + + const attrs: any = { + 'id': id, + 'statustext': field.help, + 'needed': field.needed, + 'readonly': this.getArrayMgr("readonlys").isReadOnly(id, null, this.options.readonly), + 'value': this.options.value[et2_customfields_list.PREFIX + field_name] + }; + // Can't have a required readonly, it will warn & be removed later, so avoid the warning + if(attrs.readonly === true) delete attrs.needed; + + if(this.options.onchange) + { + attrs.onchange = this.options.onchange; + } + + if(this[setup_function]) { + const no_skip = this[setup_function].call(this, field_name, field, attrs); + if(!no_skip) continue; + } + + if(this.getType() == 'customfields-list') { + // No label, cust widget + attrs.readonly = true; + // Widget tooltips don't work in nextmatch because of the creation / binding separation + // Set title to field label so browser will show something + // Add field label & help as data attribute to row, so it can be stylied with CSS (title should be disabled) + row.attr('title', field.label); + row.attr('data-label', field.label); + row.attr('data-field', field_name); + row.attr('data-help', field.help); + this.detachedNodes.push(row[0]); + } + else + { + // Label in first column, widget in 2nd + cf.text(field.label + ""); + cf = jQuery(document.createElement("td")) + .appendTo(row); + } + this.rows[id] = cf[0]; + + // Set any additional attributes set in options, but not for widgets that pass actual options + if(['select','radio','radiogroup','checkbox','button'].indexOf(field.type) == -1 && !jQuery.isEmptyObject(field.values)) + { + const w = et2_registry[attrs.type ? attrs.type : field.type]; + for(let attr_name in field.values) + { + if (typeof w.prototype.attributes[attr_name] != "undefined") + attrs[attr_name] = field.values[attr_name]; + } + } + // Create widget + const widget = this.widgets[field_name] = et2_createWidget(attrs.type ? attrs.type : field.type, attrs, this); + } + + // Field is not to be shown + if(!this.options.fields || jQuery.isEmptyObject(this.options.fields) || this.options.fields[field_name] == true) + { + jQuery(this.rows[field_name]).show(); + } + else + { + jQuery(this.rows[field_name]).hide(); + } + + } + } + + /** + * Read needed info on available custom fields from various places it's stored. + */ + transformAttributes( _attrs) + { + super.transformAttributes(_attrs); + + // Add in settings that are objects + + // Customized settings for this widget (unlikely) + const data = this.getArrayMgr("modifications").getEntry(this.id); + // Check for global settings + const global_data = this.getArrayMgr("modifications").getRoot().getEntry('~custom_fields~', true); + if(global_data) + { + for(let key in data) + { + // Don't overwrite fields with global values + if(global_data[key] && key !== 'fields') + { + data[key] = jQuery.extend(true, {}, data[key], global_data[key]); + } + } + } + for(var key in data) + { + _attrs[key] = data[key]; + } + for(let key in global_data) + { + if(typeof global_data[key] != 'undefined' && ! _attrs[key]) _attrs[key] = global_data[key]; + } + + if (this.id) + { + // Set the value for this element + const contentMgr = this.getArrayMgr("content"); + if (contentMgr != null) { + const val = contentMgr.getEntry(this.id); + _attrs["value"] = {}; + if (val !== null) + { + if(this.id.indexOf(et2_customfields_list.PREFIX) === 0 && typeof data.fields != 'undefined' && data.fields[this.id.replace(et2_customfields_list.PREFIX,'')] === true) + { + _attrs['value'][this.id] = val; + } + else + { + // Only set the values that match desired custom fields + for(let key in val) + { + if(key.indexOf(et2_customfields_list.PREFIX) == 0) { + _attrs["value"][key] = val[key]; + } + } + } + //_attrs["value"] = val; + } + else + { + // Check for custom fields directly in record + for(var key in _attrs.customfields) + { + _attrs["value"][et2_customfields_list.PREFIX + key] = contentMgr.getEntry(et2_customfields_list.PREFIX + key); + } + } + } + } + } + + loadFromXML( _node) + { + this.loadFields(); + + // Load the nodes as usual + super.loadFromXML(_node); + } + + set_value( _value) + { + if(!this.options.customfields) return; + for(let field_name in this.options.customfields) + { + // Skip fields if we're filtering + if(!jQuery.isEmptyObject(this.options.fields) && !this.options.fields[field_name]) continue; + + // Make sure widget is created, and has the needed function + if(!this.widgets[field_name] || !this.widgets[field_name].set_value) continue; + let value = _value[et2_customfields_list.PREFIX + field_name] ? _value[et2_customfields_list.PREFIX + field_name] : null; + + // Check if ID was missing + if(value == null && this.id == et2_customfields_list.DEFAULT_ID && this.getArrayMgr("content").getEntry(et2_customfields_list.PREFIX + field_name)) + { + value = this.getArrayMgr("content").getEntry(et2_customfields_list.PREFIX + field_name); + } + + switch(this.options.customfields[field_name].type) + { + case 'date': + // Date custom fields are always in Y-m-d, which seldom matches user's preference + // which fails when sent to date widget. This is only used for nm rows, when possible + // this is fixed server side + if(value && isNaN(value)) + { + value = jQuery.datepicker.parseDate("yy-mm-dd",value); + } + break; + } + this.widgets[field_name].set_value(value); + } + } + + /** + * et2_IInput so the custom field can be it's own widget. + */ + getValue( ) + { + // Not using an ID means we have to grab all the widget values, and put them where server knows to look + if(this.id != et2_customfields_list.DEFAULT_ID) + { + return null; + } + const value = {}; + for(let field_name in this.widgets) + { + if(this.widgets[field_name].getValue && !this.widgets[field_name].options.readonly) + { + value[et2_customfields_list.PREFIX + field_name] = this.widgets[field_name].getValue(); + } + } + return value; + } + + isDirty( ) + { + let dirty = true; + for(let field_name in this.widgets) + { + if(this.widgets[field_name].isDirty) + { + dirty = dirty && this.widgets[field_name].isDirty(); + } + } + return dirty; + } + + resetDirty( ) + { + for(let field_name in this.widgets) + { + if(this.widgets[field_name].resetDirty) + { + this.widgets[field_name].resetDirty(); + } + } + } + + isValid( ) + { + // Individual customfields will handle themselves + return true; + } + + /** + * Adapt provided attributes to match options for widget + * + * rows > 1 --> textarea, with rows=rows and cols=len + * !rows --> input, with size=len + * rows = 1 --> input, with size=len, maxlength=len + */ + _setup_text( field_name, field, attrs) + { + // No label on the widget itself + delete(attrs.label); + + field.type = 'textbox'; + attrs.rows = field.rows > 1 ? field.rows : null; + + if(field.len) + { + attrs.size = field.len; + if (field.rows == 1) attrs.maxlength = field.len; + } + return true; + } + _setup_ajax_select( field_name, field, attrs) + { + const attributes = ['get_rows', 'get_title', 'id_field', 'template']; + if(field.values) + { + for(let i = 0; i < attributes.length; i++) + { + if(typeof field.values[attributes[i]] !== 'undefined') + { + attrs[attributes[i]] = field.values[attributes[i]]; + } + } + } + return true; + } + _setup_float( field_name, field, attrs) + { + // No label on the widget itself + delete(attrs.label); + + field.type = 'float'; + + if(field.len) + { + attrs.size = field.len; + } + return true; + } + _setup_select( field_name, field, attrs) + { + // No label on the widget itself + delete(attrs.label); + + attrs.rows = field.rows; + // select_options are now send from server-side incl. ones defined via a file in EGroupware root + attrs.tags = field.tags; + + return true; + } + _setup_select_account( field_name, field, attrs) + { + attrs.empty_label = egw.lang('Select'); + if(field.account_type) + { + attrs.account_type = field.account_type; + } + return this._setup_select(field_name, field, attrs); + } + + _setup_date(field_name, field, attrs) { + attrs.data_format = field.values && field.values.format ? field.values.format : 'Y-m-d'; + return true; + } + _setup_date_time( field_name, field, attrs) + { + attrs.data_format = field.values && field.values.format ? field.values.format : 'Y-m-d H:i:s'; + return true; + } + _setup_htmlarea( field_name, field, attrs) + { + attrs.config = field.config ? field.config : {}; + attrs.config.toolbarStartupExpanded = false; + if(field.len) + { + attrs.config.width = field.len+'px'; + } + attrs.config.height = (((field.rows > 0 && field.rows !='undefined') ? field.rows : 5) *16) +'px'; + + // We have to push the config modifications into the modifications array, or they'll + // be overwritten by the site config from the server + const data = this.getArrayMgr("modifications").getEntry(et2_customfields_list.PREFIX + field_name); + if(data) jQuery.extend(data.config, attrs.config); + + return true; + } + _setup_radio( field_name, field, attrs) + { + // 'Empty' label will be first + delete(attrs.label); + + if(field.values && field.values['']) + { + attrs.label = field.values['']; + delete field.values['']; + } + + field.type = 'radiogroup'; + attrs.options = field.values; + return true; + } + + _setup_checkbox( field_name, field, attrs) + { + // Read-only checkbox is just text + if(attrs.readonly) + { + attrs.ro_true = field.label; + } + return true; + } + + /** + * People set button attributes as + * label: javascript + */ + _setup_button( field_name, field, attrs) + { + // No label on the widget itself + delete(attrs.label); + + attrs.label = field.label; + + if (this.getType() == 'customfields-list') + { + // No buttons in a list, it causes problems with detached nodes + return false; + } + // Simple case, one widget for a custom field + if(!field.values || typeof field.values != 'object' || Object.keys(field.values).length == 1) + { + for(let key in field.values) + { + attrs.label = key; + attrs.onclick = field.values[key]; + } + if (!attrs.label) + { + attrs.label = 'No "label=onclick" in values!'; + attrs.onclick = function(){ return false; }; + } + return !attrs.readonly; + } + else + { + // Complicated case, a single custom field you get multiple widgets + // Handle it all here, since this is the exception + const row = jQuery('tr', this.tbody).last(); + let cf = jQuery('td', row); + // Label in first column, widget in 2nd + cf.text(field.label + ""); + cf = jQuery(document.createElement("td")) + .appendTo(row); + + for(var key in field.values) + { + const button_attrs = jQuery.extend({}, attrs); + button_attrs.label = key; + button_attrs.onclick = field.values[key]; + button_attrs.id = attrs.id + '_' + key; + + // This controls where the button is placed in the DOM + this.rows[button_attrs.id] = cf[0]; + + // Do not store in the widgets list, one name for multiple widgets would cause problems + /*this.widgets[field_name] = */ et2_createWidget(attrs.type ? attrs.type : field.type, button_attrs, this); + } + return false; + } + } + _setup_link_entry( field_name, field, attrs) + { + if(field.type === 'filemanager') + { + return this._setup_filemanager(field_name, field, attrs); + } + // No label on the widget itself + delete(attrs.label); + + attrs.type = "link-entry"; + attrs.only_app = field.type; + return true; + } + + _setup_filemanager( field_name, field, attrs) + { + attrs.type = 'vfs-upload'; + delete(attrs.label); + + if (this.getType() == 'customfields-list') + { + // No special UI needed? + return true; + } + else + { + // Complicated case, a single custom field you get multiple widgets + // Handle it all here, since this is the exception + const row = jQuery('tr', this.tbody).last(); + let cf = jQuery('td', row); + + // Label in first column, widget in 2nd + cf.text(field.label + ""); + cf = jQuery(document.createElement("td")) + .appendTo(row); + + // Create upload widget + let widget = this.widgets[field_name] = et2_createWidget(attrs.type ? attrs.type : field.type, attrs, this); + + // This controls where the widget is placed in the DOM + this.rows[attrs.id] = cf[0]; + jQuery(widget.getDOMNode(widget)).css('vertical-align','top'); + + // Add a link to existing VFS file + const select_attrs = jQuery.extend({}, + attrs, + // Filemanager select + { + label: '', + mode: widget.options.multiple ? 'open-multiple' : 'open', + method: 'EGroupware\\Api\\Etemplate\\Widget\\Link::ajax_link_existing', + method_id: attrs.path, + button_label: egw.lang('Link') + }, {type: 'vfs-select'}); + select_attrs.id = attrs.id + '_vfs_select'; + + // This controls where the button is placed in the DOM + this.rows[select_attrs.id] = cf[0]; + + // Do not store in the widgets list, one name for multiple widgets would cause problems + widget = et2_createWidget(select_attrs.type, select_attrs, this); + jQuery(widget.getDOMNode(widget)).css('vertical-align','top').prependTo(cf); + } + return false; + } + + /** + * Set which fields are visible, by name + * + * Note: no # prefix on the name + * + */ + set_visible( _fields) + { + for(let name in _fields) + { + if(this.rows[et2_customfields_list.PREFIX + name]) + { + if(_fields[name]) + { + jQuery(this.rows[et2_customfields_list.PREFIX+name]).show(); + } + else + { + jQuery(this.rows[et2_customfields_list.PREFIX+name]).hide(); + } + } + this.options.fields[name] = _fields[name]; + } + } + + /** + * Code for implementing et2_IDetachedDOM + */ + getDetachedAttributes(_attrs) + { + _attrs.push("value", "class"); + } + + getDetachedNodes() + { + return this.detachedNodes ? this.detachedNodes : []; + } + + setDetachedAttributes(_nodes, _values) + { + // Individual widgets are detected and handled by the grid, but the interface is needed for this to happen + + // Show the row if there's a value, hide it if there is no value + for(let i = 0; i < _nodes.length; i++) + { + // toggle() needs a boolean to do what we want + const key = _nodes[i].getAttribute('data-field'); + jQuery(_nodes[i]).toggle(_values.fields[key] && _values.value[et2_customfields_list.PREFIX + key]?true:false); + } + } +} +et2_register_widget(et2_customfields_list, ["customfields", "customfields-list"]); + diff --git a/api/js/etemplate/et2_extension_itempicker_actions.js b/api/js/etemplate/et2_extension_itempicker_actions.js index 81c3de63d8..34a70e177e 100644 --- a/api/js/etemplate/et2_extension_itempicker_actions.js +++ b/api/js/etemplate/et2_extension_itempicker_actions.js @@ -10,19 +10,16 @@ * @author Nathan Gray * @copyright 2012 Christian Binder * @copyright 2011 Nathan Gray - * @version $Id: et2_widget_itempicker.js 38623 2012-03-26 23:27:53Z jaytraxx $ */ - -function itempickerDocumentAction(context, data) -{ - "use strict"; - - var formid = "itempicker_action_form"; - var form = "
" - + "" - + "" - + "" - + "
"; - jQuery("body").append(form); - jQuery("#" + formid).submit().remove(); +function itempickerDocumentAction(context, data) { + "use strict"; + var formid = "itempicker_action_form"; + var form = "
" + + "" + + "" + + "" + + "
"; + jQuery("body").append(form); + jQuery("#" + formid).submit().remove(); } +//# sourceMappingURL=et2_extension_itempicker_actions.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_extension_itempicker_actions.ts b/api/js/etemplate/et2_extension_itempicker_actions.ts new file mode 100644 index 0000000000..4fa825c9bd --- /dev/null +++ b/api/js/etemplate/et2_extension_itempicker_actions.ts @@ -0,0 +1,28 @@ +/** + * EGroupware eTemplate2 - JS Itempicker object + * derived from et2_link_entry widget @copyright 2011 Nathan Gray + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage api + * @link http://www.egroupware.org + * @author Christian Binder + * @author Nathan Gray + * @copyright 2012 Christian Binder + * @copyright 2011 Nathan Gray + */ + +function itempickerDocumentAction(context, data) +{ + "use strict"; + + let formid = "itempicker_action_form"; + let form = "
" + + "" + + "" + + "" + + "
"; + jQuery("body").append(form); + jQuery("#" + formid).submit().remove(); +} + diff --git a/api/js/etemplate/et2_extension_nextmatch.js b/api/js/etemplate/et2_extension_nextmatch.js index e2f4ed2a33..8643780ee4 100644 --- a/api/js/etemplate/et2_extension_nextmatch.js +++ b/api/js/etemplate/et2_extension_nextmatch.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - JS Nextmatch object * @@ -8,60 +9,73 @@ * @author Andreas Stöckel * @copyright Stylite 2011 * @version $Id$ - */ + * /*egw:uses - // Include the action system - egw_action.egw_action; - egw_action.egw_action_popup; - egw_action.egw_action_dragdrop; - egw_action.egw_menu_dhtmlx; + // Include the action system + egw_action.egw_action; + egw_action.egw_action_popup; + egw_action.egw_action_dragdrop; + egw_action.egw_menu_dhtmlx; - // Include some core classes - et2_core_widget; - et2_core_interfaces; - et2_core_DOMWidget; + // Include some core classes + et2_core_widget; + et2_core_interfaces; + et2_core_DOMWidget; - // Include all widgets the nextmatch extension will create - et2_widget_template; - et2_widget_grid; - et2_widget_selectbox; - et2_widget_selectAccount; - et2_widget_taglist; - et2_extension_customfields; + // Include all widgets the nextmatch extension will create + et2_widget_template; + et2_widget_grid; + et2_widget_selectbox; + et2_widget_selectAccount; + et2_widget_taglist; + et2_extension_customfields; - // Include all nextmatch subclasses - et2_extension_nextmatch_controller; - et2_extension_nextmatch_rowProvider; - et2_extension_nextmatch_dynheight; + // Include all nextmatch subclasses + et2_extension_nextmatch_rowProvider; + et2_extension_nextmatch_controller; + et2_widget_dynheight; - // Include the grid classes - et2_dataview; + // Include the grid classes + et2_dataview; */ - -/** - * Interface all special nextmatch header elements have to implement. - */ -var et2_INextmatchHeader = new Interface({ - - /** - * The 'setNextmatch' function is called by the parent nextmatch widget - * and tells the nextmatch header widgets which widget they should direct - * their 'sort', 'search' or 'filter' calls to. - * - * @param {et2_nextmatch} _nextmatch - */ - setNextmatch: function(_nextmatch) {} -}); - -var et2_INextmatchSortable = new Interface({ - - setSortmode: function(_mode) {} - -}); - +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 }); +require("./et2_core_common"); +require("./et2_core_interfaces"); +var et2_core_inheritance_1 = require("./et2_core_inheritance"); +var et2_core_widget_1 = require("./et2_core_widget"); +var et2_core_DOMWidget_1 = require("./et2_core_DOMWidget"); +var et2_core_baseWidget_1 = require("./et2_core_baseWidget"); +var et2_core_inputWidget_1 = require("./et2_core_inputWidget"); +var et2_widget_selectbox_1 = require("./et2_widget_selectbox"); +var et2_extension_nextmatch_rowProvider_1 = require("./et2_extension_nextmatch_rowProvider"); +var et2_extension_nextmatch_controller_1 = require("./et2_extension_nextmatch_controller"); +var et2_dataview_1 = require("./et2_dataview"); +var et2_dataview_model_columns_1 = require("./et2_dataview_model_columns"); +var et2_extension_customfields_1 = require("./et2_extension_customfields"); +var et2_INextmatchHeader = "et2_INextmatchHeader"; +function implements_et2_INextmatchHeader(obj) { + return implements_methods(obj, ["setNextmatch"]); +} +var et2_INextmatchSortable = "et2_INextmatchSortable"; +function implements_et2_INextmatchSortable(obj) { + return implements_methods(obj, ["setSortmode"]); +} /** * Class which implements the "nextmatch" XET-Tag * @@ -81,2428 +95,1962 @@ var et2_INextmatchSortable = new Interface({ * +--------------+-----------+-------+ * @augments et2_DOMWidget */ -var et2_nextmatch = (function(){ "use strict"; return et2_DOMWidget.extend([et2_IResizeable, et2_IInput, et2_IPrint], -{ - attributes: { - // These normally set in settings, but broken out into attributes to allow run-time changes - "template": { - "name": "Template", - "type": "string", - "description": "The id of the template which contains the grid layout." - }, - "hide_header": { - "name": "Hide header", - "type": "boolean", - "description": "Hide the header", - "default": false - }, - "header_left": { - "name": "Left custom template", - "type": "string", - "description": "Customise the nextmatch - left side. Provided template becomes a child of nextmatch, and any input widgets are automatically bound to refresh the nextmatch on change. Any inputs with an onChange attribute can trigger the nextmatch to refresh by returning true.", - "default": "" - }, - "header_right": { - "name": "Right custom template", - "type": "string", - "description": "Customise the nextmatch - right side. Provided template becomes a child of nextmatch, and any input widgets are automatically bound to refresh the nextmatch on change. Any inputs with an onChange attribute can trigger the nextmatch to refresh by returning true.", - "default": "" - }, - "header_row": { - "name": "Inline custom template", - "type": "string", - "description": "Customise the nextmatch - inline, after row count. Provided template becomes a child of nextmatch, and any input widgets are automatically bound to refresh the nextmatch on change. Any inputs with an onChange attribute can trigger the nextmatch to refresh by returning true.", - "default": "" - }, - "no_filter": { - "name": "No filter", - "type": "boolean", - "description": "Hide the first filter", - "default": et2_no_init - }, - "no_filter2": { - "name": "No filter2", - "type": "boolean", - "description": "Hide the second filter", - "default": et2_no_init - }, - "view": { - "name": "View", - "type": "string", - "description": "Display entries as either 'row' or 'tile'. A matching template must also be set after changing this.", - "default": et2_no_init - }, - "onselect": { - "name": "onselect", - "type": "js", - "default": et2_no_init, - "description": "JS code which gets executed when rows are selected. Can also be a app.appname.func(selected) style method" - }, - "onfiledrop": { - "name": "onFileDrop", - "type": "js", - "default": et2_no_init, - "description": "JS code that gets executed when a _file_ is dropped on a row. Other drop interactions are handled by the action system. Return false to prevent the default link action." - }, - "settings": { - "name": "Settings", - "type": "any", - "description": "The nextmatch settings", - "default": {} - } - }, - - legacyOptions: ["template","hide_header","header_left","header_right"], - createNamespace: true, - - columns: [], - - // Current view, either row or tile. We store it here as controllers are - // recreated when the template changes. - view: 'row', - - /** - * Constructor - * - * @memberOf et2_nextmatch - */ - init: function() { - this._super.apply(this, arguments); - this.activeFilters = {col_filter:{}}; - - // Directly set current col_filters from settings - jQuery.extend(this.activeFilters.col_filter, this.options.settings.col_filter); - - /* - Process selected custom fields here, so that the settings are correctly - set before the row template is parsed - */ - var prefs = this._getPreferences(); - var cfs = {}; - for(var i = 0; i < prefs.visible.length; i++) - { - if(prefs.visible[i].indexOf(et2_nextmatch_customfields.prototype.prefix) == 0) - { - cfs[prefs.visible[i].substr(1)] = !prefs.negated; - } - } - var global_data = this.getArrayMgr("modifications").getRoot().getEntry('~custom_fields~'); - if(typeof global_data == 'object' && global_data != null) - { - global_data.fields = cfs; - } - - this.div = jQuery(document.createElement("div")) - .addClass("et2_nextmatch"); - - - this.header = et2_createWidget("nextmatch_header_bar", {}, this); - this.innerDiv = jQuery(document.createElement("div")) - .appendTo(this.div); - - // Create the dynheight component which dynamically scales the inner - // container. - this.dynheight = this._getDynheight(); - - // Create the outer grid container - this.dataview = new et2_dataview(this.innerDiv, this.egw()); - - // Blank placeholder - this.blank = jQuery(document.createElement("div")) - .appendTo(this.dataview.table); - - // We cannot create the grid controller now, as this depends on the grid - // instance, which can first be created once we have the columns - this.controller = null; - this.rowProvider = null; - - // keeps sorted columns - this.sortedColumnsList = []; - }, - - /** - * Destroys all - */ - destroy: function() { - // Stop autorefresh - if(this._autorefresh_timer) - { - window.clearInterval(this._autorefresh_timer); - this._autorefresh_timer = null; - } - // Unbind handler used for toggling autorefresh - jQuery(this.getInstanceManager().DOMContainer.parentNode).off('show.et2_nextmatch'); - jQuery(this.getInstanceManager().DOMContainer.parentNode).off('hide.et2_nextmatch'); - - // Free the grid components - this.dataview.free(); - if(this.rowProvider) - { - this.rowProvider.free(); - } - if(this.controller) - { - this.controller.free(); - } - this.dynheight.free(); - - this._super.apply(this, arguments); - }, - - /** - * Loads the nextmatch settings - * - * @param {object} _attrs - */ - transformAttributes: function(_attrs) { - this._super.apply(this, arguments); - - if (this.id) - { - var entry = this.getArrayMgr("content").data; - _attrs["settings"] = {}; - - if (entry) - { - _attrs["settings"] = entry; - - // Make sure there's an action var parameter - if(_attrs["settings"]["actions"] && !_attrs.settings["action_var"]) - { - _attrs.settings.action_var = "action"; - } - - // Merge settings mess into attributes - for(var attr in this.attributes) - { - if(_attrs.settings[attr]) - { - _attrs[attr] = _attrs.settings[attr]; - delete _attrs.settings[attr]; - } - } - } - } - }, - - doLoadingFinished: function() { - this._super.apply(this, arguments); - - if(!this.dynheight) - { - this.dynheight = this._getDynheight(); - } - - // Register handler for dropped files, if possible - if(this.options.settings.row_id) - { - // Appname should be first part of the template name - var split = this.options.template.split('.'); - var appname = split[0]; - - // Check link registry - if(this.egw().link_get_registry(appname)) - { - var self = this; - // Register a handler - jQuery(this.div) - .on('dragenter','.egwGridView_grid tr',function(e) { - // Figure out _which_ row - var row = self.controller.getRowByNode(this); - - if(!row || !row.uid) - { - return false; - } - e.stopPropagation(); e.preventDefault(); - - // Indicate acceptance - if(row.controller && row.controller._selectionMgr) - { - row.controller._selectionMgr.setFocused(row.uid,true); - } - return false; - }) - .on('dragexit','.egwGridView_grid tr', function(e) { - self.controller._selectionMgr.setFocused(); - }) - .on('dragover','.egwGridView_grid tr',false).attr("dropzone","copy") - - .on('drop', '.egwGridView_grid tr',function(e) { - self.handle_drop(e,this); - return false; - }); - } - } - // stop invalidation in no visible tabs - jQuery(this.getInstanceManager().DOMContainer.parentNode).on('hide.et2_nextmatch', jQuery.proxy(function(e) { - if(this.controller && this.controller._grid) - { - this.controller._grid.doInvalidate = false; - } - },this)); - jQuery(this.getInstanceManager().DOMContainer.parentNode).on('show.et2_nextmatch', jQuery.proxy(function(e) { - if(this.controller && this.controller._grid) - { - this.controller._grid.doInvalidate = true; - } - },this)); - - return true; - }, - - /** - * Implements the et2_IResizeable interface - lets the dynheight manager - * update the width and height and then update the dataview container. - */ - resize: function() - { - if (this.dynheight) - { - this.dynheight.update(function(_w, _h) { - this.dataview.resize(_w, _h); - }, this); - } - }, - - /** - * Sorts the nextmatch widget by the given ID. - * - * @param {string} _id is the id of the data entry which should be sorted. - * @param {boolean} _asc if true, the elements are sorted ascending, otherwise - * descending. If not set, the sort direction will be determined - * automatically. - * @param {boolean} _update true/undefined: call applyFilters, false: only set sort - */ - sortBy: function(_id, _asc, _update) { - if (typeof _update == "undefined") - { - _update = true; - } - - // Create the "sort" entry in the active filters if it did not exist - // yet. - if (typeof this.activeFilters["sort"] == "undefined") - { - this.activeFilters["sort"] = { - "id": null, - "asc": true - }; - } - - // Determine the sort direction automatically if it is not set - if (typeof _asc == "undefined") - { - _asc = true; - if (this.activeFilters["sort"].id == _id) - { - _asc = !this.activeFilters["sort"].asc; - } - } - - // Set the sortmode display - this.iterateOver(function(_widget) { - _widget.setSortmode((_widget.id == _id) ? (_asc ? "asc": "desc") : "none"); - }, this, et2_INextmatchSortable); - - if (_update) - { - this.applyFilters({sort: { id: _id, asc: _asc}}); - } - else - { - // Update the entry in the activeFilters object - this.activeFilters["sort"] = { - "id": _id, - "asc": _asc - }; - } - }, - - /** - * Removes the sort entry from the active filters object and thus returns to - * the natural sort order. - */ - resetSort: function() { - // Check whether the nextmatch widget is currently sorted - if (typeof this.activeFilters["sort"] != "undefined") - { - // Reset the sortmode - this.iterateOver(function(_widget) { - _widget.setSortmode("none"); - }, this, et2_INextmatchSortable); - - // Delete the "sort" filter entry - this.applyFilters({sort: undefined}); - } - }, - - /** - * Apply current or modified filters on NM widget (updating rows accordingly) - * - * @param _set filter(s) to set eg. { filter: '' } to reset filter in NM header - */ - applyFilters: function(_set) { - var changed = false; - var keep_selection = false; - - // Avoid loops cause by change events - if(this.update_in_progress) return; - this.update_in_progress = true; - - // Cleared explicitly - if(typeof _set != 'undefined' && jQuery.isEmptyObject(_set)) - { - changed = true; - this.activeFilters = {}; - } - if(typeof this.activeFilters == "undefined") - { - this.activeFilters = {col_filter: {}}; - } - if(typeof this.activeFilters.col_filter == "undefined") - { - this.activeFilters.col_filter = {}; - } - - if (typeof _set == 'object') - { - for(var s in _set) - { - if (s == 'col_filter') - { - // allow apps setState() to reset all col_filter by using undefined or null for it - // they can not pass {} for _set / state.state, if they need to set something - if (_set.col_filter === undefined || _set.col_filter === null) - { - this.activeFilters.col_filter = {}; - changed = true; - } - else - { - for(var c in _set.col_filter) - { - if (this.activeFilters.col_filter[c] !== _set.col_filter[c]) - { - if (_set.col_filter[c]) - { - this.activeFilters.col_filter[c] = _set.col_filter[c]; - } - else - { - delete this.activeFilters.col_filter[c]; - } - changed = true; - } - } - } - } - else if (s === 'selected') - { - changed = true; - keep_selection = true; - this.controller._selectionMgr.resetSelection(); - this.controller._objectManager.clear(); - for(var i in _set.selected) - { - this.controller._selectionMgr.setSelected(_set.selected[i].indexOf('::') > 0 ? _set.selected[i] : this.controller.dataStorePrefix + '::'+_set.selected[i],true); - } - delete _set.selected; - } - else if (this.activeFilters[s] !== _set[s]) - { - this.activeFilters[s] = _set[s]; - changed = true; - } - } - } - - this.egw().debug("info", "Changing nextmatch filters to ", this.activeFilters); - - // Keep the selection after applying filters, but only if unchanged - if(!changed || keep_selection) - { - this.controller.keepSelection(); - } - else - { - // Do not keep selection +var et2_nextmatch = /** @class */ (function (_super) { + __extends(et2_nextmatch, _super); + /** + * Constructor + * + * @memberOf et2_nextmatch + */ + function et2_nextmatch(_parent, _attrs, _child) { + var _this = _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_nextmatch._attributes, _child || {})) || this; + _this.activeFilters = { col_filter: {} }; + _this.columns = []; + // keeps sorted columns + _this.sortedColumnsList = []; + // Directly set current col_filters from settings + jQuery.extend(_this.activeFilters.col_filter, _this.options.settings.col_filter); + /* + Process selected custom fields here, so that the settings are correctly + set before the row template is parsed + */ + var prefs = _this._getPreferences(); + var cfs = {}; + for (var i = 0; i < prefs.visible.length; i++) { + if (prefs.visible[i].indexOf(et2_nextmatch_customfields.PREFIX) == 0) { + cfs[prefs.visible[i].substr(1)] = !prefs.negated; + } + } + var global_data = _this.getArrayMgr("modifications").getRoot().getEntry('~custom_fields~'); + if (typeof global_data == 'object' && global_data != null) { + global_data.fields = cfs; + } + _this.div = jQuery(document.createElement("div")) + .addClass("et2_nextmatch"); + _this.header = et2_createWidget("nextmatch_header_bar", {}, _this); + _this.innerDiv = jQuery(document.createElement("div")) + .appendTo(_this.div); + // Create the dynheight component which dynamically scales the inner + // container. + _this.dynheight = _this._getDynheight(); + // Create the outer grid container + _this.dataview = new et2_dataview_1.et2_dataview(_this.innerDiv, _this.egw()); + // Blank placeholder + _this.blank = jQuery(document.createElement("div")) + .appendTo(_this.dataview.table); + // We cannot create the grid controller now, as this depends on the grid + // instance, which can first be created once we have the columns + _this.controller = null; + _this.rowProvider = null; + return _this; + } + /** + * Destroys all + */ + et2_nextmatch.prototype.destroy = function () { + // Stop auto-refresh + if (this._autorefresh_timer) { + window.clearInterval(this._autorefresh_timer); + this._autorefresh_timer = null; + } + // Unbind handler used for toggling autorefresh + jQuery(this.getInstanceManager().DOMContainer.parentNode).off('show.et2_nextmatch'); + jQuery(this.getInstanceManager().DOMContainer.parentNode).off('hide.et2_nextmatch'); + // Free the grid components + this.dataview.destroy(); + if (this.rowProvider) { + this.rowProvider.destroy(); + } + if (this.controller) { + this.controller.destroy(); + } + this.dynheight.destroy(); + _super.prototype.destroy.call(this); + }; + et2_nextmatch.prototype.getController = function () { + return this.controller; + }; + /** + * Loads the nextmatch settings + * + * @param {object} _attrs + */ + et2_nextmatch.prototype.transformAttributes = function (_attrs) { + _super.prototype.transformAttributes.call(this, _attrs); + if (this.id) { + var entry = this.getArrayMgr("content").data; + _attrs["settings"] = {}; + if (entry) { + _attrs["settings"] = entry; + // Make sure there's an action var parameter + if (_attrs["settings"]["actions"] && !_attrs.settings["action_var"]) { + _attrs.settings.action_var = "action"; + } + // Merge settings mess into attributes + for (var attr in this.attributes) { + if (_attrs.settings[attr]) { + _attrs[attr] = _attrs.settings[attr]; + delete _attrs.settings[attr]; + } + } + } + } + }; + et2_nextmatch.prototype.doLoadingFinished = function () { + _super.prototype.doLoadingFinished.call(this); + if (!this.dynheight) { + this.dynheight = this._getDynheight(); + } + // Register handler for dropped files, if possible + if (this.options.settings.row_id) { + // Appname should be first part of the template name + var split = this.options.template.split('.'); + var appname = split[0]; + // Check link registry + if (this.egw().link_get_registry(appname)) { + var self_1 = this; + // Register a handler + // @ts-ignore + jQuery(this.div) + .on('dragenter', '.egwGridView_grid tr', function (e) { + // Figure out _which_ row + var row = self_1.controller.getRowByNode(this); + if (!row || !row.uid) { + return false; + } + e.stopPropagation(); + e.preventDefault(); + // Indicate acceptance + if (row.controller && row.controller._selectionMgr) { + row.controller._selectionMgr.setFocused(row.uid, true); + } + return false; + }) + .on('dragexit', '.egwGridView_grid tr', function () { + self_1.controller._selectionMgr.setFocused(); + }) + .on('dragover', '.egwGridView_grid tr', false).attr("dropzone", "copy") + .on('drop', '.egwGridView_grid tr', function (e) { + self_1.handle_drop(e, this); + return false; + }); + } + } + // stop invalidation in no visible tabs + jQuery(this.getInstanceManager().DOMContainer.parentNode).on('hide.et2_nextmatch', jQuery.proxy(function () { + if (this.controller && this.controller._grid) { + this.controller._grid.doInvalidate = false; + } + }, this)); + jQuery(this.getInstanceManager().DOMContainer.parentNode).on('show.et2_nextmatch', jQuery.proxy(function () { + if (this.controller && this.controller._grid) { + this.controller._grid.doInvalidate = true; + } + }, this)); + return true; + }; + /** + * Implements the et2_IResizeable interface - lets the dynheight manager + * update the width and height and then update the dataview container. + */ + et2_nextmatch.prototype.resize = function () { + if (this.dynheight) { + this.dynheight.update(function (_w, _h) { + this.dataview.resize(_w, _h); + }, this); + } + }; + /** + * Sorts the nextmatch widget by the given ID. + * + * @param {string} _id is the id of the data entry which should be sorted. + * @param {boolean} _asc if true, the elements are sorted ascending, otherwise + * descending. If not set, the sort direction will be determined + * automatically. + * @param {boolean} _update true/undefined: call applyFilters, false: only set sort + */ + et2_nextmatch.prototype.sortBy = function (_id, _asc, _update) { + if (typeof _update == "undefined") { + _update = true; + } + // Create the "sort" entry in the active filters if it did not exist + // yet. + if (typeof this.activeFilters["sort"] == "undefined") { + this.activeFilters["sort"] = { + "id": null, + "asc": true + }; + } + // Determine the sort direction automatically if it is not set + if (typeof _asc == "undefined") { + _asc = true; + if (this.activeFilters["sort"].id == _id) { + _asc = !this.activeFilters["sort"].asc; + } + } + // Set the sortmode display + this.iterateOver(function (_widget) { + _widget.setSortmode((_widget.id == _id) ? (_asc ? "asc" : "desc") : "none"); + }, this, et2_INextmatchSortable); + if (_update) { + this.applyFilters({ sort: { id: _id, asc: _asc } }); + } + else { + // Update the entry in the activeFilters object + this.activeFilters["sort"] = { + "id": _id, + "asc": _asc + }; + } + }; + /** + * Removes the sort entry from the active filters object and thus returns to + * the natural sort order. + */ + et2_nextmatch.prototype.resetSort = function () { + // Check whether the nextmatch widget is currently sorted + if (typeof this.activeFilters["sort"] != "undefined") { + // Reset the sort mode + this.iterateOver(function (_widget) { + _widget.setSortmode("none"); + }, this, et2_INextmatchSortable); + // Delete the "sort" filter entry + this.applyFilters({ sort: undefined }); + } + }; + /** + * Apply current or modified filters on NM widget (updating rows accordingly) + * + * @param _set filter(s) to set eg. { filter: '' } to reset filter in NM header + */ + et2_nextmatch.prototype.applyFilters = function (_set) { + var changed = false; + var keep_selection = false; + // Avoid loops cause by change events + if (this.update_in_progress) + return; + this.update_in_progress = true; + // Cleared explicitly + if (typeof _set != 'undefined' && jQuery.isEmptyObject(_set)) { + changed = true; + this.activeFilters = { col_filter: {} }; + } + if (typeof this.activeFilters == "undefined") { + this.activeFilters = { col_filter: {} }; + } + if (typeof this.activeFilters.col_filter == "undefined") { + this.activeFilters.col_filter = {}; + } + if (typeof _set == 'object') { + for (var s in _set) { + if (s == 'col_filter') { + // allow apps setState() to reset all col_filter by using undefined or null for it + // they can not pass {} for _set / state.state, if they need to set something + if (_set.col_filter === undefined || _set.col_filter === null) { + this.activeFilters.col_filter = {}; + changed = true; + } + else { + for (var c in _set.col_filter) { + if (this.activeFilters.col_filter[c] !== _set.col_filter[c]) { + if (_set.col_filter[c]) { + this.activeFilters.col_filter[c] = _set.col_filter[c]; + } + else { + delete this.activeFilters.col_filter[c]; + } + changed = true; + } + } + } + } + else if (s === 'selected') { + changed = true; + keep_selection = true; + this.controller._selectionMgr.resetSelection(); + this.controller._objectManager.clear(); + for (var i in _set.selected) { + this.controller._selectionMgr.setSelected(_set.selected[i].indexOf('::') > 0 ? _set.selected[i] : this.controller.dataStorePrefix + '::' + _set.selected[i], true); + } + delete _set.selected; + } + else if (this.activeFilters[s] !== _set[s]) { + this.activeFilters[s] = _set[s]; + changed = true; + } + } + } + this.egw().debug("info", "Changing nextmatch filters to ", this.activeFilters); + // Keep the selection after applying filters, but only if unchanged + if (!changed || keep_selection) { + this.controller.keepSelection(); + } + else { + // Do not keep selection this.controller._selectionMgr.resetSelection(); - this.controller._objectManager.clear(); - this.controller.keepSelection(); - } - - // Update the filters in the grid controller - this.controller.setFilters(this.activeFilters); - - // Update the header - this.header.setFilters(this.activeFilters); - - // Update any column filters - this.iterateOver(function(column) { - // Skip favorites - it implements et2_INextmatchHeader, but we don't want it in the filter - if(typeof column.id != "undefined" && column.id.indexOf('favorite') == 0) return; - - if(typeof column.set_value != "undefined" && column.id) - { - column.set_value(typeof this[column.id] == "undefined" || this[column.id] == null ? "" : this[column.id]); - } - if (column.id && typeof column.get_value == "function") - { - this[column.id] = column.get_value(); - } - }, this.activeFilters.col_filter, et2_INextmatchHeader); - - // Trigger an update - this.controller.update(true); - - if(changed) - { - // Highlight matching favorite in sidebox - if(this.getInstanceManager().app) - { - var appname = this.getInstanceManager().app; - if(app[appname] && app[appname].highlight_favorite) - { - app[appname].highlight_favorite(); - } - } - } - - this.update_in_progress = false; - }, - - /** - * Refresh given rows for specified change - * - * Change type parameters allows for quicker refresh then complete server side reload: - * - 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 - * - * @param {string[]|string} _row_ids rows to refresh - * @param {?string} _type "update", "edit", "delete" or "add" - * - * @see jsapi.egw_refresh() - * @fires refresh from the widget itself - */ - refresh: function(_row_ids, _type) { - // Framework trying to refresh, but nextmatch not fully initialized - if(this.controller === null || !this.div) - { - return; - } - if (!this.div.is(':visible')) // run refresh, once we become visible again - { - jQuery(this.getInstanceManager().DOMContainer.parentNode).one('show.et2_nextmatch', - // Important to use anonymous function instead of just 'this.refresh' because - // of the parameters passed - jQuery.proxy(function() {this.refresh();},this) - ); - return; - } - if (typeof _type == 'undefined') _type = 'edit'; - if (typeof _row_ids == 'string' || typeof _row_ids == 'number') _row_ids = [_row_ids]; - if (typeof _row_ids == "undefined" || _row_ids === null) - { - this.applyFilters(); - - // Trigger an event so app code can act on it - jQuery(this).triggerHandler("refresh",[this]); - - return; - } - - if(_type == "delete") - { - // Record current & next index - var uid = _row_ids[0].toString().indexOf(this.controller.dataStorePrefix) == 0 ? _row_ids[0] : this.controller.dataStorePrefix + "::" + _row_ids[0]; - var entry = this.controller._selectionMgr._getRegisteredRowsEntry(uid); - var next = (entry.ao?entry.ao.getNext(_row_ids.length):null); - if(next == null || !next.id || next.id == uid) - { - // No next, select previous - next = (entry.ao?entry.ao.getPrevious(1):null); - } - - // Stop automatic updating - this.dataview.grid.doInvalidate = false; - for(var i = 0; i < _row_ids.length; i++) - { - uid = _row_ids[i].toString().indexOf(this.controller.dataStorePrefix) == 0 ? _row_ids[i] : this.controller.dataStorePrefix + "::" + _row_ids[i]; - - // Delete from internal references - this.controller.deleteRow(uid); - } - - // Select & focus next row - if(next && next.id) - { - this.controller._selectionMgr.setSelected(next.id,true); - this.controller._selectionMgr.setFocused(next.id,true); - } - - // Update the count - var total = this.dataview.grid._total - _row_ids.length; - // This will remove the last row! - // That's OK, because grid adds one in this.controller.deleteRow() - this.dataview.grid.setTotalCount(total); - // Re-enable automatic updating - this.dataview.grid.doInvalidate = true; - this.dataview.grid.invalidate(); - } - - id_loop: - for(var i = 0; i < _row_ids.length; i++) - { - var uid = _row_ids[i].toString().indexOf(this.controller.dataStorePrefix) == 0 ? _row_ids[i] : this.controller.dataStorePrefix + "::" + _row_ids[i]; - switch(_type) - { - case "update": - if(!this.egw().dataRefreshUID(uid)) - { - // Could not update just that row - this.applyFilters(); - break id_loop; - } - break; - case "delete": - // Handled above, more code to execute after loop - break; - case "edit": - case "add": - default: - // Trigger refresh - this.applyFilters(); - break id_loop; - } - } - // Trigger an event so app code can act on it - jQuery(this).triggerHandler("refresh",[this,_row_ids,_type]); - }, - - /** - * Gets the selection - * - * @return Object { ids: [UIDs], inverted: boolean} - */ - getSelection: function() { - var selected = this.controller && this.controller._selectionMgr ? this.controller._selectionMgr.getSelected() : null; - if(typeof selected == "object" && selected != null) - { - return selected; - } - return {ids:[],all:false}; - }, - - /** - * Event handler for when the selection changes - * - * If the onselect attribute was set to a string with javascript code, it will - * be executed "legacy style". You can get the selected values with getSelection(). - * If the onselect attribute is in app.appname.function style, it will be called - * with the nextmatch and an array of selected row IDs. - * - * The array can be empty, if user cleared the selection. - * - * @param action ActionObject From action system. Ignored. - * @param senders ActionObjectImplemetation From action system. Ignored. - */ - onselect: function(action,senders) { - // Execute the JS code connected to the event handler - if (typeof this.options.onselect == 'function') - { - return this.options.onselect.call(this, this.getSelection().ids, this); - } - }, - - /** - * Create the dynamic height so nm fills all available space - * - * @returns {undefined} - */ - _getDynheight: function() { - // Find the parent container, either a tab or the main container - var tab = this.get_tab_info(); - - if(!tab) - { - return new et2_dynheight(this.getInstanceManager().DOMContainer, this.innerDiv, 100); - } - - else if (tab && tab.contentDiv) - { - return new et2_dynheight(tab.contentDiv, this.innerDiv, 100); - } - - return false; - }, - - /** - * Generates the column caption for the given column widget - * - * @param {et2_widget} _widget - */ - _genColumnCaption: function(_widget) { - var result = null; - - if(typeof _widget._genColumnCaption == "function") return _widget._genColumnCaption(); - var self = this; - - _widget.iterateOver(function(_widget) { - var label = self.egw().lang(_widget.options.label || _widget.options.empty_label || ''); - if (!label) return; // skip empty, undefined or null labels - if (!result) - { - result = label; - } - else - { - result += ", " + label; - } - }, this, et2_INextmatchHeader); - - return result; - }, - - /** - * Generates the column name (internal) for the given column widget - * Used in preferences to refer to the columns by name instead of position - * - * See _getColumnCaption() for human fiendly captions - * - * @param {et2_widget} _widget - */ - _getColumnName: function(_widget) { - if(typeof _widget._getColumnName == 'function') return _widget._getColumnName(); - - var name = _widget.id; - var child_names = []; - var children = _widget.getChildren(); - for(var i = 0; i < children.length; i++) { - if(children[i].id) child_names.push(children[i].id); - } - - var colName = name + (name != "" && child_names.length > 0 ? "_" : "") + child_names.join("_"); - if(colName == "") { - this.egw().debug("info", "Unable to generate nm column name for ", _widget); - } - return colName; - }, - - - /** - * Retrieve the user's preferences for this nextmatch merged with defaults - * Column display, column size, etc. - */ - _getPreferences: function() { - // Read preference or default for column visibility - var negated = false; - var columnPreference = ""; - if(this.options.settings.default_cols) - { - negated = this.options.settings.default_cols[0] == "!"; - columnPreference = negated ? this.options.settings.default_cols.substring(1) : this.options.settings.default_cols; - } - if(this.options.settings.selectcols && this.options.settings.selectcols.length) - { - columnPreference = this.options.settings.selectcols; - negated = false; - } - if(!this.options.settings.columnselection_pref) - { - // Set preference name so changes are saved - this.options.settings.columnselection_pref = this.options.template; - } - - var app = ''; - var list = []; - if(this.options.settings.columnselection_pref) { - var pref = {}; - list = et2_csvSplit(this.options.settings.columnselection_pref, 2, "."); - if(this.options.settings.columnselection_pref.indexOf('nextmatch') == 0) - { - app = list[0].substring('nextmatch'.length+1); - pref = egw.preference(this.options.settings.columnselection_pref, app); - } - else - { - app = list[0]; - // 'nextmatch-' prefix is there in preference name, but not in setting, so add it in - pref = egw.preference("nextmatch-"+this.options.settings.columnselection_pref, app); - } - if(pref) - { - negated = (pref[0] == "!"); - columnPreference = negated ? pref.substring(1) : pref; - } - } - - // If no column preference or default set, use all columns - if(typeof columnPreference =="string" && columnPreference.length == 0) - { - columnDisplay = {}; - negated = true; - } - - var columnDisplay = typeof columnPreference === "string" - ? et2_csvSplit(columnPreference,null,",") : columnPreference; - - // Adjusted column sizes - var size = {}; - if(this.options.settings.columnselection_pref && app) - { - var size_pref = this.options.settings.columnselection_pref +"-size"; - - // If columnselection pref is missing prefix, add it in - if(size_pref.indexOf('nextmatch') == -1) - { - size_pref = 'nextmatch-'+size_pref; - } - size = this.egw().preference(size_pref, app); - } - if(!size) size = {}; - - // Column order - var order = {}; - for(var i = 0; i < columnDisplay.length; i++) - { - order[columnDisplay[i]] = i; - } - return { - visible: columnDisplay, - visible_negated: negated, - size: size, - order: order - }; - }, - - /** - * Apply stored user preferences to discovered columns - * - * @param {array} _row - * @param {array} _colData - */ - _applyUserPreferences: function(_row, _colData) { - var prefs = this._getPreferences(); - var columnDisplay = prefs.visible; - var size = prefs.size; - var negated = prefs.visible_negated; - var order = prefs.order; - var colName = ''; - - // Add in display preferences - if(columnDisplay && columnDisplay.length > 0) - { - RowLoop: - for(var i = 0; i < _row.length; i++) - { - colName = ''; - if(_row[i].disabled === true) - { - _colData[i].visible = false; - continue; - } - - // Customfields needs special processing - if(_row[i].widget.instanceOf(et2_nextmatch_customfields)) - { - // Find cf field - for(var j = 0; j < columnDisplay.length; j++) - { - if(columnDisplay[j].indexOf(_row[i].widget.id) == 0) { - _row[i].widget.options.fields = {}; - for(var k = j; k < columnDisplay.length; k++) - { - if(columnDisplay[k].indexOf(_row[i].widget.prefix) == 0) - { - _row[i].widget.options.fields[columnDisplay[k].substr(1)] = true; - } - } - // Resets field visibility too - _row[i].widget._getColumnName(); - _colData[i].visible = !(negated || jQuery.isEmptyObject(_row[i].widget.options.fields)); - break; - } - } - // Disable if there are no custom fields - if(jQuery.isEmptyObject(_row[i].widget.customfields)) - { - _colData[i].visible = false; - continue; - } - colName = _row[i].widget.id; - } - else - { - colName = this._getColumnName(_row[i].widget); - } - if(!colName) continue; - - if(size[colName]) - { - // Make sure percentages stay percentages, and forget any preference otherwise - if(_colData[i].width.charAt(_colData[i].width.length - 1) == "%") - { - _colData[i].width = typeof size[colName] == 'string' && size[colName].charAt(size[colName].length - 1) == "%" ? size[colName] : _colData[i].width; - } - else - { - _colData[i].width = parseInt(size[colName])+'px'; - } - } - if(!negated) - { - _colData[i].order = typeof order[colName] === 'undefined' ? i : order[colName]; - } - for(var j = 0; j < columnDisplay.length; j++) - { - if(columnDisplay[j] == colName) - { - _colData[i].visible = !negated; - - continue RowLoop; - } - } - _colData[i].visible = negated; - } - } - - _colData.sort(function(a,b) { - return a.order - b.order; - }); - _row.sort(function(a,b) { - if(typeof a.colData !== 'undefined' && typeof b.colData !== 'undefined') - { - return a.colData.order - b.colData.order; - } - else if (typeof a.order !== 'undefined' && typeof b.order !== 'undefined') - { - return a.order - b.order; - } - }); - }, - - /** - * Take current column display settings and store them in this.egw().preferences - * for next time - */ - _updateUserPreferences: function() { - var colMgr = this.dataview.getColumnMgr(); - var app = ""; - if(!this.options.settings.columnselection_pref) { - this.options.settings.columnselection_pref = this.options.template; - } - - var visibility = colMgr.getColumnVisibilitySet(); - var colDisplay = []; - var colSize = {}; - var custom_fields = []; - - // visibility is indexed by internal ID, widget is referenced by position, preference needs name - for(var i = 0; i < colMgr.columns.length; i++) - { - var widget = this.columns[i].widget; - var colName = this._getColumnName(widget); - if(colName) { - // Server side wants each cf listed as a seperate column - if(widget.instanceOf(et2_nextmatch_customfields)) - { - // Just the ID for server side, not the whole nm name - some apps use it to skip custom fields - colName = widget.id; - for(var name in widget.options.fields) { - if(widget.options.fields[name]) custom_fields.push(widget.prefix+name); - } - } - if(visibility[colMgr.columns[i].id].visible) colDisplay.push(colName); - - // When saving sizes, only save columns with explicit values, preserving relative vs fixed - // Others will be left to flex if width changes or more columns are added - if(colMgr.columns[i].relativeWidth) - { - colSize[colName] = (colMgr.columns[i].relativeWidth * 100) + "%"; - } - else if (colMgr.columns[i].fixedWidth) - { - colSize[colName] = colMgr.columns[i].fixedWidth; - } - } else if (colMgr.columns[i].fixedWidth || colMgr.columns[i].relativeWidth) { - this.egw().debug("info", "Could not save column width - no name", colMgr.columns[i].id); - } - } - - var list = et2_csvSplit(this.options.settings.columnselection_pref, 2, "."); - var pref = this.options.settings.columnselection_pref; - if(pref.indexOf('nextmatch') == 0) - { - app = list[0].substring('nextmatch'.length+1); - } - else - { - app = list[0]; - // 'nextmatch-' prefix is there in preference name, but not in setting, so add it in - pref = "nextmatch-"+this.options.settings.columnselection_pref; - } - - // Server side wants each cf listed as a seperate column - jQuery.merge(colDisplay, custom_fields); - - // Update query value, so data source can use visible columns to exclude expensive sub-queries - var oldCols = this.activeFilters.selectcols ? this.activeFilters.selectcols : []; - - this.activeFilters.selectcols = this.sortedColumnsList ? this.sortedColumnsList : colDisplay; - - // We don't need to re-query if they've removed a column - var changed = []; - ColLoop: - for(var i = 0; i < colDisplay.length; i++) - { - for(var j = 0; j < oldCols.length; j++) { - if(colDisplay[i] == oldCols[j]) continue ColLoop; - } - changed.push(colDisplay[i]); - } - - // If a custom field column was added, throw away cache to deal with - // efficient apps that didn't send all custom fields in the first request - var cf_added = jQuery(changed).filter(jQuery(custom_fields)).length > 0; - - // Save visible columns - // 'nextmatch-' prefix is there in preference name, but not in setting, so add it in - this.egw().set_preference(app, pref, this.activeFilters.selectcols.join(","), - // Use callback after the preference gets set to trigger refresh, in case app - // isn't looking at selectcols and just uses preference - cf_added ? jQuery.proxy(function() {if(this.controller) this.controller.update(true);}, this):null - ); - - // Save adjusted column sizes - this.egw().set_preference(app, pref+"-size", colSize); - - // No significant change (just normal columns shown) and no need to wait, - // but the grid still needs to be redrawn if a custom field was removed because - // the cell content changed. This is a cheaper refresh than the callback, - // this.controller.update(true) - if((changed.length || custom_fields.length) && !cf_added) this.applyFilters(); - }, - - _parseHeaderRow: function(_row, _colData) { - - // Make sure there's a widget - cols disabled in template can be missing them, and the header really likes to have a widget - - for (var x = 0; x < _row.length; x++) - { - if(!_row[x].widget) - { - _row[x].widget = et2_createWidget("label"); - } - } - - // Get column display preference - this._applyUserPreferences(_row, _colData); - - // Go over the header row and create the column entries - this.columns = new Array(_row.length); - var columnData = new Array(_row.length); - - // No action columns in et2 - var remove_action_index = null; - - for (var x = 0; x < _row.length; x++) - { - this.columns[x] = jQuery.extend({ - "order": _colData[x] && typeof _colData[x].order !== 'undefined' ? _colData[x].order : x, - "widget": _row[x].widget - },_colData[x]); - - var visibility = (!_colData[x] || _colData[x].visible) ? - ET2_COL_VISIBILITY_VISIBLE : - ET2_COL_VISIBILITY_INVISIBLE; - if(_colData[x].disabled && _colData[x].disabled !=='' && - this.getArrayMgr("content").parseBoolExpression(_colData[x].disabled)) - { - visibility = ET2_COL_VISIBILITY_DISABLED; - } - columnData[x] = { - "id": "col_" + x, - "order": this.columns[x].order, - "caption": this._genColumnCaption(_row[x].widget), - "visibility": visibility, - "width": _colData[x] ? _colData[x].width : 0 - }; - if(_colData[x].width === 'auto') - { - // Column manager does not understand 'auto', which grid widget - // uses if width is not set - columnData[x].width = '100%'; - } - if(_colData[x].minWidth) - { - columnData[x].minWidth = _colData[x].minWidth; - } - if(_colData[x].maxWidth) - { - columnData[x].maxWidth = _colData[x].maxWidth; - } - - // No action columns in et2 - var colName = this._getColumnName(_row[x].widget); - if(colName == 'actions' || colName == 'legacy_actions' || colName == 'legacy_actions_check_all') - { - remove_action_index = x; - continue; - } - else if (!colName) - { - // Unnamed column cannot be toggled or saved - columnData[x].visibility = ET2_COL_VISIBILITY_ALWAYS_NOSELECT; - } - - } - - // Remove action column - if(remove_action_index != null) - { - this.columns.splice(remove_action_index,remove_action_index); - columnData.splice(remove_action_index,remove_action_index); - _colData.splice(remove_action_index,remove_action_index); - } - - // Create the column manager and update the grid container - this.dataview.setColumns(columnData); - - for (var x = 0; x < _row.length; x++) - { - // Append the widget to this container - this.addChild(_row[x].widget); - } - - // Create the nextmatch row provider - this.rowProvider = new et2_nextmatch_rowProvider( - this.dataview.rowProvider, this._getSubgrid, this); - - // Register handler to update preferences when column properties are changed - var self = this; - this.dataview.onUpdateColumns = function() { - // Use apply to make sure context is there - self._updateUserPreferences.apply(self); - - // Allow column widgets a chance to resize - self.iterateOver(function(widget) {widget.resize();}, self, et2_IResizeable); - }; - - // Register handler for column selection popup, or disable - if(this.selectPopup) - { - this.selectPopup.remove(); - this.selectPopup = null; - } - if(this.options.settings.no_columnselection) - { - this.dataview.selectColumnsClick = function() {return false;}; - jQuery('span.selectcols',this.dataview.headTr).hide(); - } - else - { - jQuery('span.selectcols',this.dataview.headTr).show(); - this.dataview.selectColumnsClick = function(event) { - self._selectColumnsClick(event); - }; - } - }, - - _parseDataRow: function(_row, _rowData, _colData) { - var columnWidgets = new Array(this.columns.length); - - _row.sort(function(a,b) { - return a.colData.order - b.colData.order; - }); - - for (var x = 0; x < columnWidgets.length; x++) - { - if (typeof _row[x] != "undefined" && _row[x].widget) - { - columnWidgets[x] = _row[x].widget; - - // Append the widget to this container - this.addChild(_row[x].widget); - } - else - { - columnWidgets[x] = _row[x].widget; - } - // Pass along column alignment - if(_row[x].align && columnWidgets[x]) - { - columnWidgets[x].align = _row[x].align; - } - } - - this.rowProvider.setDataRowTemplate(columnWidgets, _rowData, this); - - - // Create the grid controller - this.controller = new et2_nextmatch_controller( - null, - this.egw(), - this.getInstanceManager().etemplate_exec_id, - this, - null, - this.dataview.grid, - this.rowProvider, - this.options.settings.action_links, - null, - this.options.actions - ); - - // Need to trigger empty row the first time - if(total == 0) this.controller._emptyRow(); - - // Set data cache prefix to either provided custom or auto - if(!this.options.settings.dataStorePrefix && this.options.settings.get_rows) - { - // Use jsapi data module to update - var list = this.options.settings.get_rows.split('.', 2); - if (list.length < 2) list = this.options.settings.get_rows.split('_'); // support "app_something::method" - this.options.settings.dataStorePrefix = list[0]; - } - this.controller.setPrefix(this.options.settings.dataStorePrefix); - - // Set the view - this.controller._view = this.view; - - // Load the initial order - /*this.controller.loadInitialOrder(this._getInitialOrder( - this.options.settings.rows, this.options.settings.row_id - ));*/ - - // Set the initial row count - var total = typeof this.options.settings.total != "undefined" ? - this.options.settings.total : 0; - // This triggers an invalidate, which updates the grid - this.dataview.grid.setTotalCount(total); - - // Insert any data sent from server, so invalidate finds data already - if(this.options.settings.rows && this.options.settings.num_rows) - { - this.controller.loadInitialData( - this.options.settings.dataStorePrefix, - this.options.settings.row_id, - this.options.settings.rows - ); - // Remove, to prevent duplication - delete this.options.settings.rows; - } - }, - - _parseGrid: function(_grid) { - // Search the rows for a header-row - if one is found, parse it - for (var y = 0; y < _grid.rowData.length; y++) - { - // Parse the first row as a header, need header to parse the data rows - if (_grid.rowData[y]["class"] == "th" || y == 0) - { - this._parseHeaderRow(_grid.cells[y], _grid.colData); - } - else - { - this._parseDataRow(_grid.cells[y], _grid.rowData[y], - _grid.colData); - } - } - this.dataview.table.resize(); - }, - - _getSubgrid: function (_row, _data, _controller) { - // Fetch the id of the element described by _data, this will be the - // parent_id of the elements in the subgrid - var rowId = _data.content[this.options.settings.row_id]; - - // Create a new grid with the row as parent and the dataview grid as - // parent grid - var grid = new et2_dataview_grid(_row, this.dataview.grid); - - // Create a new controller for the grid - var controller = new et2_nextmatch_controller( - _controller, - this.egw(), - this.getInstanceManager().etemplate_exec_id, - this, - rowId, - grid, - this.rowProvider, - this.options.settings.action_links, - _controller.getObjectManager() - ); - controller.update(); - - // Register inside the destruction callback of the grid - grid.setDestroyCallback(function () { - controller.free(); - }); - - return grid; - }, - - _getInitialOrder: function (_rows, _rowId) { - - var _order = []; - - // Get the length of the non-numerical rows arra - var len = 0; - for (var key in _rows) { - if (!isNaN(key) && parseInt(key) > len) - len = parseInt(key); - } - - // Iterate over the rows - for (var i = 0; i < len; i++) - { - // Get the uid from the data - var uid = this.egw().appName + '::' + _rows[i][_rowId]; - - // Store the data for that uid - this.egw().dataStoreUID(uid, _rows[i]); - - // Push the uid onto the order array - _order.push(uid); - } - - return _order; - }, - - _selectColumnsClick: function(e) { - var self = this; - var columnMgr = this.dataview.getColumnMgr(); - - // ID for faking letter selection in column selection - var LETTERS = '~search_letter~'; - - var columns = {}; - var columns_selected = []; - - for (var i = 0; i < columnMgr.columns.length; i++) - { - var col = columnMgr.columns[i]; - var widget = this.columns[i].widget; - - if(col.caption && col.visibility !== ET2_COL_VISIBILITY_ALWAYS_NOSELECT && - col.visibility !== ET2_COL_VISIBILITY_DISABLED) - { - columns[col.id] = col.caption; - if(col.visibility == ET2_COL_VISIBILITY_VISIBLE) columns_selected.push(col.id); - } - // Custom fields get listed separately - if(widget.instanceOf(et2_nextmatch_customfields)) - { - if(jQuery.isEmptyObject(widget.customfields)) - { - // No customfields defined, don't show column - delete(columns[col.id]); - continue; - } - for(var field_name in widget.customfields) - { - columns[widget.prefix+field_name] = " - "+widget.customfields[field_name].label; - if(widget.options.fields[field_name]) columns_selected.push(et2_customfields_list.prototype.prefix+field_name); - } - } - } - - // Letter search - if(this.options.settings.lettersearch) - { - columns[LETTERS] = egw.lang('Search letter'); - if(this.header.lettersearch.is(':visible')) columns_selected.push(LETTERS); - } - - // Build the popup - if(!this.selectPopup) - { - var select = et2_createWidget("select", { - multiple: true, - rows: 8, - empty_label:this.egw().lang("select columns"), - selected_first: false, - value_class:"selcolumn_sortable_" - }, this); - select.set_select_options(columns); - select.set_value(columns_selected); - - var autoRefresh = et2_createWidget("select", { - "empty_label":"Refresh" - }, this); - autoRefresh.set_id("nm_autorefresh"); - autoRefresh.set_select_options({ - // Cause [unknown] problems with mail - //30: "30 seconds", - //60: "1 Minute", - 300: "5 Minutes", - 900: "15 Minutes", - 1800: "30 Minutes" - }); - autoRefresh.set_value(this._get_autorefresh()); - autoRefresh.set_statustext(egw.lang("Automatically refresh list")); - - var defaultCheck = et2_createWidget("select", {"empty_label":"Preference"}, this); - defaultCheck.set_id('nm_col_preference'); - defaultCheck.set_select_options({ - 'default': {label: 'Default',title:'Set these columns as the default'}, - 'reset': {label: 'Reset', title:"Reset all user's column preferences"}, - 'force': {label: 'Force', title:'Force column preference so users cannot change it'} - }); - defaultCheck.set_value(this.options.settings.columns_forced ? 'force': ''); - - var okButton = et2_createWidget("buttononly", {"background_image":true, image:"check"}, this); - okButton.set_label(this.egw().lang("ok")); - okButton.onclick = function() { - // Update visibility - var visibility = {}; - for (var i = 0; i < columnMgr.columns.length; i++) - { - var col = columnMgr.columns[i]; - if(col.caption && col.visibility !== ET2_COL_VISIBILITY_ALWAYS_NOSELECT && - col.visibility !== ET2_COL_VISIBILITY_DISABLED ) - { - visibility[col.id] = {visible: false}; - } - } - var value = select.getValue(); - - // Update & remove letter filter - if(self.header.lettersearch) - { - var show_letters = true; - if(value.indexOf(LETTERS) >= 0) - { - value.splice(value.indexOf(LETTERS),1); - } - else - { - show_letters = false; - } - self._set_lettersearch(show_letters); - } - - var column = 0; - for(var i = 0; i < value.length; i++) - { - // Handle skipped columns - while(value[i] != "col_"+column && column < columnMgr.columns.length) - { - column++; - } - if(visibility[value[i]]) - { - visibility[value[i]].visible = true; - } - // Custom fields are listed seperately in column list, but are only 1 column - if(self.columns[column] && self.columns[column].widget.instanceOf(et2_nextmatch_customfields)) { - var cf = self.columns[column].widget.options.customfields; - var visible = self.columns[column].widget.options.fields; - - // Turn off all custom fields - for(var field_name in cf) - { - visible[field_name] = false; - } - // Turn on selected custom fields - start from 0 in case they're not in order - for(var j = 0; j < value.length; j++) - { - if(value[j].indexOf(et2_customfields_list.prototype.prefix) != 0) continue; - visible[value[j].substring(1)] = true; - i++; - } - self.columns[column].widget.set_visible(visible); - } - } - columnMgr.setColumnVisibilitySet(visibility); - - this.sortedColumnsList = []; - jQuery(select.getDOMNode()).find('li[class^="selcolumn_sortable_"]').each(function(i,v){ - var data_id = v.getAttribute('data-value'); - var value = select.getValue(); - if (data_id.match(/^col_/) && value.indexOf(data_id) != -1) - { - var col_id = data_id.replace('col_','') - var col_widget = self.columns[col_id].widget; - if (col_widget.customfields) - { - self.sortedColumnsList.push(col_widget.id); - for(var field_name in col_widget.customfields) - { - if(jQuery.isEmptyObject(col_widget.options.fields) || col_widget.options.fields[field_name] == true) - { - self.sortedColumnsList.push(et2_customfields_list.prototype.prefix + field_name); - } - } - } - else - { - self.sortedColumnsList.push(self._getColumnName(col_widget)); - } - } - }); - - // Hide popup - self.selectPopup.toggle(); - - self.dataview.updateColumns(); - - // Auto refresh - self._set_autorefresh(autoRefresh.get_value()); - - // Set default or clear forced - if(show_letters) - { - self.activeFilters.selectcols.push('lettersearch'); - } - self.getInstanceManager().submit(); - - self.selectPopup = null; - }; - - var cancelButton = et2_createWidget("buttononly", {"background_image":true, image:"cancel"}, this); - cancelButton.set_label(this.egw().lang("cancel")); - cancelButton.onclick = function() { - self.selectPopup.toggle(); - self.selectPopup = null; - }; - var $select = jQuery(select.getDOMNode()); - $select.find('.ui-multiselect-checkboxes').sortable({ - placeholder:'ui-fav-sortable-placeholder', - items:'li[class^="selcolumn_sortable_col"]', - cancel: 'li[class^="selcolumn_sortable_#"]', - cursor: "move", - tolerance: "pointer", - axis: 'y', - containment: "parent", - delay: 250, //(millisecond) delay before the sorting should start - beforeStop: function(event, ui) { - jQuery('li[class^="selcolumn_sortable_#"]', this).css({ - opacity: 1 - }); - }, - start: function(event, ui){ - jQuery('li[class^="selcolumn_sortable_#"]', this).css({ - opacity: 0.5 - }); - }, - sort: function (event, ui) - { - jQuery( this ).sortable("refreshPositions" ); - } - }); - $select.disableSelection(); - $select.find('li[class^="selcolumn_sortable_"]').each(function(i,v){ - jQuery(v).attr('data-value',(jQuery(v).find('input')[0].value)) - }); - var $footerWrap = jQuery(document.createElement("div")) - .addClass('dialogFooterToolbar') - .append(okButton.getDOMNode()) - .append(cancelButton.getDOMNode()); - this.selectPopup = jQuery(document.createElement("div")) - .addClass("colselection ui-dialog ui-widget-content") - .append(select.getDOMNode()) - .append($footerWrap) - .appendTo(this.innerDiv); - - // Add autorefresh - $footerWrap.append(autoRefresh.getSurroundings().getDOMNode(autoRefresh.getDOMNode())); - - // Add default checkbox for admins - var apps = this.egw().user('apps'); - if(apps['admin']) - { - $footerWrap.append(defaultCheck.getSurroundings().getDOMNode(defaultCheck.getDOMNode())); - } - } - else - { - this.selectPopup.toggle(); - } - var t_position = jQuery(e.target).position(); - var s_position = this.div.position(); - var max_height = this.getDOMNode().getElementsByClassName('egwGridView_outer')[0]['tBodies'][0].clientHeight - - (2 * this.selectPopup.find('.dialogFooterToolbar').height()); - this.selectPopup.find('.ui-multiselect-checkboxes').css('max-height',max_height); - this.selectPopup.css("top", t_position.top) - .css("left", s_position.left + this.div.width() - this.selectPopup.width()); - }, - - /** - * Set the currently displayed columns, without updating user's preference - * - * @param {string[]} column_list List of column names - * @param {boolean} trigger_update =false - explicitly trigger an update - */ - set_columns: function(column_list, trigger_update) - { - var columnMgr = this.dataview.getColumnMgr(); - var visibility = {}; - - // Initialize to false - for (var i = 0; i < columnMgr.columns.length; i++) - { - var col = columnMgr.columns[i]; - if(col.caption && col.visibility != ET2_COL_VISIBILITY_ALWAYS_NOSELECT ) - { - visibility[col.id] = {visible: false}; - } - } - for(var i = 0; i < this.columns.length; i++) - { - - var widget = this.columns[i].widget; - var colName = this._getColumnName(widget); - if(column_list.indexOf(colName) !== -1 && - typeof visibility[columnMgr.columns[i].id] !== 'undefined' - ) - { - visibility[columnMgr.columns[i].id].visible = true; - } - // Custom fields are listed seperately in column list, but are only 1 column - if(widget && widget.instanceOf(et2_nextmatch_customfields)) { - - // Just the ID for server side, not the whole nm name - some apps use it to skip custom fields - colName = widget.id; - if(column_list.indexOf(colName) !== -1) - { - visibility[columnMgr.columns[i].id].visible = true; - } - - var cf = this.columns[i].widget.options.customfields; - var visible = this.columns[i].widget.options.fields; - - // Turn off all custom fields - for(var field_name in cf) - { - visible[field_name] = false; - } - // Turn on selected custom fields - start from 0 in case they're not in order - for(var j = 0; j < column_list.length; j++) - { - if(column_list[j].indexOf(et2_customfields_list.prototype.prefix) != 0) continue; - visible[column_list[j].substring(1)] = true; - } - widget.set_visible(visible); - } - } - columnMgr.setColumnVisibilitySet(visibility); - - // We don't want to update user's preference, so directly update - this.dataview._updateColumns(); - - // Allow column widgets a chance to resize - this.iterateOver(function(widget) {widget.resize();}, this, et2_IResizeable); - }, - - /** - * Set the letter search preference, and update the UI - * - * @param {boolean} letters_on - */ - _set_lettersearch: function(letters_on) { - if(letters_on) - { - this.header.lettersearch.show(); - } - else - { - this.header.lettersearch.hide(); - } - var lettersearch_preference = "nextmatch-" + this.options.settings.columnselection_pref + "-lettersearch"; - this.egw().set_preference(this.egw().getAppName(),lettersearch_preference,letters_on); - }, - - /** - * Set the auto-refresh time period, and starts the timer if not started - * - * @param time int Refresh period, in seconds - */ - _set_autorefresh: function(time) { - // Store preference - var refresh_preference = "nextmatch-" + this.options.settings.columnselection_pref + "-autorefresh"; - var app = this.options.template.split("."); - if(this._get_autorefresh() != time) - { - this.egw().set_preference(app[0],refresh_preference,time); - } - - // Start / update timer - if (this._autorefresh_timer) - { - window.clearInterval(this._autorefresh_timer); - delete this._autorefresh_timer; - } - if(time > 0) - { - this._autorefresh_timer = setInterval(jQuery.proxy(this.controller.update, this.controller), time * 1000); - - // Bind to tab show/hide events, so that we don't bother refreshing in the background - jQuery(this.getInstanceManager().DOMContainer.parentNode).on('hide.et2_nextmatch', jQuery.proxy(function(e) { - // Stop - window.clearInterval(this._autorefresh_timer); - jQuery(e.target).off(e); - - // If the autorefresh time is up, bind once to trigger a refresh - // (if needed) when tab is activated again - this._autorefresh_timer = setTimeout(jQuery.proxy(function() { - // Check in case it was stopped / destroyed since - if(!this._autorefresh_timer || !this.getInstanceManager()) return; - - jQuery(this.getInstanceManager().DOMContainer.parentNode).one('show.et2_nextmatch', - // Important to use anonymous function instead of just 'this.refresh' because - // of the parameters passed - jQuery.proxy(function() {this.refresh();},this) - ); - },this), time*1000); - },this)); - jQuery(this.getInstanceManager().DOMContainer.parentNode).on('show.et2_nextmatch', jQuery.proxy(function(e) { - // Start normal autorefresh timer again - this._set_autorefresh(this._get_autorefresh()); - jQuery(e.target).off(e); - },this)); - } - }, - - /** - * Get the auto-refresh timer - * - * @return int Refresh period, in secods - */ - _get_autorefresh: function() { - var refresh_preference = "nextmatch-" + this.options.settings.columnselection_pref + "-autorefresh"; - var app = this.options.template.split("."); - return this.egw().preference(refresh_preference,app[0]); - }, - - /** - * When the template attribute is set, the nextmatch widget tries to load - * that template and to fetch the grid which is inside of it. It then calls - * - * @param {string} _value template name - */ - set_template: function(_value) { - if(this.template) - { - // Stop early to prevent unneeded processing, and prevent infinite - // loops if the server changes the template in get_rows - if(this.template == _value) - { - return; - } - - // Free the grid components - they'll be re-created as the template is processed - this.dataview.free(); - this.rowProvider.free(); - this.controller.free(); - - // Free any children from previous template - // They may get left behind because of how detached nodes are processed - // We don't use iterateOver because it checks sub-children - for(var i = this._children.length-1; i >=0 ; i--) - { - var _node = this._children[i]; - if(_node != this.header) { - this.removeChild(_node); - _node.destroy(); - } - } - - // Clear this setting if it's the same as the template, or - // the columns will not be loaded - if(this.template == this.options.settings.columnselection_pref) - { - this.options.settings.columnselection_pref = _value; - } - this.dataview = new et2_dataview(this.innerDiv, this.egw()); - } - - // Create the template - var template = et2_createWidget("template", {"id": _value}, this); - - if (!template) - { - this.egw().debug("error", "Error while loading definition template for " + - "nextmatch widget.",_value); - return; - } - - if(this.options.disabled) - { - return; - } - - // Deferred parse function - template might not be fully loaded - var parse = function(template) - { - // Keep the name of the template, as we'll free up the widget after parsing - this.template = _value; - - // Fetch the grid element and parse it - var definitionGrid = template.getChildren()[0]; - if (definitionGrid && definitionGrid instanceof et2_grid) - { - this._parseGrid(definitionGrid); - } - else - { - this.egw().debug("error", "Nextmatch widget expects a grid to be the " + - "first child of the defined template."); - return; - } - - // Free the template again, but don't remove it - setTimeout(function() { - template.free(); - },1); - - // Call the "setNextmatch" function of all registered - // INextmatchHeader widgets. This updates this.activeFilters.col_filters according - // to what's in the template. - this.iterateOver(function (_node) { - _node.setNextmatch(this); - }, this, et2_INextmatchHeader); - - // Set filters to current values - this.controller.setFilters(this.activeFilters); - - // If no data was sent from the server, and num_rows is 0, the nm will be empty. - // This triggers a cache check. - if(!this.options.settings.num_rows) - { - this.controller.update(); - } - - // Load the default sort order - if (this.options.settings.order && this.options.settings.sort) - { - this.sortBy(this.options.settings.order, - this.options.settings.sort == "ASC", false); - } - - // Start auto-refresh - this._set_autorefresh(this._get_autorefresh()); - }; - - // Template might not be loaded yet, defer parsing - var promise = []; - template.loadingFinished(promise); - - // Wait until template (& children) are done - jQuery.when.apply(null, promise).done( - jQuery.proxy(function() { - parse.call(this, template); - if(!this.dynheight) - { - this.dynheight = this._getDynheight(); - } - this.dynheight.initialized = false; - this.resize(); - }, this) - ); - }, - - // Some accessors to match conventions - set_hide_header: function(hide) { - (hide ? this.header.div.hide() : this.header.div.show()); - }, - - set_header_left: function(template) { - this.header._build_header("left",template); - }, - set_header_right: function(template) { - this.header._build_header("right",template); - }, - set_header_row: function(template) { - this.header._build_header("row",template); - }, - set_no_filter: function(bool, filter_name) { - if(typeof filter_name == 'undefined') - { - filter_name = 'filter'; - } - this.options['no_'+filter_name] = bool; - - var filter = this.header[filter_name]; - if(filter) - { - filter.set_disabled(bool); - } - else if (bool) - { - filter = this.header._build_select(filter_name, 'select', - this.settings[filter_name], this.settings[filter_name+'_no_lang']); - } - }, - set_no_filter2: function(bool) { - this.set_no_filter(bool,'filter2'); - }, - - /** - * Directly change filter value, with no server query. - * - * This allows the server app code to change filter value, and have it - * updated in the client UI. - * - * @param {String|number} value - */ - set_filter: function(value) { - var update = this.update_in_progress; - this.update_in_progress = true; - - this.activeFilters.filter = value; - - // Update the header - this.header.setFilters(this.activeFilters); - - this.update_in_progress = update; - }, - - /** - * Directly change filter2 value, with no server query. - * - * This allows the server app code to change filter2 value, and have it - * updated in the client UI. - * - * @param {String|number} value - */ - set_filter2: function(value) { - var update = this.update_in_progress; - this.update_in_progress = true; - - this.activeFilters.filter2 = value; - - // Update the header - this.header.setFilters(this.activeFilters); - - this.update_in_progress = update; - }, - - /** - * If nextmatch starts disabled, it will need a resize after being shown - * to get all the sizing correct. Override the parent to add the resize - * when enabling. - * - * @param {boolean} _value - */ - set_disabled: function(_value) - { - var previous = this.disabled; - this._super.apply(this, arguments); - - if(previous && !_value) - { - this.resize(); - } - }, - - /** - * Actions are handled by the controller, so ignore these during init. - * - * @param {object} actions - */ - set_actions: function(actions) { - if(actions != this.options.actions && this.controller != null && this.controller._actionManager) - { - for(var i = this.controller._actionManager.children.length - 1; i >= 0; i--) - { - this.controller._actionManager.children[i].remove(); - } - this.options.actions = actions; - this.options.settings.action_links = this.controller._actionLinks = this._get_action_links(actions); - - this.controller._initActions(actions); - } - }, - - /** - * Switch view between row and tile. - * This should be followed by a call to change the template to match, which - * will cause a reload of the grid using the new settings. - * - * @param {string} view Either 'tile' or 'row' - */ - set_view: function(view) - { - // Restrict to the only 2 accepted values - if(view == 'tile') - { - this.view = 'tile'; - } - else - { - this.view = 'row'; - } - }, - - /** - * Set a different / additional handler for dropped files. - * - * File dropping doesn't work with the action system, so we handle it in the - * nextmatch by linking automatically to the target row. This allows an additional handler. - * It should accept a row UID and a File[], and return a boolean Execute the default (link) action - * - * @param {String|Function} handler - */ - set_onfiledrop: function(handler) { - this.options.onfiledrop = handler; - }, - - /** - * Handle drops of files by linking to the row, if possible. - * - * HTML5 / native file drops conflict with jQueryUI draggable, which handles - * all our drop actions. So we side-step the issue by registering an additional - * drop handler on the rows parent. If the row/actions itself doesn't handle - * the drop, it should bubble and get handled here. - * - * @param {object} event - * @param {object} target - */ - handle_drop: function(event, target) { - // Check to see if we can handle the link - // First, find the UID - var row = this.controller.getRowByNode(target); - var uid = row.uid || null; - - // Get the file information - var files = []; - if(event.originalEvent && event.originalEvent.dataTransfer && - event.originalEvent.dataTransfer.files && event.originalEvent.dataTransfer.files.length > 0) - { - files = event.originalEvent.dataTransfer.files; - } - else - { - return false; - } - - // Exectute the custom handler code - if (this.options.onfiledrop && !this.options.onfiledrop.call(this, uid, files)) - { - return false; - } - event.stopPropagation(); - event.preventDefault(); - - if(!row || !row.uid) return false; - - // Link the file to the row - // just use a link widget, it's all already done - var split = uid.split('::'); - var link_value = { - to_app: split.shift(), - to_id: split.join('::') - }; - // Create widget and mangle to our needs - var link = et2_createWidget("link-to", {value: link_value}, this); - link.loadingFinished(); - link.file_upload.set_drop_target(false); - - if(row.row.tr) - { - // Ignore most of the UI, just use the status indicators - var status = jQuery(document.createElement("div")) - .addClass('et2_link_to') - .width(row.row.tr.width()) - .position({my: "left top", at: "left top", of: row.row.tr}) - .append(link.status_span) - .append(link.file_upload.progress) - .appendTo(row.row.tr); - - // Bind to link event so we can remove when done - link.div.on('link.et2_link_to', function(e, linked) { - if(!linked) - { - jQuery("li.success", link.file_upload.progress) - .removeClass('success').addClass('validation_error'); - } - else - { - // Update row - link._parent.refresh(uid,'edit'); - } - // Fade out nicely - status.delay(linked ? 1 : 2000) - .fadeOut(500, function() { - link.free(); - status.remove(); - }); - - }); - } - - // Upload and link - this triggers the upload, which triggers the link, which triggers the cleanup and refresh - link.file_upload.set_value(files); - }, - - getDOMNode: function(_sender) { - if (_sender == this || typeof _sender === 'undefined') - { - return this.div[0]; - } - if (_sender == this.header) - { - return this.header.div[0]; - } - for (var i = 0; i < this.columns.length; i++) - { - if (this.columns[i] && this.columns[i].widget && _sender == this.columns[i].widget) - { - return this.dataview.getHeaderContainerNode(i); - } - } - - // Let header have a chance - if(_sender && _sender._parent && _sender._parent == this) - { - return this.header.getDOMNode(_sender); - } - - return null; - }, - - // Input widget - - /** - * Get the current 'value' for the nextmatch - */ - getValue: function() { - var _ids = this.getSelection(); - - // Translate the internal uids back to server uids - var idsArr = _ids.ids; - for (var i = 0; i < idsArr.length; i++) - { - idsArr[i] = idsArr[i].split("::").pop(); - } - var value = { - "selected": idsArr - }; - jQuery.extend(value, this.activeFilters, this.value); - return value; - }, - resetDirty: function() {}, - isDirty: function() { return typeof this.value !== 'undefined';}, - isValid: function() { return true;}, - set_value: function(_value) - { - this.value = _value; - }, - - // Printing - /** - * Prepare for printing - * - * We check for un-loaded rows, and ask the user what they want to do about them. - * If they want to print them all, we ask the server and print when they're loaded. - */ - beforePrint: function() { - // Add the class, if needed - this.div.addClass('print'); - - // Trigger resize, so we can fit on a page - this.dynheight.outerNode.css('max-width',this.div.css('max-width')); - this.resize(); - // Reset height to auto (after width resize) so there's no restrictions - this.dynheight.innerNode.css('height', 'auto'); - - // Check for rows that aren't loaded yet, or lots of rows - var range = this.controller._grid.getIndexRange(); - this.old_height = this.controller._grid._scrollHeight; - var loaded_count = range.bottom - range.top +1; - var total = this.controller._grid.getTotalCount(); - - // Defer the printing to ask about columns & rows - var defer = jQuery.Deferred(); - - - var pref = this.options.settings.columnselection_pref; - if(pref.indexOf('nextmatch') == 0) - { - pref = 'nextmatch-'+pref; - } - var app = this.getInstanceManager().app; - - var columns = {}; - var columnMgr = this.dataview.getColumnMgr(); - pref += '_print'; - var columns_selected = []; - - // Get column names - for (var i = 0; i < columnMgr.columns.length; i++) - { - var col = columnMgr.columns[i]; - var widget = this.columns[i].widget; - var colName = this._getColumnName(widget); - - if(col.caption && col.visibility !== ET2_COL_VISIBILITY_ALWAYS_NOSELECT && - col.visibility !== ET2_COL_VISIBILITY_DISABLED) - { - columns[colName] = col.caption; - if(col.visibility === ET2_COL_VISIBILITY_VISIBLE) columns_selected.push(colName); - } - // Custom fields get listed separately - if(widget.instanceOf(et2_nextmatch_customfields)) - { - delete(columns[colName]); - colName = widget.id; - if(col.visibility === ET2_COL_VISIBILITY_VISIBLE && ! - jQuery.isEmptyObject(widget.customfields) - ) - { - columns[colName] = col.caption; - for(var field_name in widget.customfields) - { - columns[widget.prefix+field_name] = " - "+widget.customfields[field_name].label; - if(widget.options.fields[field_name] && columns_selected.indexOf(colName) >= 0) - { - columns_selected.push(et2_customfields_list.prototype.prefix+field_name); - } - } - } - } - } - - // Preference exists? Set it now - if(this.egw().preference(pref,app)) - { - this.set_columns(jQuery.extend([],this.egw().preference(pref,app))); - } - - var callback = jQuery.proxy(function(button, value) { - if(button === et2_dialog.CANCEL_BUTTON) { - // Give dialog a chance to close, or it will be in the print - window.setTimeout(function() {defer.reject();}, 0); - return; - } - - // Set CSS for orientation - this.div.addClass(value.orientation); - this.egw().set_preference(app,pref+'_orientation',value.orientation); - - - // Try to tell browser about orientation - var css = '@page { size: '+ value.orientation + '; }', - head = document.head || document.getElementsByTagName('head')[0], - style = document.createElement('style'); - - style.type = 'text/css'; - style.media = 'print'; - - if (style.styleSheet){ - style.styleSheet.cssText = css; - } else { - style.appendChild(document.createTextNode(css)); - } - - head.appendChild(style); - this.orientation_style = style; - - // Trigger resize, so we can fit on a page - this.dynheight.outerNode.css('max-width',this.div.css('max-width')); - - // Handle columns - this.set_columns(value.columns); - this.egw().set_preference(app,pref,value.columns); - - var rows = parseInt(value.row_count); - if(rows > total) - { - rows = total; - } - - // If they want the whole thing, style it as all - if(button === et2_dialog.OK_BUTTON && rows == this.controller._grid.getTotalCount()) - { - // Add the class, gives more reliable sizing - this.div.addClass('print'); - // Show it all - jQuery('.egwGridView_scrollarea',this.div).css('height','auto'); - } - // We need more rows - if(button === 'dialog[all]' || rows > loaded_count) - { - var count = 0; - var fetchedCount = 0; - var cancel = false; - var nm = this; - var dialog = et2_dialog.show_dialog( - // Abort the long task if they canceled the data load - function() {count = total; cancel=true;window.setTimeout(function() {defer.reject();},0);}, - egw.lang('Loading'), egw.lang('please wait...'),{},[ - {"button_id": et2_dialog.CANCEL_BUTTON,"text": 'cancel',id: 'dialog[cancel]',image: 'cancel'} - ] - ); - - // dataFetch() is asyncronous, so all these requests just get fired off... - // 200 rows chosen arbitrarily to reduce requests. - do { - var ctx = { - "self": this.controller, - "start": count, - "count": Math.min(rows,200), - "lastModification": this.controller._lastModification - }; - if(nm.controller.dataStorePrefix) - { - ctx.prefix = nm.controller.dataStorePrefix; - } - nm.controller.dataFetch({start:count, num_rows: Math.min(rows,200)}, function(data) { - // Keep track - if(data && data.order) - { - fetchedCount += data.order.length; - } - nm.controller._fetchCallback.apply(this, arguments); - - if(fetchedCount >= rows) - { - if(cancel) - { - dialog.destroy(); - defer.reject(); - return; - } - // Use CSS to hide all but the requested rows - // Prevents us from showing more than requested, if actual height was less than average - nm.print_row_selector = ".egwGridView_grid > tbody > tr:not(:nth-child(-n+"+rows+"))"; - egw.css(nm.print_row_selector, 'display: none'); - - // No scrollbar in print view - jQuery('.egwGridView_scrollarea',this.div).css('overflow-y','hidden'); - // Show it all - jQuery('.egwGridView_scrollarea',this.div).css('height','auto'); - - // Grid needs to redraw before it can be printed, so wait - window.setTimeout(jQuery.proxy(function() { - dialog.destroy(); - - // Should be OK to print now - defer.resolve(); - },nm),ET2_GRID_INVALIDATE_TIMEOUT); - - } - - },ctx); - count += 200; - } while (count < rows) - nm.controller._grid.setScrollHeight(nm.controller._grid.getAverageHeight() * (rows+1)); - } - else - { - // Don't need more rows, limit to requested and finish - - // Show it all - jQuery('.egwGridView_scrollarea',this.div).css('height','auto'); - - // Use CSS to hide all but the requested rows - // Prevents us from showing more than requested, if actual height was less than average - this.print_row_selector = ".egwGridView_grid > tbody > tr:not(:nth-child(-n+"+rows+"))"; - egw.css(this.print_row_selector, 'display: none'); - - // No scrollbar in print view - jQuery('.egwGridView_scrollarea',this.div).css('overflow-y','hidden'); - // Give dialog a chance to close, or it will be in the print - window.setTimeout(function() {defer.resolve();}, 0); - } - },this); - var value = { - content: { - row_count: Math.min(100,total), - columns: this.egw().preference(pref,app) || columns_selected, - orientation: this.egw().preference(pref+'_orientation',app) - }, - sel_options: { - columns: columns - } - }; - this._create_print_dialog.call(this, value, callback); - - return defer; - }, - - /** - * Create and show the print dialog, which calls the provided callback when - * done. Broken out for overriding if needed. - * - * @param {Object} value Current settings and preferences, passed to the dialog for - * the template - * @param {Object} value.content - * @param {Object} value.sel_options - * - * @param {function(int, Object)} callback - Process the dialog response, - * format things according to the specified orientation and fetch any needed - * rows. - * - */ - _create_print_dialog: function _create_print_dialog(value, callback) - { - var base_url = this.getInstanceManager().template_base_url; - if (base_url.substr(base_url.length - 1) == '/') base_url = base_url.slice (0, -1); // otherwise we generate a url //api/templates, which is wrong - var tab = this.get_tab_info(); - // Get title for print dialog from settings or tab, if available - var title = this.options.settings.label ? this.options.settings.label : (tab ? tab.label : ''); - var dialog = et2_createWidget("dialog",{ - // If you use a template, the second parameter will be the value of the template, as if it were submitted. - callback: callback, // return false to prevent dialog closing - buttons: et2_dialog.BUTTONS_OK_CANCEL, - title: this.egw().lang('Print') + ' ' + this.egw().lang(title), - template:this.egw().link(base_url+'/api/templates/default/nm_print_dialog.xet'), - value: value - }); - }, - - /** - * Try to clean up the mess we made getting ready for printing - * in beforePrint() - */ - afterPrint: function() { - - this.div.removeClass('print landscape portrait'); - jQuery(this.orientation_style).remove(); - delete this.orientation_style; - - // Put scrollbar back - jQuery('.egwGridView_scrollarea',this.div).css('overflow-y',''); - - // Correct size of grid, and trigger resize to fix it - this.controller._grid.setScrollHeight(this.old_height); - delete this.old_height; - - // Remove CSS rule hiding extra rows - if(this.print_row_selector) - { - egw.css(this.print_row_selector, false); - delete this.print_row_selector; - } - - // Restore columns - var pref = []; - var app = this.getInstanceManager().app; - if(this.options.settings.columnselection_pref.indexOf('nextmatch') == 0) - { - pref = egw.preference(this.options.settings.columnselection_pref, app); - } - else - { - // 'nextmatch-' prefix is there in preference name, but not in setting, so add it in - pref = egw.preference("nextmatch-"+this.options.settings.columnselection_pref, app); - } - if(pref) - { - if(typeof pref === 'string') pref = pref.split(','); - this.set_columns(pref,app); - } - this.dynheight.outerNode.css('max-width','inherit'); - this.resize(); - } -});}).call(this); -et2_register_widget(et2_nextmatch, ["nextmatch"]); - + this.controller._objectManager.clear(); + this.controller.keepSelection(); + } + // Update the filters in the grid controller + this.controller.setFilters(this.activeFilters); + // Update the header + this.header.setFilters(this.activeFilters); + // Update any column filters + this.iterateOver(function (column) { + // Skip favorites - it implements et2_INextmatchHeader, but we don't want it in the filter + if (typeof column.id != "undefined" && column.id.indexOf('favorite') == 0) + return; + if (typeof column.set_value != "undefined" && column.id) { + column.set_value(typeof this[column.id] == "undefined" || this[column.id] == null ? "" : this[column.id]); + } + if (column.id && typeof column.get_value == "function") { + this[column.id] = column.get_value(); + } + }, this.activeFilters.col_filter, et2_INextmatchHeader); + // Trigger an update + this.controller.update(true); + if (changed) { + // Highlight matching favorite in sidebox + if (this.getInstanceManager().app) { + var appname = this.getInstanceManager().app; + if (app[appname] && app[appname].highlight_favorite) { + app[appname].highlight_favorite(); + } + } + } + this.update_in_progress = false; + }; + /** + * Refresh given rows for specified change + * + * Change type parameters allows for quicker refresh then complete server side reload: + * - 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 + * + * @param {string[]|string} _row_ids rows to refresh + * @param {?string} _type "update", "edit", "delete" or "add" + * + * @see jsapi.egw_refresh() + * @fires refresh from the widget itself + */ + et2_nextmatch.prototype.refresh = function (_row_ids, _type) { + // Framework trying to refresh, but nextmatch not fully initialized + if (this.controller === null || !this.div) { + return; + } + if (!this.div.is(':visible')) // run refresh, once we become visible again + { + jQuery(this.getInstanceManager().DOMContainer.parentNode).one('show.et2_nextmatch', + // Important to use anonymous function instead of just 'this.refresh' because + // of the parameters passed + jQuery.proxy(function () { this.refresh(); }, this)); + return; + } + if (typeof _type == 'undefined') + _type = 'edit'; + if (typeof _row_ids == 'string' || typeof _row_ids == 'number') + _row_ids = [_row_ids]; + if (typeof _row_ids == "undefined" || _row_ids === null) { + this.applyFilters(); + // Trigger an event so app code can act on it + jQuery(this).triggerHandler("refresh", [this]); + return; + } + if (_type == "delete") { + // Record current & next index + var uid = _row_ids[0].toString().indexOf(this.controller.dataStorePrefix) == 0 ? _row_ids[0] : this.controller.dataStorePrefix + "::" + _row_ids[0]; + var entry = this.controller._selectionMgr._getRegisteredRowsEntry(uid); + var next = (entry.ao ? entry.ao.getNext(_row_ids.length) : null); + if (next == null || !next.id || next.id == uid) { + // No next, select previous + next = (entry.ao ? entry.ao.getPrevious(1) : null); + } + // Stop automatic updating + this.dataview.grid.doInvalidate = false; + for (var i = 0; i < _row_ids.length; i++) { + uid = _row_ids[i].toString().indexOf(this.controller.dataStorePrefix) == 0 ? _row_ids[i] : this.controller.dataStorePrefix + "::" + _row_ids[i]; + // Delete from internal references + this.controller.deleteRow(uid); + } + // Select & focus next row + if (next && next.id) { + this.controller._selectionMgr.setSelected(next.id, true); + this.controller._selectionMgr.setFocused(next.id, true); + } + // Update the count + var total = this.dataview.grid._total - _row_ids.length; + // This will remove the last row! + // That's OK, because grid adds one in this.controller.deleteRow() + this.dataview.grid.setTotalCount(total); + // Re-enable automatic updating + this.dataview.grid.doInvalidate = true; + this.dataview.grid.invalidate(); + } + id_loop: for (var i = 0; i < _row_ids.length; i++) { + var uid = _row_ids[i].toString().indexOf(this.controller.dataStorePrefix) == 0 ? _row_ids[i] : this.controller.dataStorePrefix + "::" + _row_ids[i]; + switch (_type) { + case "update": + if (!this.egw().dataRefreshUID(uid)) { + // Could not update just that row + this.applyFilters(); + break id_loop; + } + break; + case "delete": + // Handled above, more code to execute after loop + break; + case "edit": + case "add": + default: + // Trigger refresh + this.applyFilters(); + break id_loop; + } + } + // Trigger an event so app code can act on it + jQuery(this).triggerHandler("refresh", [this, _row_ids, _type]); + }; + /** + * Gets the selection + * + * @return Object { ids: [UIDs], inverted: boolean} + */ + et2_nextmatch.prototype.getSelection = function () { + var selected = this.controller && this.controller._selectionMgr ? this.controller._selectionMgr.getSelected() : null; + if (typeof selected == "object" && selected != null) { + return selected; + } + return { ids: [], all: false }; + }; + /** + * Event handler for when the selection changes + * + * If the onselect attribute was set to a string with javascript code, it will + * be executed "legacy style". You can get the selected values with getSelection(). + * If the onselect attribute is in app.appname.function style, it will be called + * with the nextmatch and an array of selected row IDs. + * + * The array can be empty, if user cleared the selection. + * + * @param action ActionObject From action system. Ignored. + * @param senders ActionObjectImplemetation From action system. Ignored. + */ + et2_nextmatch.prototype.onselect = function (action, senders) { + // Execute the JS code connected to the event handler + if (typeof this.options.onselect == 'function') { + return this.options.onselect.call(this, this.getSelection().ids, this); + } + }; + /** + * Nextmatch needs a namespace + */ + et2_nextmatch.prototype._createNamespace = function () { + return true; + }; + /** + * Create the dynamic height so nm fills all available space + * + * @returns {undefined} + */ + et2_nextmatch.prototype._getDynheight = function () { + // Find the parent container, either a tab or the main container + var tab = this.get_tab_info(); + if (!tab) { + return new et2_dynheight(this.getInstanceManager().DOMContainer, this.innerDiv, 100); + } + else if (tab && tab.contentDiv) { + return new et2_dynheight(tab.contentDiv, this.innerDiv, 100); + } + return false; + }; + /** + * Generates the column caption for the given column widget + * + * @param {et2_widget} _widget + */ + et2_nextmatch.prototype._genColumnCaption = function (_widget) { + var result = null; + if (typeof _widget._genColumnCaption == "function") + return _widget._genColumnCaption(); + var self = this; + _widget.iterateOver(function (_widget) { + var label = self.egw().lang(_widget.options.label || _widget.options.empty_label || ''); + if (!label) + return; // skip empty, undefined or null labels + if (!result) { + result = label; + } + else { + result += ", " + label; + } + }, this, et2_INextmatchHeader); + return result; + }; + /** + * Generates the column name (internal) for the given column widget + * Used in preferences to refer to the columns by name instead of position + * + * See _getColumnCaption() for human fiendly captions + * + * @param {et2_widget} _widget + */ + et2_nextmatch.prototype._getColumnName = function (_widget) { + if (typeof _widget._getColumnName == 'function') + return _widget._getColumnName(); + var name = _widget.id; + var child_names = []; + var children = _widget.getChildren(); + for (var i = 0; i < children.length; i++) { + if (children[i].id) + child_names.push(children[i].id); + } + var colName = name + (name != "" && child_names.length > 0 ? "_" : "") + child_names.join("_"); + if (colName == "") { + this.egw().debug("info", "Unable to generate nm column name for ", _widget); + } + return colName; + }; + /** + * Retrieve the user's preferences for this nextmatch merged with defaults + * Column display, column size, etc. + */ + et2_nextmatch.prototype._getPreferences = function () { + // Read preference or default for column visibility + var negated = false; + var columnPreference = ""; + if (this.options.settings.default_cols) { + negated = this.options.settings.default_cols[0] == "!"; + columnPreference = negated ? this.options.settings.default_cols.substring(1) : this.options.settings.default_cols; + } + if (this.options.settings.selectcols && this.options.settings.selectcols.length) { + columnPreference = this.options.settings.selectcols; + negated = false; + } + if (!this.options.settings.columnselection_pref) { + // Set preference name so changes are saved + this.options.settings.columnselection_pref = this.options.template; + } + var app = ''; + var list = []; + if (this.options.settings.columnselection_pref) { + var pref = {}; + list = et2_csvSplit(this.options.settings.columnselection_pref, 2, "."); + if (this.options.settings.columnselection_pref.indexOf('nextmatch') == 0) { + app = list[0].substring('nextmatch'.length + 1); + pref = egw.preference(this.options.settings.columnselection_pref, app); + } + else { + app = list[0]; + // 'nextmatch-' prefix is there in preference name, but not in setting, so add it in + pref = egw.preference("nextmatch-" + this.options.settings.columnselection_pref, app); + } + if (pref) { + negated = (pref[0] == "!"); + columnPreference = negated ? pref.substring(1) : pref; + } + } + var columnDisplay = []; + // If no column preference or default set, use all columns + if (typeof columnPreference == "string" && columnPreference.length == 0) { + columnDisplay = []; + negated = true; + } + columnDisplay = typeof columnPreference === "string" + ? et2_csvSplit(columnPreference, null, ",") : columnPreference; + // Adjusted column sizes + var size = {}; + if (this.options.settings.columnselection_pref && app) { + var size_pref = this.options.settings.columnselection_pref + "-size"; + // If columnselection pref is missing prefix, add it in + if (size_pref.indexOf('nextmatch') == -1) { + size_pref = 'nextmatch-' + size_pref; + } + size = this.egw().preference(size_pref, app); + } + if (!size) + size = {}; + // Column order + var order = {}; + for (var i = 0; i < columnDisplay.length; i++) { + order[columnDisplay[i]] = i; + } + return { + visible: columnDisplay, + visible_negated: negated, + negated: negated, + size: size, + order: order + }; + }; + /** + * Apply stored user preferences to discovered columns + * + * @param {array} _row + * @param {array} _colData + */ + et2_nextmatch.prototype._applyUserPreferences = function (_row, _colData) { + var prefs = this._getPreferences(); + var columnDisplay = prefs.visible; + var size = prefs.size; + var negated = prefs.visible_negated; + var order = prefs.order; + var colName = ''; + // Add in display preferences + if (columnDisplay && columnDisplay.length > 0) { + RowLoop: for (var i = 0; i < _row.length; i++) { + colName = ''; + if (_row[i].disabled === true) { + _colData[i].visible = false; + continue; + } + // Customfields needs special processing + if (_row[i].widget.instanceOf(et2_nextmatch_customfields)) { + // Find cf field + for (var j = 0; j < columnDisplay.length; j++) { + if (columnDisplay[j].indexOf(_row[i].widget.id) == 0) { + _row[i].widget.options.fields = {}; + for (var k = j; k < columnDisplay.length; k++) { + if (columnDisplay[k].indexOf(_row[i].widget.prefix) == 0) { + _row[i].widget.options.fields[columnDisplay[k].substr(1)] = true; + } + } + // Resets field visibility too + _row[i].widget._getColumnName(); + _colData[i].visible = !(negated || jQuery.isEmptyObject(_row[i].widget.options.fields)); + break; + } + } + // Disable if there are no custom fields + if (jQuery.isEmptyObject(_row[i].widget.customfields)) { + _colData[i].visible = false; + continue; + } + colName = _row[i].widget.id; + } + else { + colName = this._getColumnName(_row[i].widget); + } + if (!colName) + continue; + if (size[colName]) { + // Make sure percentages stay percentages, and forget any preference otherwise + if (_colData[i].width.charAt(_colData[i].width.length - 1) == "%") { + _colData[i].width = typeof size[colName] == 'string' && size[colName].charAt(size[colName].length - 1) == "%" ? size[colName] : _colData[i].width; + } + else { + _colData[i].width = parseInt(size[colName]) + 'px'; + } + } + if (!negated) { + _colData[i].order = typeof order[colName] === 'undefined' ? i : order[colName]; + } + for (var j = 0; j < columnDisplay.length; j++) { + if (columnDisplay[j] == colName) { + _colData[i].visible = !negated; + continue RowLoop; + } + } + _colData[i].visible = negated; + } + } + _colData.sort(function (a, b) { + return a.order - b.order; + }); + _row.sort(function (a, b) { + if (typeof a.colData !== 'undefined' && typeof b.colData !== 'undefined') { + return a.colData.order - b.colData.order; + } + else if (typeof a.order !== 'undefined' && typeof b.order !== 'undefined') { + return a.order - b.order; + } + }); + }; + /** + * Take current column display settings and store them in this.egw().preferences + * for next time + */ + et2_nextmatch.prototype._updateUserPreferences = function () { + var colMgr = this.dataview.getColumnMgr(); + var app = ""; + if (!this.options.settings.columnselection_pref) { + this.options.settings.columnselection_pref = this.options.template; + } + var visibility = colMgr.getColumnVisibilitySet(); + var colDisplay = []; + var colSize = {}; + var custom_fields = []; + // visibility is indexed by internal ID, widget is referenced by position, preference needs name + for (var i = 0; i < colMgr.columns.length; i++) { + // @ts-ignore + var widget = this.columns[i].widget; + var colName = this._getColumnName(widget); + if (colName) { + // Server side wants each cf listed as a seperate column + if (widget.instanceOf(et2_nextmatch_customfields)) { + // Just the ID for server side, not the whole nm name - some apps use it to skip custom fields + colName = widget.id; + for (var name_1 in widget.options.fields) { + if (widget.options.fields[name_1]) + custom_fields.push(et2_nextmatch_customfields.PREFIX + name_1); + } + } + if (visibility[colMgr.columns[i].id].visible) + colDisplay.push(colName); + // When saving sizes, only save columns with explicit values, preserving relative vs fixed + // Others will be left to flex if width changes or more columns are added + if (colMgr.columns[i].relativeWidth) { + colSize[colName] = (colMgr.columns[i].relativeWidth * 100) + "%"; + } + else if (colMgr.columns[i].fixedWidth) { + colSize[colName] = colMgr.columns[i].fixedWidth; + } + } + else if (colMgr.columns[i].fixedWidth || colMgr.columns[i].relativeWidth) { + this.egw().debug("info", "Could not save column width - no name", colMgr.columns[i].id); + } + } + var list = et2_csvSplit(this.options.settings.columnselection_pref, 2, "."); + var pref = this.options.settings.columnselection_pref; + if (pref.indexOf('nextmatch') == 0) { + app = list[0].substring('nextmatch'.length + 1); + } + else { + app = list[0]; + // 'nextmatch-' prefix is there in preference name, but not in setting, so add it in + pref = "nextmatch-" + this.options.settings.columnselection_pref; + } + // Server side wants each cf listed as a seperate column + jQuery.merge(colDisplay, custom_fields); + // Update query value, so data source can use visible columns to exclude expensive sub-queries + var oldCols = this.activeFilters.selectcols ? this.activeFilters.selectcols : []; + this.activeFilters.selectcols = this.sortedColumnsList ? this.sortedColumnsList : colDisplay; + // We don't need to re-query if they've removed a column + var changed = []; + ColLoop: for (var i = 0; i < colDisplay.length; i++) { + for (var j = 0; j < oldCols.length; j++) { + if (colDisplay[i] == oldCols[j]) + continue ColLoop; + } + changed.push(colDisplay[i]); + } + // If a custom field column was added, throw away cache to deal with + // efficient apps that didn't send all custom fields in the first request + var cf_added = jQuery(changed).filter(jQuery(custom_fields)).length > 0; + // Save visible columns + // 'nextmatch-' prefix is there in preference name, but not in setting, so add it in + this.egw().set_preference(app, pref, this.activeFilters.selectcols.join(","), + // Use callback after the preference gets set to trigger refresh, in case app + // isn't looking at selectcols and just uses preference + cf_added ? jQuery.proxy(function () { if (this.controller) + this.controller.update(true); }, this) : null); + // Save adjusted column sizes + this.egw().set_preference(app, pref + "-size", colSize); + // No significant change (just normal columns shown) and no need to wait, + // but the grid still needs to be redrawn if a custom field was removed because + // the cell content changed. This is a cheaper refresh than the callback, + // this.controller.update(true) + if ((changed.length || custom_fields.length) && !cf_added) + this.applyFilters(); + }; + et2_nextmatch.prototype._parseHeaderRow = function (_row, _colData) { + // Make sure there's a widget - cols disabled in template can be missing them, and the header really likes to have a widget + for (var x = 0; x < _row.length; x++) { + if (!_row[x].widget) { + _row[x].widget = et2_createWidget("label", {}); + } + } + // Get column display preference + this._applyUserPreferences(_row, _colData); + // Go over the header row and create the column entries + this.columns = new Array(_row.length); + var columnData = new Array(_row.length); + // No action columns in et2 + var remove_action_index = null; + for (var x = 0; x < _row.length; x++) { + this.columns[x] = jQuery.extend({ + "order": _colData[x] && typeof _colData[x].order !== 'undefined' ? _colData[x].order : x, + "widget": _row[x].widget + }, _colData[x]); + var visibility = (!_colData[x] || _colData[x].visible) ? + et2_dataview_model_columns_1.et2_dataview_column.ET2_COL_VISIBILITY_VISIBLE : + et2_dataview_model_columns_1.et2_dataview_column.ET2_COL_VISIBILITY_INVISIBLE; + if (_colData[x].disabled && _colData[x].disabled !== '' && + this.getArrayMgr("content").parseBoolExpression(_colData[x].disabled)) { + visibility = et2_dataview_model_columns_1.et2_dataview_column.ET2_COL_VISIBILITY_DISABLED; + } + columnData[x] = { + "id": "col_" + x, + // @ts-ignore + "order": this.columns[x].order, + "caption": this._genColumnCaption(_row[x].widget), + "visibility": visibility, + "width": _colData[x] ? _colData[x].width : 0 + }; + if (_colData[x].width === 'auto') { + // Column manager does not understand 'auto', which grid widget + // uses if width is not set + columnData[x].width = '100%'; + } + if (_colData[x].minWidth) { + columnData[x].minWidth = _colData[x].minWidth; + } + if (_colData[x].maxWidth) { + columnData[x].maxWidth = _colData[x].maxWidth; + } + // No action columns in et2 + var colName = this._getColumnName(_row[x].widget); + if (colName == 'actions' || colName == 'legacy_actions' || colName == 'legacy_actions_check_all') { + remove_action_index = x; + } + else if (!colName) { + // Unnamed column cannot be toggled or saved + columnData[x].visibility = et2_dataview_grid.ET2_COL_VISIBILITY_ALWAYS_NOSELECT; + } + } + // Remove action column + if (remove_action_index != null) { + this.columns.splice(remove_action_index, remove_action_index); + columnData.splice(remove_action_index, remove_action_index); + _colData.splice(remove_action_index, remove_action_index); + } + // Create the column manager and update the grid container + this.dataview.setColumns(columnData); + for (var x = 0; x < _row.length; x++) { + // Append the widget to this container + this.addChild(_row[x].widget); + } + // Create the nextmatch row provider + this.rowProvider = new et2_extension_nextmatch_rowProvider_1.et2_nextmatch_rowProvider(this.dataview.rowProvider, this._getSubgrid, this); + // Register handler to update preferences when column properties are changed + var self = this; + this.dataview.onUpdateColumns = function () { + // Use apply to make sure context is there + self._updateUserPreferences.apply(self); + // Allow column widgets a chance to resize + self.iterateOver(function (widget) { widget.resize(); }, self, et2_IResizeable); + }; + // Register handler for column selection popup, or disable + if (this.selectPopup) { + this.selectPopup.remove(); + this.selectPopup = null; + } + if (this.options.settings.no_columnselection) { + this.dataview.selectColumnsClick = function () { return false; }; + jQuery('span.selectcols', this.dataview.headTr).hide(); + } + else { + jQuery('span.selectcols', this.dataview.headTr).show(); + this.dataview.selectColumnsClick = function (event) { + self._selectColumnsClick(event); + }; + } + }; + et2_nextmatch.prototype._parseDataRow = function (_row, _rowData, _colData) { + var columnWidgets = new Array(this.columns.length); + _row.sort(function (a, b) { + return a.colData.order - b.colData.order; + }); + for (var x = 0; x < columnWidgets.length; x++) { + if (typeof _row[x] != "undefined" && _row[x].widget) { + columnWidgets[x] = _row[x].widget; + // Append the widget to this container + this.addChild(_row[x].widget); + } + else { + columnWidgets[x] = _row[x].widget; + } + // Pass along column alignment + if (_row[x].align && columnWidgets[x]) { + columnWidgets[x].align = _row[x].align; + } + } + this.rowProvider.setDataRowTemplate(columnWidgets, _rowData, this); + // Create the grid controller + this.controller = new et2_extension_nextmatch_controller_1.et2_nextmatch_controller(null, this.egw(), this.getInstanceManager().etemplate_exec_id, this, null, this.dataview.grid, this.rowProvider, this.options.settings.action_links, null, this.options.actions); + // Need to trigger empty row the first time + if (total == 0) + this.controller._emptyRow(); + // Set data cache prefix to either provided custom or auto + if (!this.options.settings.dataStorePrefix && this.options.settings.get_rows) { + // Use jsapi data module to update + var list = this.options.settings.get_rows.split('.', 2); + if (list.length < 2) + list = this.options.settings.get_rows.split('_'); // support "app_something::method" + this.options.settings.dataStorePrefix = list[0]; + } + this.controller.setPrefix(this.options.settings.dataStorePrefix); + // Set the view + this.controller._view = this.view; + // Load the initial order + /*this.controller.loadInitialOrder(this._getInitialOrder( + this.options.settings.rows, this.options.settings.row_id + ));*/ + // Set the initial row count + var total = typeof this.options.settings.total != "undefined" ? + this.options.settings.total : 0; + // This triggers an invalidate, which updates the grid + this.dataview.grid.setTotalCount(total); + // Insert any data sent from server, so invalidate finds data already + if (this.options.settings.rows && this.options.settings.num_rows) { + this.controller.loadInitialData(this.options.settings.dataStorePrefix, this.options.settings.row_id, this.options.settings.rows); + // Remove, to prevent duplication + delete this.options.settings.rows; + } + }; + et2_nextmatch.prototype._parseGrid = function (_grid) { + // Search the rows for a header-row - if one is found, parse it + for (var y = 0; y < _grid.rowData.length; y++) { + // Parse the first row as a header, need header to parse the data rows + if (_grid.rowData[y]["class"] == "th" || y == 0) { + this._parseHeaderRow(_grid.cells[y], _grid.colData); + } + else { + this._parseDataRow(_grid.cells[y], _grid.rowData[y], _grid.colData); + } + } + this.dataview.table.resize(); + }; + et2_nextmatch.prototype._getSubgrid = function (_row, _data, _controller) { + // Fetch the id of the element described by _data, this will be the + // parent_id of the elements in the subgrid + var rowId = _data.content[this.options.settings.row_id]; + // Create a new grid with the row as parent and the dataview grid as + // parent grid + var grid = new et2_dataview_grid(_row, this.dataview.grid); + // Create a new controller for the grid + var controller = new et2_extension_nextmatch_controller_1.et2_nextmatch_controller(_controller, this.egw(), this.getInstanceManager().etemplate_exec_id, this, rowId, grid, this.rowProvider, this.options.settings.action_links, _controller.getObjectManager()); + controller.update(); + // Register inside the destruction callback of the grid + grid.setDestroyCallback(function () { + controller.destroy(); + }); + return grid; + }; + et2_nextmatch.prototype._getInitialOrder = function (_rows, _rowId) { + var _order = []; + // Get the length of the non-numerical rows arra + var len = 0; + for (var key in _rows) { + if (!isNaN(parseInt(key)) && parseInt(key) > len) + len = parseInt(key); + } + // Iterate over the rows + for (var i = 0; i < len; i++) { + // Get the uid from the data + var uid = this.egw().app_name() + '::' + _rows[i][_rowId]; + // Store the data for that uid + this.egw().dataStoreUID(uid, _rows[i]); + // Push the uid onto the order array + _order.push(uid); + } + return _order; + }; + et2_nextmatch.prototype._selectColumnsClick = function (e) { + var self = this; + var columnMgr = this.dataview.getColumnMgr(); + // ID for faking letter selection in column selection + var LETTERS = '~search_letter~'; + var columns = {}; + var columns_selected = []; + for (var i = 0; i < columnMgr.columns.length; i++) { + var col = columnMgr.columns[i]; + var widget = this.columns[i].widget; + if (col.visibility == et2_dataview_model_columns_1.et2_dataview_column.ET2_COL_VISIBILITY_DISABLED || + col.visibility == et2_dataview_model_columns_1.et2_dataview_column.ET2_COL_VISIBILITY_ALWAYS_NOSELECT) { + continue; + } + if (col.caption) { + columns[col.id] = col.caption; + if (col.visibility == et2_dataview_model_columns_1.et2_dataview_column.ET2_COL_VISIBILITY_VISIBLE) + columns_selected.push(col.id); + } + // Custom fields get listed separately + if (widget.instanceOf(et2_nextmatch_customfields)) { + if (jQuery.isEmptyObject(widget.customfields)) { + // No customfields defined, don't show column + delete (columns[col.id]); + continue; + } + for (var field_name in widget.customfields) { + columns[et2_nextmatch_customfields.PREFIX + field_name] = " - " + + widget.customfields[field_name].label; + if (widget.options.fields[field_name]) + columns_selected.push(et2_extension_customfields_1.et2_customfields_list.PREFIX + field_name); + } + } + } + // Letter search + if (this.options.settings.lettersearch) { + columns[LETTERS] = egw.lang('Search letter'); + if (this.header.lettersearch.is(':visible')) + columns_selected.push(LETTERS); + } + // Build the popup + if (!this.selectPopup) { + var select_1 = et2_createWidget("select", { + multiple: true, + rows: 8, + empty_label: this.egw().lang("select columns"), + selected_first: false, + value_class: "selcolumn_sortable_" + }, this); + select_1.set_select_options(columns); + select_1.set_value(columns_selected); + var autoRefresh_1 = et2_createWidget("select", { + "empty_label": "Refresh" + }, this); + autoRefresh_1.set_id("nm_autorefresh"); + autoRefresh_1.set_select_options({ + // Cause [unknown] problems with mail + //30: "30 seconds", + //60: "1 Minute", + 300: "5 Minutes", + 900: "15 Minutes", + 1800: "30 Minutes" + }); + autoRefresh_1.set_value(this._get_autorefresh()); + autoRefresh_1.set_statustext(egw.lang("Automatically refresh list")); + var defaultCheck = et2_createWidget("select", { "empty_label": "Preference" }, this); + defaultCheck.set_id('nm_col_preference'); + defaultCheck.set_select_options({ + 'default': { label: 'Default', title: 'Set these columns as the default' }, + 'reset': { label: 'Reset', title: "Reset all user's column preferences" }, + 'force': { label: 'Force', title: 'Force column preference so users cannot change it' } + }); + defaultCheck.set_value(this.options.settings.columns_forced ? 'force' : ''); + var okButton = et2_createWidget("buttononly", { "background_image": true, image: "check" }, this); + okButton.set_label(this.egw().lang("ok")); + okButton.onclick = function () { + // Update visibility + var visibility = {}; + for (var i = 0; i < columnMgr.columns.length; i++) { + var col_1 = columnMgr.columns[i]; + if (col_1.caption && col_1.visibility !== et2_dataview_model_columns_1.et2_dataview_column.ET2_COL_VISIBILITY_ALWAYS_NOSELECT && + col_1.visibility !== et2_dataview_model_columns_1.et2_dataview_column.ET2_COL_VISIBILITY_DISABLED) { + visibility[col_1.id] = { visible: false }; + } + } + var value = select_1.getValue(); + // Update & remove letter filter + if (self.header.lettersearch) { + var show_letters = true; + if (value.indexOf(LETTERS) >= 0) { + value.splice(value.indexOf(LETTERS), 1); + } + else { + show_letters = false; + } + self._set_lettersearch(show_letters); + } + var column = 0; + for (var i = 0; i < value.length; i++) { + // Handle skipped columns + while (value[i] != "col_" + column && column < columnMgr.columns.length) { + column++; + } + if (visibility[value[i]]) { + visibility[value[i]].visible = true; + } + // Custom fields are listed seperately in column list, but are only 1 column + if (self.columns[column] && self.columns[column].widget.instanceOf(et2_nextmatch_customfields)) { + var cf = self.columns[column].widget.options.customfields; + var visible = self.columns[column].widget.options.fields; + // Turn off all custom fields + for (var field_name in cf) { + visible[field_name] = false; + } + // Turn on selected custom fields - start from 0 in case they're not in order + for (var j = 0; j < value.length; j++) { + if (value[j].indexOf(et2_extension_customfields_1.et2_customfields_list.PREFIX) != 0) + continue; + visible[value[j].substring(1)] = true; + i++; + } + self.columns[column].widget.set_visible(visible); + } + } + columnMgr.setColumnVisibilitySet(visibility); + this.sortedColumnsList = []; + jQuery(select_1.getDOMNode()).find('li[class^="selcolumn_sortable_"]').each(function (i, v) { + var data_id = v.getAttribute('data-value'); + var value = select_1.getValue(); + if (data_id.match(/^col_/) && value.indexOf(data_id) != -1) { + var col_id = data_id.replace('col_', ''); + var col_widget = self.columns[col_id].widget; + if (col_widget.customfields) { + self.sortedColumnsList.push(col_widget.id); + for (var field_name_1 in col_widget.customfields) { + if (jQuery.isEmptyObject(col_widget.options.fields) || col_widget.options.fields[field_name_1] == true) { + self.sortedColumnsList.push(et2_extension_customfields_1.et2_customfields_list.PREFIX + field_name_1); + } + } + } + else { + self.sortedColumnsList.push(self._getColumnName(col_widget)); + } + } + }); + // Hide popup + self.selectPopup.toggle(); + self.dataview.updateColumns(); + // Auto refresh + self._set_autorefresh(autoRefresh_1.get_value()); + // Set default or clear forced + if (show_letters) { + self.activeFilters.selectcols.push('lettersearch'); + } + self.getInstanceManager().submit(); + self.selectPopup = null; + }; + var cancelButton = et2_createWidget("buttononly", { "background_image": true, image: "cancel" }, this); + cancelButton.set_label(this.egw().lang("cancel")); + cancelButton.onclick = function () { + self.selectPopup.toggle(); + self.selectPopup = null; + }; + var $select = jQuery(select_1.getDOMNode()); + $select.find('.ui-multiselect-checkboxes').sortable({ + placeholder: 'ui-fav-sortable-placeholder', + items: 'li[class^="selcolumn_sortable_col"]', + cancel: 'li[class^="selcolumn_sortable_#"]', + cursor: "move", + tolerance: "pointer", + axis: 'y', + containment: "parent", + delay: 250, + beforeStop: function (event, ui) { + jQuery('li[class^="selcolumn_sortable_#"]', this).css({ + opacity: 1 + }); + }, + start: function (event, ui) { + jQuery('li[class^="selcolumn_sortable_#"]', this).css({ + opacity: 0.5 + }); + }, + sort: function (event, ui) { + jQuery(this).sortable("refreshPositions"); + } + }); + $select.disableSelection(); + $select.find('li[class^="selcolumn_sortable_"]').each(function (i, v) { + // @ts-ignore + jQuery(v).attr('data-value', (jQuery(v).find('input')[0].value)); + }); + var $footerWrap = jQuery(document.createElement("div")) + .addClass('dialogFooterToolbar') + .append(okButton.getDOMNode()) + .append(cancelButton.getDOMNode()); + this.selectPopup = jQuery(document.createElement("div")) + .addClass("colselection ui-dialog ui-widget-content") + .append(select_1.getDOMNode()) + .append($footerWrap) + .appendTo(this.innerDiv); + // Add autorefresh + $footerWrap.append(autoRefresh_1.getSurroundings().getDOMNode(autoRefresh_1.getDOMNode())); + // Add default checkbox for admins + var apps = this.egw().user('apps'); + if (apps['admin']) { + $footerWrap.append(defaultCheck.getSurroundings().getDOMNode(defaultCheck.getDOMNode())); + } + } + else { + this.selectPopup.toggle(); + } + var t_position = jQuery(e.target).position(); + var s_position = this.div.position(); + var max_height = this.getDOMNode().getElementsByClassName('egwGridView_outer')[0]['tBodies'][0].clientHeight - + (2 * this.selectPopup.find('.dialogFooterToolbar').height()); + this.selectPopup.find('.ui-multiselect-checkboxes').css('max-height', max_height); + this.selectPopup.css("top", t_position.top) + .css("left", s_position.left + this.div.width() - this.selectPopup.width()); + }; + /** + * Set the currently displayed columns, without updating user's preference + * + * @param {string[]} column_list List of column names + * @param {boolean} trigger_update =false - explicitly trigger an update + */ + et2_nextmatch.prototype.set_columns = function (column_list, trigger_update) { + if (trigger_update === void 0) { trigger_update = false; } + var columnMgr = this.dataview.getColumnMgr(); + var visibility = {}; + // Initialize to false + for (var i = 0; i < columnMgr.columns.length; i++) { + var col = columnMgr.columns[i]; + if (col.caption && col.visibility != et2_dataview_grid.ET2_COL_VISIBILITY_ALWAYS_NOSELECT) { + visibility[col.id] = { visible: false }; + } + } + for (var i = 0; i < this.columns.length; i++) { + var widget = this.columns[i].widget; + var colName = this._getColumnName(widget); + if (column_list.indexOf(colName) !== -1 && + typeof visibility[columnMgr.columns[i].id] !== 'undefined') { + visibility[columnMgr.columns[i].id].visible = true; + } + // Custom fields are listed seperately in column list, but are only 1 column + if (widget && widget.instanceOf(et2_nextmatch_customfields)) { + // Just the ID for server side, not the whole nm name - some apps use it to skip custom fields + colName = widget.id; + if (column_list.indexOf(colName) !== -1) { + visibility[columnMgr.columns[i].id].visible = true; + } + var cf = this.columns[i].widget.options.customfields; + var visible = this.columns[i].widget.options.fields; + // Turn off all custom fields + for (var field_name in cf) { + visible[field_name] = false; + } + // Turn on selected custom fields - start from 0 in case they're not in order + for (var j = 0; j < column_list.length; j++) { + if (column_list[j].indexOf(et2_extension_customfields_1.et2_customfields_list.PREFIX) != 0) + continue; + visible[column_list[j].substring(1)] = true; + } + widget.set_visible(visible); + } + } + columnMgr.setColumnVisibilitySet(visibility); + // We don't want to update user's preference, so directly update + this.dataview._updateColumns(); + // Allow column widgets a chance to resize + this.iterateOver(function (widget) { widget.resize(); }, this, et2_IResizeable); + }; + /** + * Set the letter search preference, and update the UI + * + * @param {boolean} letters_on + */ + et2_nextmatch.prototype._set_lettersearch = function (letters_on) { + if (letters_on) { + this.header.lettersearch.show(); + } + else { + this.header.lettersearch.hide(); + } + var lettersearch_preference = "nextmatch-" + this.options.settings.columnselection_pref + "-lettersearch"; + this.egw().set_preference(this.egw().app_name(), lettersearch_preference, letters_on); + }; + /** + * Set the auto-refresh time period, and starts the timer if not started + * + * @param time int Refresh period, in seconds + */ + et2_nextmatch.prototype._set_autorefresh = function (time) { + // Store preference + var refresh_preference = "nextmatch-" + this.options.settings.columnselection_pref + "-autorefresh"; + var app = this.options.template.split("."); + if (this._get_autorefresh() != time) { + this.egw().set_preference(app[0], refresh_preference, time); + } + // Start / update timer + if (this._autorefresh_timer) { + window.clearInterval(this._autorefresh_timer); + delete this._autorefresh_timer; + } + if (time > 0) { + this._autorefresh_timer = setInterval(jQuery.proxy(this.controller.update, this.controller), time * 1000); + // Bind to tab show/hide events, so that we don't bother refreshing in the background + jQuery(this.getInstanceManager().DOMContainer.parentNode).on('hide.et2_nextmatch', jQuery.proxy(function (e) { + // Stop + window.clearInterval(this._autorefresh_timer); + jQuery(e.target).off(e); + // If the autorefresh time is up, bind once to trigger a refresh + // (if needed) when tab is activated again + this._autorefresh_timer = setTimeout(jQuery.proxy(function () { + // Check in case it was stopped / destroyed since + if (!this._autorefresh_timer || !this.getInstanceManager()) + return; + jQuery(this.getInstanceManager().DOMContainer.parentNode).one('show.et2_nextmatch', + // Important to use anonymous function instead of just 'this.refresh' because + // of the parameters passed + jQuery.proxy(function () { this.refresh(); }, this)); + }, this), time * 1000); + }, this)); + jQuery(this.getInstanceManager().DOMContainer.parentNode).on('show.et2_nextmatch', jQuery.proxy(function (e) { + // Start normal autorefresh timer again + this._set_autorefresh(this._get_autorefresh()); + jQuery(e.target).off(e); + }, this)); + } + }; + /** + * Get the auto-refresh timer + * + * @return int Refresh period, in secods + */ + et2_nextmatch.prototype._get_autorefresh = function () { + var refresh_preference = "nextmatch-" + this.options.settings.columnselection_pref + "-autorefresh"; + var app = this.options.template.split("."); + return this.egw().preference(refresh_preference, app[0]); + }; + /** + * When the template attribute is set, the nextmatch widget tries to load + * that template and to fetch the grid which is inside of it. It then calls + * + * @param {string} template_name Full template name in the form app.template[.template] + */ + et2_nextmatch.prototype.set_template = function (template_name) { + var template = et2_createWidget("template", { "id": template_name }, this); + if (this.template) { + // Stop early to prevent unneeded processing, and prevent infinite + // loops if the server changes the template in get_rows + if (this.template == template_name) { + return; + } + // Free the grid components - they'll be re-created as the template is processed + this.dataview.destroy(); + this.rowProvider.destroy(); + this.controller.destroy(); + // Free any children from previous template + // They may get left behind because of how detached nodes are processed + // We don't use iterateOver because it checks sub-children + for (var i = this._children.length - 1; i >= 0; i--) { + var _node = this._children[i]; + if (_node != this.header) { + this.removeChild(_node); + _node.destroy(); + } + } + // Clear this setting if it's the same as the template, or + // the columns will not be loaded + if (this.template == this.options.settings.columnselection_pref) { + this.options.settings.columnselection_pref = template_name; + } + this.dataview = new et2_dataview_1.et2_dataview(this.innerDiv, this.egw()); + } + // Create the template + if (template_name) { + } + if (!template) { + this.egw().debug("error", "Error while loading definition template for " + + "nextmatch widget.", template_name); + return; + } + if (this.options.disabled) { + return; + } + // Deferred parse function - template might not be fully loaded + var parse = function (template) { + // Keep the name of the template, as we'll free up the widget after parsing + this.template = template_name; + // Fetch the grid element and parse it + var definitionGrid = template.getChildren()[0]; + if (definitionGrid && definitionGrid instanceof et2_grid) { + this._parseGrid(definitionGrid); + } + else { + this.egw().debug("error", "Nextmatch widget expects a grid to be the " + + "first child of the defined template."); + return; + } + // Free the template again, but don't remove it + setTimeout(function () { + template.destroy(); + }, 1); + // Call the "setNextmatch" function of all registered + // INextmatchHeader widgets. This updates this.activeFilters.col_filters according + // to what's in the template. + this.iterateOver(function (_node) { + _node.setNextmatch(this); + }, this, et2_INextmatchHeader); + // Set filters to current values + // TODO this.controller.setFilters(this.activeFilters); + // If no data was sent from the server, and num_rows is 0, the nm will be empty. + // This triggers a cache check. + if (!this.options.settings.num_rows && this.controller) { + this.controller.update(); + } + // Load the default sort order + if (this.options.settings.order && this.options.settings.sort) { + this.sortBy(this.options.settings.order, this.options.settings.sort == "ASC", false); + } + // Start auto-refresh + this._set_autorefresh(this._get_autorefresh()); + }; + // Template might not be loaded yet, defer parsing + var promise = []; + template.loadingFinished(promise); + // Wait until template (& children) are done + jQuery.when.apply(null, promise).done(jQuery.proxy(function () { + parse.call(this, template); + if (!this.dynheight) { + this.dynheight = this._getDynheight(); + } + this.dynheight.initialized = false; + this.resize(); + }, this)); + }; + // Some accessors to match conventions + et2_nextmatch.prototype.set_hide_header = function (hide) { + (hide ? this.header.div.hide() : this.header.div.show()); + }; + et2_nextmatch.prototype.set_header_left = function (template) { + this.header._build_header("left", template); + }; + et2_nextmatch.prototype.set_header_right = function (template) { + this.header._build_header("right", template); + }; + et2_nextmatch.prototype.set_header_row = function (template) { + this.header._build_header("row", template); + }; + et2_nextmatch.prototype.set_no_filter = function (bool, filter_name) { + if (typeof filter_name == 'undefined') { + filter_name = 'filter'; + } + this.options['no_' + filter_name] = bool; + var filter = this.header[filter_name]; + if (filter) { + filter.set_disabled(bool); + } + else if (bool) { + filter = this.header._build_select(filter_name, 'select', this.settings[filter_name], this.settings[filter_name + '_no_lang']); + } + }; + et2_nextmatch.prototype.set_no_filter2 = function (bool) { + this.set_no_filter(bool, 'filter2'); + }; + /** + * Directly change filter value, with no server query. + * + * This allows the server app code to change filter value, and have it + * updated in the client UI. + * + * @param {String|number} value + */ + et2_nextmatch.prototype.set_filter = function (value) { + var update = this.update_in_progress; + this.update_in_progress = true; + this.activeFilters.filter = value; + // Update the header + this.header.setFilters(this.activeFilters); + this.update_in_progress = update; + }; + /** + * Directly change filter2 value, with no server query. + * + * This allows the server app code to change filter2 value, and have it + * updated in the client UI. + * + * @param {String|number} value + */ + et2_nextmatch.prototype.set_filter2 = function (value) { + var update = this.update_in_progress; + this.update_in_progress = true; + this.activeFilters.filter2 = value; + // Update the header + this.header.setFilters(this.activeFilters); + this.update_in_progress = update; + }; + /** + * If nextmatch starts disabled, it will need a resize after being shown + * to get all the sizing correct. Override the parent to add the resize + * when enabling. + * + * @param {boolean} _value + */ + et2_nextmatch.prototype.set_disabled = function (_value) { + var previous = this.disabled; + _super.prototype.set_disabled.call(this, _value); + if (previous && !_value) { + this.resize(); + } + }; + /** + * Actions are handled by the controller, so ignore these during init. + * + * @param {object} actions + */ + et2_nextmatch.prototype.set_actions = function (actions) { + if (actions != this.options.actions && this.controller != null && this.controller._actionManager) { + for (var i = this.controller._actionManager.children.length - 1; i >= 0; i--) { + this.controller._actionManager.children[i].remove(); + } + this.options.actions = actions; + this.options.settings.action_links = this.controller._actionLinks = this._get_action_links(actions); + this.controller._initActions(actions); + } + }; + /** + * Switch view between row and tile. + * This should be followed by a call to change the template to match, which + * will cause a reload of the grid using the new settings. + * + * @param {string} view Either 'tile' or 'row' + */ + et2_nextmatch.prototype.set_view = function (view) { + // Restrict to the only 2 accepted values + if (view == 'tile') { + this.view = 'tile'; + } + else { + this.view = 'row'; + } + }; + /** + * Set a different / additional handler for dropped files. + * + * File dropping doesn't work with the action system, so we handle it in the + * nextmatch by linking automatically to the target row. This allows an additional handler. + * It should accept a row UID and a File[], and return a boolean Execute the default (link) action + * + * @param {String|Function} handler + */ + et2_nextmatch.prototype.set_onfiledrop = function (handler) { + this.options.onfiledrop = handler; + }; + /** + * Handle drops of files by linking to the row, if possible. + * + * HTML5 / native file drops conflict with jQueryUI draggable, which handles + * all our drop actions. So we side-step the issue by registering an additional + * drop handler on the rows parent. If the row/actions itself doesn't handle + * the drop, it should bubble and get handled here. + * + * @param {object} event + * @param {object} target + */ + et2_nextmatch.prototype.handle_drop = function (event, target) { + // Check to see if we can handle the link + // First, find the UID + var row = this.controller.getRowByNode(target); + var uid = row.uid || null; + // Get the file information + var files = []; + if (event.originalEvent && event.originalEvent.dataTransfer && + event.originalEvent.dataTransfer.files && event.originalEvent.dataTransfer.files.length > 0) { + files = event.originalEvent.dataTransfer.files; + } + else { + return false; + } + // Exectute the custom handler code + if (this.options.onfiledrop && !this.options.onfiledrop.call(this, uid, files)) { + return false; + } + event.stopPropagation(); + event.preventDefault(); + if (!row || !row.uid) + return false; + // Link the file to the row + // just use a link widget, it's all already done + var split = uid.split('::'); + var link_value = { + to_app: split.shift(), + to_id: split.join('::') + }; + // Create widget and mangle to our needs + var link = et2_createWidget("link-to", { value: link_value }, this); + link.loadingFinished(); + link.file_upload.set_drop_target(false); + if (row.row.tr) { + // Ignore most of the UI, just use the status indicators + var status_1 = jQuery(document.createElement("div")) + .addClass('et2_link_to') + .width(row.row.tr.width()) + .position({ my: "left top", at: "left top", of: row.row.tr }) + .append(link.status_span) + .append(link.file_upload.progress) + .appendTo(row.row.tr); + // Bind to link event so we can remove when done + link.div.on('link.et2_link_to', function (e, linked) { + if (!linked) { + jQuery("li.success", link.file_upload.progress) + .removeClass('success').addClass('validation_error'); + } + else { + // Update row + link._parent.refresh(uid, 'edit'); + } + // Fade out nicely + status_1.delay(linked ? 1 : 2000) + .fadeOut(500, function () { + link.destroy(); + status_1.remove(); + }); + }); + } + // Upload and link - this triggers the upload, which triggers the link, which triggers the cleanup and refresh + link.file_upload.set_value(files); + }; + et2_nextmatch.prototype.getDOMNode = function (_sender) { + if (_sender == this || typeof _sender === 'undefined') { + return this.div[0]; + } + if (_sender == this.header) { + return this.header.div[0]; + } + for (var i = 0; i < this.columns.length; i++) { + if (this.columns[i] && this.columns[i].widget && _sender == this.columns[i].widget) { + return this.dataview.getHeaderContainerNode(i); + } + } + // Let header have a chance + if (_sender && _sender._parent && _sender._parent == this) { + return this.header.getDOMNode(_sender); + } + return null; + }; + // Input widget + /** + * Get the current 'value' for the nextmatch + */ + et2_nextmatch.prototype.getValue = function () { + var _ids = this.getSelection(); + // Translate the internal uids back to server uids + var idsArr = _ids.ids; + for (var i = 0; i < idsArr.length; i++) { + idsArr[i] = idsArr[i].split("::").pop(); + } + var value = { + "selected": idsArr + }; + jQuery.extend(value, this.activeFilters, this.value); + return value; + }; + et2_nextmatch.prototype.resetDirty = function () { }; + et2_nextmatch.prototype.isDirty = function () { return typeof this.value !== 'undefined'; }; + et2_nextmatch.prototype.isValid = function () { return true; }; + et2_nextmatch.prototype.set_value = function (_value) { + this.value = _value; + }; + // Printing + /** + * Prepare for printing + * + * We check for un-loaded rows, and ask the user what they want to do about them. + * If they want to print them all, we ask the server and print when they're loaded. + */ + et2_nextmatch.prototype.beforePrint = function () { + // Add the class, if needed + this.div.addClass('print'); + // Trigger resize, so we can fit on a page + this.dynheight.outerNode.css('max-width', this.div.css('max-width')); + this.resize(); + // Reset height to auto (after width resize) so there's no restrictions + this.dynheight.innerNode.css('height', 'auto'); + // Check for rows that aren't loaded yet, or lots of rows + var range = this.controller._grid.getIndexRange(); + this.print.old_height = this.controller._grid._scrollHeight; + var loaded_count = range.bottom - range.top + 1; + var total = this.controller._grid.getTotalCount(); + // Defer the printing to ask about columns & rows + var defer = jQuery.Deferred(); + var pref = this.options.settings.columnselection_pref; + if (pref.indexOf('nextmatch') == 0) { + pref = 'nextmatch-' + pref; + } + var app = this.getInstanceManager().app; + var columns = {}; + var columnMgr = this.dataview.getColumnMgr(); + pref += '_print'; + var columns_selected = []; + // Get column names + for (var i = 0; i < columnMgr.columns.length; i++) { + var col = columnMgr.columns[i]; + var widget = this.columns[i].widget; + var colName = this._getColumnName(widget); + if (col.caption && col.visibility !== et2_dataview_grid.ET2_COL_VISIBILITY_ALWAYS_NOSELECT && + col.visibility !== et2_dataview_grid.ET2_COL_VISIBILITY_DISABLED) { + columns[colName] = col.caption; + if (col.visibility === et2_dataview_grid.ET2_COL_VISIBILITY_VISIBLE) + columns_selected.push(colName); + } + // Custom fields get listed separately + if (widget.instanceOf(et2_nextmatch_customfields)) { + delete (columns[colName]); + colName = widget.id; + if (col.visibility === et2_dataview_grid.ET2_COL_VISIBILITY_VISIBLE && !jQuery.isEmptyObject(widget.customfields)) { + columns[colName] = col.caption; + for (var field_name in widget.customfields) { + columns[et2_nextmatch_customfields.PREFIX + field_name] = " - " + widget.customfields[field_name].label; + if (widget.options.fields[field_name] && columns_selected.indexOf(colName) >= 0) { + columns_selected.push(et2_nextmatch_customfields.PREFIX + field_name); + } + } + } + } + } + // Preference exists? Set it now + if (this.egw().preference(pref, app)) { + this.set_columns(jQuery.extend([], this.egw().preference(pref, app))); + } + var callback = jQuery.proxy(function (button, value) { + if (button === et2_dialog.CANCEL_BUTTON) { + // Give dialog a chance to close, or it will be in the print + window.setTimeout(function () { + defer.reject(); + }, 0); + return; + } + // Set CSS for orientation + this.div.addClass(value.orientation); + this.egw().set_preference(app, pref + '_orientation', value.orientation); + // Try to tell browser about orientation + var css = '@page { size: ' + value.orientation + '; }', head = document.head || document.getElementsByTagName('head')[0], style = document.createElement('style'); + style.type = 'text/css'; + style.media = 'print'; + // @ts-ignore + if (style.styleSheet) { + // @ts-ignore + style.styleSheet.cssText = css; + } + else { + style.appendChild(document.createTextNode(css)); + } + head.appendChild(style); + this.print.orientation_style = style; + // Trigger resize, so we can fit on a page + this.dynheight.outerNode.css('max-width', this.div.css('max-width')); + // Handle columns + this.set_columns(value.columns); + this.egw().set_preference(app, pref, value.columns); + var rows = parseInt(value.row_count); + if (rows > total) { + rows = total; + } + // If they want the whole thing, style it as all + if (button === et2_dialog.OK_BUTTON && rows == this.controller._grid.getTotalCount()) { + // Add the class, gives more reliable sizing + this.div.addClass('print'); + // Show it all + jQuery('.egwGridView_scrollarea', this.div).css('height', 'auto'); + } + // We need more rows + if (button === 'dialog[all]' || rows > loaded_count) { + var count_1 = 0; + var fetchedCount_1 = 0; + var cancel_1 = false; + var nm_1 = this; + var dialog_1 = et2_dialog.show_dialog( + // Abort the long task if they canceled the data load + function () { + count_1 = total; + cancel_1 = true; + window.setTimeout(function () { + defer.reject(); + }, 0); + }, egw.lang('Loading'), egw.lang('please wait...'), {}, [ + { "button_id": et2_dialog.CANCEL_BUTTON, "text": 'cancel', id: 'dialog[cancel]', image: 'cancel' } + ]); + // dataFetch() is asynchronous, so all these requests just get fired off... + // 200 rows chosen arbitrarily to reduce requests. + do { + var ctx = { + "self": this.controller, + "start": count_1, + "count": Math.min(rows, 200), + "lastModification": this.controller._lastModification + }; + if (nm_1.controller.dataStorePrefix) { + // @ts-ignore + ctx.prefix = nm_1.controller.dataStorePrefix; + } + nm_1.controller.dataFetch({ start: count_1, num_rows: Math.min(rows, 200) }, function (data) { + // Keep track + if (data && data.order) { + fetchedCount_1 += data.order.length; + } + nm_1.controller._fetchCallback.apply(this, arguments); + if (fetchedCount_1 >= rows) { + if (cancel_1) { + dialog_1.destroy(); + defer.reject(); + return; + } + // Use CSS to hide all but the requested rows + // Prevents us from showing more than requested, if actual height was less than average + nm_1.print_row_selector = ".egwGridView_grid > tbody > tr:not(:nth-child(-n+" + rows + "))"; + egw.css(nm_1.print_row_selector, 'display: none'); + // No scrollbar in print view + jQuery('.egwGridView_scrollarea', this.div).css('overflow-y', 'hidden'); + // Show it all + jQuery('.egwGridView_scrollarea', this.div).css('height', 'auto'); + // Grid needs to redraw before it can be printed, so wait + window.setTimeout(jQuery.proxy(function () { + dialog_1.destroy(); + // Should be OK to print now + defer.resolve(); + }, nm_1), et2_dataview_grid.ET2_GRID_INVALIDATE_TIMEOUT); + } + }, ctx); + count_1 += 200; + } while (count_1 < rows); + nm_1.controller._grid.setScrollHeight(nm_1.controller._grid.getAverageHeight() * (rows + 1)); + } + else { + // Don't need more rows, limit to requested and finish + // Show it all + jQuery('.egwGridView_scrollarea', this.div).css('height', 'auto'); + // Use CSS to hide all but the requested rows + // Prevents us from showing more than requested, if actual height was less than average + this.print_row_selector = ".egwGridView_grid > tbody > tr:not(:nth-child(-n+" + rows + "))"; + egw.css(this.print_row_selector, 'display: none'); + // No scrollbar in print view + jQuery('.egwGridView_scrollarea', this.div).css('overflow-y', 'hidden'); + // Give dialog a chance to close, or it will be in the print + window.setTimeout(function () { + defer.resolve(); + }, 0); + } + }, this); + var value = { + content: { + row_count: Math.min(100, total), + columns: this.egw().preference(pref, app) || columns_selected, + orientation: this.egw().preference(pref + '_orientation', app) + }, + sel_options: { + columns: columns + } + }; + this._create_print_dialog.call(this, value, callback); + return defer; + }; + /** + * Create and show the print dialog, which calls the provided callback when + * done. Broken out for overriding if needed. + * + * @param {Object} value Current settings and preferences, passed to the dialog for + * the template + * @param {Object} value.content + * @param {Object} value.sel_options + * + * @param {function(int, Object)} callback - Process the dialog response, + * format things according to the specified orientation and fetch any needed + * rows. + * + */ + et2_nextmatch.prototype._create_print_dialog = function (value, callback) { + var base_url = this.getInstanceManager().template_base_url; + if (base_url.substr(base_url.length - 1) == '/') + base_url = base_url.slice(0, -1); // otherwise we generate a url //api/templates, which is wrong + var tab = this.get_tab_info(); + // Get title for print dialog from settings or tab, if available + var title = this.options.settings.label ? this.options.settings.label : (tab ? tab.label : ''); + var dialog = et2_createWidget("dialog", { + // If you use a template, the second parameter will be the value of the template, as if it were submitted. + callback: callback, + buttons: et2_dialog.BUTTONS_OK_CANCEL, + title: this.egw().lang('Print') + ' ' + this.egw().lang(title), + template: this.egw().link(base_url + '/api/templates/default/nm_print_dialog.xet'), + value: value + }); + }; + /** + * Try to clean up the mess we made getting ready for printing + * in beforePrint() + */ + et2_nextmatch.prototype.afterPrint = function () { + this.div.removeClass('print landscape portrait'); + jQuery(this.print.orientation_style).remove(); + delete this.print.orientation_style; + // Put scrollbar back + jQuery('.egwGridView_scrollarea', this.div).css('overflow-y', ''); + // Correct size of grid, and trigger resize to fix it + this.controller._grid.setScrollHeight(this.print.old_height); + delete this.print.old_height; + // Remove CSS rule hiding extra rows + if (this.print.row_selector) { + egw.css(this.print.row_selector, ''); + delete this.print.row_selector; + } + // Restore columns + var pref = []; + var app = this.getInstanceManager().app; + if (this.options.settings.columnselection_pref.indexOf('nextmatch') == 0) { + pref = egw.preference(this.options.settings.columnselection_pref, app); + } + else { + // 'nextmatch-' prefix is there in preference name, but not in setting, so add it in + pref = egw.preference("nextmatch-" + this.options.settings.columnselection_pref, app); + } + if (pref) { + if (typeof pref === 'string') + pref = pref.split(','); + // @ts-ignore + this.set_columns(pref, app); + } + this.dynheight.outerNode.css('max-width', 'inherit'); + this.resize(); + }; + et2_nextmatch._attributes = { + // These normally set in settings, but broken out into attributes to allow run-time changes + "template": { + "name": "Template", + "type": "string", + "description": "The id of the template which contains the grid layout." + }, + "hide_header": { + "name": "Hide header", + "type": "boolean", + "description": "Hide the header", + "default": false + }, + "header_left": { + "name": "Left custom template", + "type": "string", + "description": "Customise the nextmatch - left side. Provided template becomes a child of nextmatch, and any input widgets are automatically bound to refresh the nextmatch on change. Any inputs with an onChange attribute can trigger the nextmatch to refresh by returning true.", + "default": "" + }, + "header_right": { + "name": "Right custom template", + "type": "string", + "description": "Customise the nextmatch - right side. Provided template becomes a child of nextmatch, and any input widgets are automatically bound to refresh the nextmatch on change. Any inputs with an onChange attribute can trigger the nextmatch to refresh by returning true.", + "default": "" + }, + "header_row": { + "name": "Inline custom template", + "type": "string", + "description": "Customise the nextmatch - inline, after row count. Provided template becomes a child of nextmatch, and any input widgets are automatically bound to refresh the nextmatch on change. Any inputs with an onChange attribute can trigger the nextmatch to refresh by returning true.", + "default": "" + }, + "no_filter": { + "name": "No filter", + "type": "boolean", + "description": "Hide the first filter", + "default": et2_no_init + }, + "no_filter2": { + "name": "No filter2", + "type": "boolean", + "description": "Hide the second filter", + "default": et2_no_init + }, + "view": { + "name": "View", + "type": "string", + "description": "Display entries as either 'row' or 'tile'. A matching template must also be set after changing this.", + "default": et2_no_init + }, + "onselect": { + "name": "onselect", + "type": "js", + "default": et2_no_init, + "description": "JS code which gets executed when rows are selected. Can also be a app.appname.func(selected) style method" + }, + "onfiledrop": { + "name": "onFileDrop", + "type": "js", + "default": et2_no_init, + "description": "JS code that gets executed when a _file_ is dropped on a row. Other drop interactions are handled by the action system. Return false to prevent the default link action." + }, + "settings": { + "name": "Settings", + "type": "any", + "description": "The nextmatch settings", + "default": {} + } + }; + et2_nextmatch.legacyOptions = ["template", "hide_header", "header_left", "header_right"]; + return et2_nextmatch; +}(et2_core_DOMWidget_1.et2_DOMWidget)); +exports.et2_nextmatch = et2_nextmatch; +et2_core_widget_1.et2_register_widget(et2_nextmatch, ["nextmatch"]); /** * Standard nextmatch header bar, containing filters, search, record count, letter filters, etc. * @@ -2510,1368 +2058,1189 @@ et2_register_widget(et2_nextmatch, ["nextmatch"]); * actually load templates from the server. * @augments et2_DOMWidget */ -var et2_nextmatch_header_bar = (function(){ "use strict"; return et2_DOMWidget.extend(et2_INextmatchHeader, -{ - attributes: { - "filter_label": { - "name": "Filter label", - "type": "string", - "description": "Label for filter", - "default": "", - "translate": true - }, - "filter_help": { - "name": "Filter help", - "type": "string", - "description": "Help message for filter", - "default": "", - "translate": true - }, - "filter": { - "name": "Filter value", - "type": "any", - "description": "Current value for filter", - "default": "" - }, - "no_filter": { - "name": "No filter", - "type": "boolean", - "description": "Remove filter", - "default": false - } - }, - headers: [], - header_div: [], - - /** - * Constructor - * - * @param nextmatch - * @param nm_div - * @memberOf et2_nextmatch_header_bar - */ - init: function(nextmatch, nm_div) { - this._super.apply(this, [nextmatch,nextmatch.options.settings]); - this.nextmatch = nextmatch; - this.div = jQuery(document.createElement("div")) - .addClass("nextmatch_header"); - this._createHeader(); - - // Flag to avoid loops while updating filters - this.update_in_progress = false; - }, - - destroy: function() { - this.nextmatch = null; - - this._super.apply(this, arguments); - this.div = null; - }, - - setNextmatch: function(nextmatch) { - var create_once = (this.nextmatch == null); - this.nextmatch = nextmatch; - if(create_once) - { - this._createHeader(); - } - - // Bind row count - this.nextmatch.dataview.grid.setInvalidateCallback(function () { - this.count_total.text(this.nextmatch.dataview.grid.getTotalCount() + ""); - }, this); - }, - - /** - * Actions are handled by the controller, so ignore these - * - * @param {object} actions - */ - set_actions: function(actions) {}, - - _createHeader: function() { - - var self = this; - var nm_div = this.nextmatch.div; - var settings = this.nextmatch.options.settings; - - this.div.prependTo(nm_div); - - // Left & Right (& row) headers - this.headers = [ - {id:this.nextmatch.options.header_left}, - {id:this.nextmatch.options.header_right}, - {id:this.nextmatch.options.header_row} - ]; - - // The rest of the header - this.header_div = this.row_div = jQuery(document.createElement("div")) - .addClass("nextmatch_header_row") - .appendTo(this.div); - this.filter_div = jQuery(document.createElement("div")) - .addClass('filtersContainer') - .appendTo(this.row_div); - - // Search - this.search_box = jQuery(document.createElement("div")) - .addClass('search') - .prependTo(egwIsMobile()?this.nextmatch.div:this.row_div); - - // searchbox widget options - var searchbox_options = { - id:"search", - overlay:(typeof settings.searchbox != 'undefined' && typeof settings.searchbox.overlay != 'undefined')?settings.searchbox.overlay:false, - onchange:function(){ - self.nextmatch.applyFilters({search: this.get_value()}); - }, - value:settings.search, - fix:!egwIsMobile() - }; - // searchbox widget - this.et2_searchbox = et2_createWidget('searchbox', searchbox_options,this); - - // Set activeFilters to current value - this.nextmatch.activeFilters.search = settings.search; - - this.et2_searchbox.set_value(settings.search); - /** - * Mobile theme specific part for nm header - * nm header has very different behaivior for mobile theme and basically - * it has its own markup separately from nm header in normal templates. - */ - if (egwIsMobile()) - { - this.search_box.addClass('nm-mob-header'); - jQuery(this.div).css({display:'inline-block'}).addClass('nm_header_hide'); - - //indicates appname in header - jQuery(document.createElement('div')) - .addClass('nm_appname_header') - .text(egw.lang(egw.app_name())) - .appendTo(this.search_box); - - this.delete_action = jQuery(document.createElement('div')) - .addClass('nm_delete_action') - .prependTo(this.search_box); - // toggle header - // add new button - this.fav_span = jQuery(document.createElement('div')) - .addClass('nm_favorites_div') - .prependTo(this.search_box); - // toggle header menu - this.toggle_header = jQuery(document.createElement('button')) - .addClass('nm_toggle_header') - .click(function(){ - jQuery(self.div).toggleClass('nm_header_hide'); - jQuery(this).toggleClass('nm_toggle_header_on'); - window.setTimeout(function(){self.nextmatch.resize();},800); - }) - .prependTo(this.search_box); - // Context menu - this.action_header = jQuery(document.createElement('button')) - .addClass('nm_action_header') - .hide() - .click (function(e){ - jQuery('tr.selected',self.nextmatch.div).trigger({type:'contextmenu',which:3,originalEvent:e}); - }) - .prependTo(this.search_box); - } - - // Add category - if(!settings.no_cat) { - if (typeof settings.cat_id_label == 'undefined') settings.cat_id_label = ''; - this.category = this._build_select('cat_id', settings.cat_is_select ? - 'select' : 'select-cat', settings.cat_id, settings.cat_is_select !== true, { - multiple: false, - tags: true, - class: "select-cat", - value_class: settings.cat_id_class - }); - } - - // Filter 1 - if(!settings.no_filter) { - this.filter = this._build_select('filter', 'select', settings.filter, settings.filter_no_lang); - } - - // Filter 2 - if(!settings.no_filter2) { - this.filter2 = this._build_select('filter2', 'select', settings.filter2, - settings.filter2_no_lang, { - multiple: false, - tags: settings.filter2_tags, - class: "select-cat", - value_class: settings.filter2_class - }); - } - - // Other stuff - this.right_div = jQuery(document.createElement("div")) - .addClass('header_row_right').appendTo(this.row_div); - - // Record count - this.count = jQuery(document.createElement("span")) - .addClass("header_count ui-corner-all"); - - // Need to figure out how to update this as grid scrolls - // this.count.append("? - ? ").append(egw.lang("of")).append(" "); - this.count_total = jQuery(document.createElement("span")) - .appendTo(this.count) - .text(settings.total + ""); - this.count.prependTo(this.right_div); - - // Favorites - this._setup_favorites(settings['favorites']); - - // Export - if(typeof settings.csv_fields != "undefined" && settings.csv_fields != false) - { - var definition = settings.csv_fields; - if(settings.csv_fields === true) - { - definition = egw.preference('nextmatch-export-definition', this.nextmatch.egw().getAppName()); - } - var button = et2_createWidget("buttononly", {id: "export", "statustext": "Export", image: "download", "background_image": true}, this); - jQuery(button.getDOMNode()) - .click(this.nextmatch, function(event) { - egw_openWindowCentered2( egw.link('/index.php', { - 'menuaction': 'importexport.importexport_export_ui.export_dialog', - 'appname': event.data.egw().getAppName(), - 'definition': definition - }), '_blank', 850, 440, 'yes'); - }); - } - - // Another place to customize nextmatch - this.header_row = jQuery(document.createElement("div")) - .addClass('header_row').appendTo(this.right_div); - - // Letter search - var current_letter = this.nextmatch.options.settings.searchletter ? - this.nextmatch.options.settings.searchletter : - (this.nextmatch.activeFilters ? this.nextmatch.activeFilters.searchletter : false); - if(this.nextmatch.options.settings.lettersearch || current_letter) - { - this.lettersearch = jQuery(document.createElement("table")) - .addClass('nextmatch_lettersearch') - .css("width", "100%") - .appendTo(this.div); - var tbody = jQuery(document.createElement("tbody")).appendTo(this.lettersearch); - var row = jQuery(document.createElement("tr")).appendTo(tbody); - - // Capitals, A-Z - var letters = this.egw().lang('ABCDEFGHIJKLMNOPQRSTUVWXYZ').split(''); - for(var i in letters) { - var button = jQuery(document.createElement("td")) - .addClass("lettersearch") - .appendTo(row) - .attr("id", letters[i]) - .text(letters[i]); - if(letters[i] == current_letter) button.addClass("lettersearch_active"); - } - button = jQuery(document.createElement("td")) - .addClass("lettersearch") - .appendTo(row) - .attr("id", "") - .text(egw.lang("all")); - if(!current_letter) button.addClass("lettersearch_active"); - - this.lettersearch.click(this.nextmatch, function(event) { - // this is the lettersearch table - jQuery("td",this).removeClass("lettersearch_active"); - jQuery(event.target).addClass("lettersearch_active"); - event.data.applyFilters({searchletter: event.target.id || false}); - }); - // Set activeFilters to current value - this.nextmatch.activeFilters.searchletter = current_letter; - } - // Apply letter search preference - var lettersearch_preference = "nextmatch-" + this.nextmatch.options.settings.columnselection_pref + "-lettersearch"; - if(this.lettersearch && !egw.preference(lettersearch_preference,this.nextmatch.egw().getAppName())) - { - this.lettersearch.hide(); - } - }, - - - /** - * Build & bind to a sub-template into the header - * - * @param {string} location One of left, right, or row - * @param {string} template_name Name of the template to load into the location - */ - _build_header: function(location, template_name) - { - var id = location == "left" ? 0 : (location == "right" ? 1 : 2); - var existing = this.headers[id]; - if(existing && existing._type) - { - if(existing.id == template_name) return; - existing.free(); - this.headers[id] = ''; - } - - // Load the template - var self = this; - var header = et2_createWidget("template", {"id": template_name}, this); - this.headers[id] = header; - var deferred = []; - header.loadingFinished(deferred); - - // Wait until all child widgets are loaded, then bind - jQuery.when.apply(jQuery,deferred).then(function() { - // fix order in DOM by reattaching templates in correct position - switch (id) { - case 0: // header_left: prepend - jQuery(header.getDOMNode()).prependTo(self.header_div); - break; - case 1: // header_right: before favorites and count - jQuery(header.getDOMNode()).prependTo(self.header_div.find('div.header_row_right')); - break; - case 2: // header_row: after search - window.setTimeout(function(){ // otherwise we might end up after filters - jQuery(header.getDOMNode()).insertAfter(self.header_div.find('div.search')); - }, 1); - break; - } - self._bindHeaderInput(header); - }); - }, - - /** - * Build the selectbox filters in the header bar - * Sets value, options, labels, and change handlers - * - * @param {string} name - * @param {string} type - * @param {string} value - * @param {string} lang - * @param {object} extra - */ - _build_select: function(name, type, value, lang, extra) { - var widget_options = jQuery.extend({ - "id": name, - "label": this.nextmatch.options.settings[name+"_label"], - "no_lang": lang, - "disabled": this.nextmatch.options['no_'+name] - }, extra); - - // Set select options - // Check in content for options- - var mgr = this.nextmatch.getArrayMgr("content"); - var options = mgr.getEntry("options-" + name); - // Look in sel_options - if(!options) options = this.nextmatch.getArrayMgr("sel_options").getEntry(name); - // Check parent sel_options, because those are usually global and don't get passed down - if(!options) options = this.nextmatch.getArrayMgr("sel_options").parentMgr.getEntry(name); - // Sometimes legacy stuff puts it in here - if(!options) options = mgr.getEntry('rows[sel_options]['+name+']'); - - // Maybe in a row, and options got stuck in ${row} instead of top level - var row_stuck = ['${row}','{$row}']; - for(var i = 0; !options && i < row_stuck.length; i++) - { - var row_id = ''; - if((!options || options.length == 0) && ( - // perspectiveData.row in nm, data["${row}"] in an auto-repeat grid - this.nextmatch.getArrayMgr("sel_options").perspectiveData.row || this.nextmatch.getArrayMgr("sel_options").data[row_stuck[i]])) - { - var row_id = name.replace(/[0-9]+/,row_stuck[i]); - options = this.nextmatch.getArrayMgr("sel_options").getEntry(row_id); - if(!options) - { - row_id = row_stuck[i] + "["+name+"]"; - options = this.nextmatch.getArrayMgr("sel_options").getEntry(row_id); - } - } - if(options) - { - this.egw().debug('warn', 'Nextmatch filter options in a weird place - "%s". Should be in sel_options[%s].',row_id,name); - } - } - // Legacy: Add in 'All' option for cat_id, if not provided. - if(name == 'cat_id' && options != null && (typeof options[''] == 'undefined' && typeof options[0] != 'undefined' && options[0].value != '')) - { - widget_options.empty_label = this.egw().lang('All categories'); - } - - // Create widget - var select = et2_createWidget(type, widget_options, this); - - if(options) select.set_select_options(options); - - // Set value - select.set_value(value); - - // Set activeFilters to current value - this.nextmatch.activeFilters[select.id] = select.get_value(); - - // Set onChange - var input = select.input; - - // Tell framework to ignore, or it will reset it to ''/empty when it does loadingFinished() - select.attributes.select_options.ignore = true; - - if (this.nextmatch.options.settings[name+"_onchange"]) - { - // Get the onchange function string - var onchange = this.nextmatch.options.settings[name+"_onchange"]; - - // Real submits cause all sorts of problems - if(onchange.match(/this\.form\.submit/)) - { - this.egw().debug("warn","%s tries to submit form, which is not allowed. Filter changes automatically refresh data with no reload.",name); - onchange = onchange.replace(/this\.form\.submit\([^)]*\);?/,'return true;'); - } - - // Connect it to the onchange event of the input element - may submit - select.change = et2_compileLegacyJS(onchange, this.nextmatch, select.getInputNode()); - this._bindHeaderInput(select); - } - else // default request changed rows with new filters, previous this.form.submit() - { - input.change(this.nextmatch, function(event) { - var set = {}; - set[name] = select.getValue(); - event.data.applyFilters(set); - }); - } - return select; - }, - - /** - * Set up the favorites UI control - * - * @param filters Array|boolean The nextmatch setting for favorites. Either true, or a list of - * additional fields/settings to add in to the favorite. - */ - _setup_favorites: function(filters) { - if(typeof filters == "undefined" || filters === false) - { - // No favorites configured - return; - } - - var list = et2_csvSplit(this.options.get_rows, 2, "."); - var widget_options = { - default_pref: "nextmatch-" + this.nextmatch.options.settings.columnselection_pref + "-favorite", - app: list[0], - filters: filters, - sidebox_target:'favorite_sidebox_'+list[0] - }; - this.favorites = et2_createWidget('favorites', widget_options, this); - - // Add into header - jQuery(this.favorites.getDOMNode(this.favorites)).prependTo(egwIsMobile()?this.search_box.find('.nm_favorites_div').show():this.right_div); - }, - - /** - * Updates all the filter elements in the header - * - * Does not actually refresh the data, just sets values to match those given. - * Called by et2_nextmatch.applyFilters(). - * - * @param filters Array Key => Value pairs of current filters - */ - setFilters: function(filters) { - - // Avoid loops cause by change events - if(this.update_in_progress) return; - this.update_in_progress = true; - - // Use an array mgr to hande non-simple IDs - var mgr = new et2_arrayMgr(filters); - - this.iterateOver(function(child) { - // Skip favorites, don't want them in the filter - if(typeof child.id != "undefined" && child.id.indexOf("favorite") == 0) return; - - var value = ''; - if(typeof child.set_value != "undefined" && child.id) - { - value = mgr.getEntry(child.id); - if (value == null) value = ''; - /** - * Sometimes a filter value is not in current options. This can - * happen in a saved favorite, for example, or if server changes - * some filter options, and the order doesn't work out. The normal behaviour - * is to warn & not set it, but for nextmatch we'll just add it - * in, and let the server either set it properly, or ignore. - */ - if(value && typeof value != 'object' && child.instanceOf(et2_selectbox)) - { - var found = typeof child.options.select_options[value] != 'undefined'; - // options is array of objects with attribute value&label - if (jQuery.isArray(child.options.select_options)) - { - for(var o=0; o < child.options.select_options.length; ++o) - { - if (child.options.select_options[o].value == value) - { - found = true; - break; - } - } - } - if (!found) - { - var old_options = child.options.select_options; - // Actual label is not available, obviously, or it would be there - old_options[value] = child.egw().lang("Loading"); - child.set_select_options(old_options); - } - } - child.set_value(value); - } - if(typeof child.get_value == "function" && child.id) - { - // Put data in the proper place - var target = this; - var value = child.get_value(); - - // Split up indexes - var indexes = child.id.replace(/[/g,'[').split('['); - - for(var i = 0; i < indexes.length; i++) - { - indexes[i] = indexes[i].replace(/]/g,'').replace(']',''); - if (i < indexes.length-1) - { - if(typeof target[indexes[i]] == "undefined") target[indexes[i]] = {}; - target = target[indexes[i]]; - } - else - { - target[indexes[i]] = value; - } - } - } - }, filters); - - // Letter search - if(this.nextmatch.options.settings.lettersearch) - { - jQuery("td",this.lettersearch).removeClass("lettersearch_active"); - jQuery(filters.searchletter ? "td#"+filters.searchletter : "td.lettersearch[id='']",this.lettersearch).addClass("lettersearch_active"); - - // Set activeFilters to current value - filters.searchletter = jQuery("td.lettersearch_active",this.lettersearch).attr("id") || false; - } - - // Reset flag - this.update_in_progress = false; - }, - - /** - * Help out nextmatch / widget stuff by checking to see if sender is part of header - * - * @param {et2_widget} _sender - */ - getDOMNode: function(_sender) { - var filters = [this.category, this.filter, this.filter2]; - for(var i = 0; i < filters.length; i++) - { - if(_sender == filters[i]) - { - // Give them the filter div - return this.filter_div[0]; - } - } - if(_sender == this.et2_searchbox) return this.search_box[0]; - if(_sender.id == 'export') return this.right_div[0]; - - if(_sender && _sender._type == "template") - { - for(var i = 0; i < this.headers.length; i++) - { - if(_sender.id == this.headers[i].id && _sender._parent == this) return i == 2 ? this.header_row[0] : this.header_div[0]; - } - } - return null; - }, - - /** - * Bind all the inputs in the header sub-templates to update the filters - * on change, and update current filter with the inputs' current values - * - * @param {et2_template} sub_header - */ - _bindHeaderInput: function(sub_header) { - var header = this; - - var bind_change = function(_widget){ - // Previously set change function - var widget_change = _widget.change; - - var change = function(_node) { - // Call previously set change function - var result = widget_change.call(_widget,_node); - - // Update filters, if we're not already doing so - if((result || typeof result === 'undefined') && _widget.isDirty() && !header.update_in_progress) { - // Update dirty - _widget._oldValue = _widget.getValue(); - - // Widget will not have an entry in getValues() because nulls - // are not returned, we remove it from activeFilters - if(_widget._oldValue == null) - { - var path = _widget.getArrayMgr('content').explodeKey(_widget.id); - if(path.length > 0) - { - var entry = header.nextmatch.activeFilters; - var i = 0; - for(; i < path.length-1; i++) - { - entry = entry[path[i]]; - } - delete entry[path[i]]; - } - header.nextmatch.applyFilters(header.nextmatch.activeFilters); - } - else - { - // Not null is easy, just get values - var value = this.getInstanceManager().getValues(sub_header); - header.nextmatch.applyFilters(value[header.nextmatch.id]); - } - } - // In case this gets bound twice, it's important to return - return true; - }; - - _widget.change = change; - - // Set activeFilters to current value - // Use an array mgr to hande non-simple IDs - var value = {}; - value[_widget.id] = _widget._oldValue = _widget.getValue(); - var mgr = new et2_arrayMgr(value); - jQuery.extend(true, this.nextmatch.activeFilters,mgr.data); - }; - if(sub_header.instanceOf(et2_inputWidget)) - { - bind_change.call(this, sub_header); - } - else - { - sub_header.iterateOver(bind_change, this, et2_inputWidget); - } - } -});}).call(this); -et2_register_widget(et2_nextmatch_header_bar, ["nextmatch_header_bar"]); - +var et2_nextmatch_header_bar = /** @class */ (function (_super) { + __extends(et2_nextmatch_header_bar, _super); + /** + * Constructor + * + * @param _parent + * @param _attrs + * @param _child + */ + function et2_nextmatch_header_bar(_parent, _attrs, _child) { + var _this = _super.call(this, _parent, [_parent, _parent.options.settings], et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_nextmatch_header_bar._attributes, _child || {})) || this; + _this.nextmatch = _parent; + _this.div = jQuery(document.createElement("div")) + .addClass("nextmatch_header"); + _this._createHeader(); + // Flag to avoid loops while updating filters + _this.update_in_progress = false; + return _this; + } + et2_nextmatch_header_bar.prototype.destroy = function () { + this.nextmatch = null; + _super.prototype.destroy.call(this); + this.div = null; + }; + et2_nextmatch_header_bar.prototype.setNextmatch = function (nextmatch) { + var create_once = (this.nextmatch == null); + this.nextmatch = nextmatch; + if (create_once) { + this._createHeader(); + } + // Bind row count + this.nextmatch.dataview.grid.setInvalidateCallback(function () { + this.count_total.text(this.nextmatch.dataview.grid.getTotalCount() + ""); + }, this); + }; + /** + * Actions are handled by the controller, so ignore these + * + * @param {object} actions + */ + et2_nextmatch_header_bar.prototype.set_actions = function (actions) { }; + et2_nextmatch_header_bar.prototype._createHeader = function () { + var button; + var self = this; + var nm_div = this.nextmatch.getDOMNode(); + var settings = this.nextmatch.options.settings; + this.div.prependTo(nm_div); + // Left & Right (& row) headers + this.headers = [ + { id: this.nextmatch.options.header_left }, + { id: this.nextmatch.options.header_right }, + { id: this.nextmatch.options.header_row } + ]; + // The rest of the header + this.header_div = this.row_div = jQuery(document.createElement("div")) + .addClass("nextmatch_header_row") + .appendTo(this.div); + this.filter_div = jQuery(document.createElement("div")) + .addClass('filtersContainer') + .appendTo(this.row_div); + // Search + this.search_box = jQuery(document.createElement("div")) + .addClass('search') + .prependTo(egwIsMobile() ? this.nextmatch.getDOMNode() : this.row_div); + // searchbox widget options + var searchbox_options = { + id: "search", + overlay: (typeof settings.searchbox != 'undefined' && typeof settings.searchbox.overlay != 'undefined') ? settings.searchbox.overlay : false, + onchange: function () { + self.nextmatch.applyFilters({ search: this.get_value() }); + }, + value: settings.search, + fix: !egwIsMobile() + }; + // searchbox widget + this.et2_searchbox = et2_createWidget('searchbox', searchbox_options, this); + // Set activeFilters to current value + this.nextmatch.activeFilters.search = settings.search; + this.et2_searchbox.set_value(settings.search); + /** + * Mobile theme specific part for nm header + * nm header has very different behaivior for mobile theme and basically + * it has its own markup separately from nm header in normal templates. + */ + if (egwIsMobile()) { + this.search_box.addClass('nm-mob-header'); + jQuery(this.div).css({ display: 'inline-block' }).addClass('nm_header_hide'); + //indicates appname in header + jQuery(document.createElement('div')) + .addClass('nm_appname_header') + .text(egw.lang(egw.app_name())) + .appendTo(this.search_box); + this.delete_action = jQuery(document.createElement('div')) + .addClass('nm_delete_action') + .prependTo(this.search_box); + // toggle header + // add new button + this.fav_span = jQuery(document.createElement('div')) + .addClass('nm_favorites_div') + .prependTo(this.search_box); + // toggle header menu + this.toggle_header = jQuery(document.createElement('button')) + .addClass('nm_toggle_header') + .click(function () { + jQuery(self.div).toggleClass('nm_header_hide'); + jQuery(this).toggleClass('nm_toggle_header_on'); + window.setTimeout(function () { self.nextmatch.resize(); }, 800); + }) + .prependTo(this.search_box); + // Context menu + this.action_header = jQuery(document.createElement('button')) + .addClass('nm_action_header') + .hide() + .click(function (e) { + // @ts-ignore + jQuery('tr.selected', self.nextmatch.getDOMNode()).trigger({ type: 'contextmenu', which: 3, originalEvent: e }); + }) + .prependTo(this.search_box); + } + // Add category + if (!settings.no_cat) { + if (typeof settings.cat_id_label == 'undefined') + settings.cat_id_label = ''; + this.category = this._build_select('cat_id', settings.cat_is_select ? + 'select' : 'select-cat', settings.cat_id, settings.cat_is_select !== true, { + multiple: false, + tags: true, + class: "select-cat", + value_class: settings.cat_id_class + }); + } + // Filter 1 + if (!settings.no_filter) { + this.filter = this._build_select('filter', 'select', settings.filter, settings.filter_no_lang); + } + // Filter 2 + if (!settings.no_filter2) { + this.filter2 = this._build_select('filter2', 'select', settings.filter2, settings.filter2_no_lang, { + multiple: false, + tags: settings.filter2_tags, + class: "select-cat", + value_class: settings.filter2_class + }); + } + // Other stuff + this.right_div = jQuery(document.createElement("div")) + .addClass('header_row_right').appendTo(this.row_div); + // Record count + this.count = jQuery(document.createElement("span")) + .addClass("header_count ui-corner-all"); + // Need to figure out how to update this as grid scrolls + // this.count.append("? - ? ").append(egw.lang("of")).append(" "); + this.count_total = jQuery(document.createElement("span")) + .appendTo(this.count) + .text(settings.total + ""); + this.count.prependTo(this.right_div); + // Favorites + this._setup_favorites(settings['favorites']); + // Export + if (typeof settings.csv_fields != "undefined" && settings.csv_fields != false) { + var definition_1 = settings.csv_fields; + if (settings.csv_fields === true) { + definition_1 = egw.preference('nextmatch-export-definition', this.nextmatch.egw().app_name()); + } + var button_1 = et2_createWidget("buttononly", { id: "export", "statustext": "Export", image: "download", "background_image": true }, this); + jQuery(button_1.getDOMNode()) + .click(this.nextmatch, function (event) { + // @ts-ignore + egw_openWindowCentered2(egw.link('/index.php', { + 'menuaction': 'importexport.importexport_export_ui.export_dialog', + 'appname': event.data.egw().getAppName(), + 'definition': definition_1 + }), '_blank', 850, 440, 'yes'); + }); + } + // Another place to customize nextmatch + this.header_row = jQuery(document.createElement("div")) + .addClass('header_row').appendTo(this.right_div); + // Letter search + var current_letter = this.nextmatch.options.settings.searchletter ? + this.nextmatch.options.settings.searchletter : + (this.nextmatch.activeFilters ? this.nextmatch.activeFilters.searchletter : false); + if (this.nextmatch.options.settings.lettersearch || current_letter) { + this.lettersearch = jQuery(document.createElement("table")) + .addClass('nextmatch_lettersearch') + .css("width", "100%") + .appendTo(this.div); + var tbody = jQuery(document.createElement("tbody")).appendTo(this.lettersearch); + var row = jQuery(document.createElement("tr")).appendTo(tbody); + // Capitals, A-Z + var letters = this.egw().lang('ABCDEFGHIJKLMNOPQRSTUVWXYZ').split(''); + for (var i in letters) { + button = jQuery(document.createElement("td")) + .addClass("lettersearch") + .appendTo(row) + .attr("id", letters[i]) + .text(letters[i]); + if (letters[i] == current_letter) + button.addClass("lettersearch_active"); + } + button = jQuery(document.createElement("td")) + .addClass("lettersearch") + .appendTo(row) + .attr("id", "") + .text(egw.lang("all")); + if (!current_letter) + button.addClass("lettersearch_active"); + this.lettersearch.click(this.nextmatch, function (event) { + // this is the lettersearch table + jQuery("td", this).removeClass("lettersearch_active"); + jQuery(event.target).addClass("lettersearch_active"); + event.data.applyFilters({ searchletter: event.target.id || false }); + }); + // Set activeFilters to current value + this.nextmatch.activeFilters.searchletter = current_letter; + } + // Apply letter search preference + var lettersearch_preference = "nextmatch-" + this.nextmatch.options.settings.columnselection_pref + "-lettersearch"; + if (this.lettersearch && !egw.preference(lettersearch_preference, this.nextmatch.egw().app_name())) { + this.lettersearch.hide(); + } + }; + /** + * Build & bind to a sub-template into the header + * + * @param {string} location One of left, right, or row + * @param {string} template_name Name of the template to load into the location + */ + et2_nextmatch_header_bar.prototype._build_header = function (location, template_name) { + var id = location == "left" ? 0 : (location == "right" ? 1 : 2); + var existing = this.headers[id]; + // @ts-ignore + if (existing && existing._type) { + if (existing.id == template_name) + return; + existing.destroy(); + this.headers[id] = null; + } + if (!template_name) + return; + // Load the template + var self = this; + var header = et2_createWidget("template", { "id": template_name }, this); + this.headers[id] = header; + var deferred = []; + header.loadingFinished(deferred); + // Wait until all child widgets are loaded, then bind + jQuery.when.apply(jQuery, deferred).then(function () { + // fix order in DOM by reattaching templates in correct position + switch (id) { + case 0: // header_left: prepend + jQuery(header.getDOMNode()).prependTo(self.header_div); + break; + case 1: // header_right: before favorites and count + jQuery(header.getDOMNode()).prependTo(self.header_div.find('div.header_row_right')); + break; + case 2: // header_row: after search + window.setTimeout(function () { + jQuery(header.getDOMNode()).insertAfter(self.header_div.find('div.search')); + }, 1); + break; + } + self._bindHeaderInput(header); + }); + }; + /** + * Build the selectbox filters in the header bar + * Sets value, options, labels, and change handlers + * + * @param {string} name + * @param {string} type + * @param {string} value + * @param {string} lang + * @param {object} extra + */ + et2_nextmatch_header_bar.prototype._build_select = function (name, type, value, lang, extra) { + var _a; + var widget_options = jQuery.extend({ + "id": name, + "label": this.nextmatch.options.settings[name + "_label"], + "no_lang": lang, + "disabled": this.nextmatch.options['no_' + name] + }, extra); + // Set select options + // Check in content for options- + var mgr = this.nextmatch.getArrayMgr("content"); + var options = mgr.getEntry("options-" + name); + // Look in sel_options + if (!options) + options = this.nextmatch.getArrayMgr("sel_options").getEntry(name); + // Check parent sel_options, because those are usually global and don't get passed down + if (!options) + options = (_a = this.nextmatch.getArrayMgr("sel_options").getParentMgr()) === null || _a === void 0 ? void 0 : _a.getEntry(name); + // Sometimes legacy stuff puts it in here + if (!options) + options = mgr.getEntry('rows[sel_options][' + name + ']'); + // Maybe in a row, and options got stuck in ${row} instead of top level + var row_stuck = ['${row}', '{$row}']; + for (var i = 0; !options && i < row_stuck.length; i++) { + var row_id = ''; + if ((!options || options.length == 0) && ( + // perspectiveData.row in nm, data["${row}"] in an auto-repeat grid + this.nextmatch.getArrayMgr("sel_options").perspectiveData.row || this.nextmatch.getArrayMgr("sel_options").data[row_stuck[i]])) { + row_id = name.replace(/[0-9]+/, row_stuck[i]); + options = this.nextmatch.getArrayMgr("sel_options").getEntry(row_id); + if (!options) { + row_id = row_stuck[i] + "[" + name + "]"; + options = this.nextmatch.getArrayMgr("sel_options").getEntry(row_id); + } + } + if (options) { + this.egw().debug('warn', 'Nextmatch filter options in a weird place - "%s". Should be in sel_options[%s].', row_id, name); + } + } + // Legacy: Add in 'All' option for cat_id, if not provided. + if (name == 'cat_id' && options != null && (typeof options[''] == 'undefined' && typeof options[0] != 'undefined' && options[0].value != '')) { + widget_options.empty_label = this.egw().lang('All categories'); + } + // Create widget + var select = et2_createWidget(type, widget_options, this); + if (options) + select.set_select_options(options); + // Set value + select.set_value(value); + // Set activeFilters to current value + this.nextmatch.activeFilters[select.id] = select.get_value(); + // Set onChange + var input = select.input; + // Tell framework to ignore, or it will reset it to ''/empty when it does loadingFinished() + select.attributes.select_options.ignore = true; + if (this.nextmatch.options.settings[name + "_onchange"]) { + // Get the onchange function string + var onchange_1 = this.nextmatch.options.settings[name + "_onchange"]; + // Real submits cause all sorts of problems + if (onchange_1.match(/this\.form\.submit/)) { + this.egw().debug("warn", "%s tries to submit form, which is not allowed. Filter changes automatically refresh data with no reload.", name); + onchange_1 = onchange_1.replace(/this\.form\.submit\([^)]*\);?/, 'return true;'); + } + // Connect it to the onchange event of the input element - may submit + select.change = et2_compileLegacyJS(onchange_1, this.nextmatch, select.getInputNode()); + this._bindHeaderInput(select); + } + else // default request changed rows with new filters, previous this.form.submit() + { + input.change(this.nextmatch, function (event) { + var set = {}; + set[name] = select.getValue(); + event.data.applyFilters(set); + }); + } + return select; + }; + /** + * Set up the favorites UI control + * + * @param filters Array|boolean The nextmatch setting for favorites. Either true, or a list of + * additional fields/settings to add in to the favorite. + */ + et2_nextmatch_header_bar.prototype._setup_favorites = function (filters) { + if (typeof filters == "undefined" || filters === false) { + // No favorites configured + return; + } + var list = et2_csvSplit(this.options.get_rows, 2, "."); + var widget_options = { + default_pref: "nextmatch-" + this.nextmatch.options.settings.columnselection_pref + "-favorite", + app: list[0], + filters: filters, + sidebox_target: 'favorite_sidebox_' + list[0] + }; + this.favorites = et2_createWidget('favorites', widget_options, this); + // Add into header + jQuery(this.favorites.getDOMNode(this.favorites)).prependTo(egwIsMobile() ? this.search_box.find('.nm_favorites_div').show() : this.right_div); + }; + /** + * Updates all the filter elements in the header + * + * Does not actually refresh the data, just sets values to match those given. + * Called by et2_nextmatch.applyFilters(). + * + * @param filters Array Key => Value pairs of current filters + */ + et2_nextmatch_header_bar.prototype.setFilters = function (filters) { + // Avoid loops cause by change events + if (this.update_in_progress) + return; + this.update_in_progress = true; + // Use an array mgr to hande non-simple IDs + var mgr = new et2_arrayMgr(filters); + this.iterateOver(function (child) { + // Skip favorites, don't want them in the filter + if (typeof child.id != "undefined" && child.id.indexOf("favorite") == 0) + return; + var value = ''; + if (typeof child.set_value != "undefined" && child.id) { + value = mgr.getEntry(child.id); + if (value == null) + value = ''; + /** + * Sometimes a filter value is not in current options. This can + * happen in a saved favorite, for example, or if server changes + * some filter options, and the order doesn't work out. The normal behaviour + * is to warn & not set it, but for nextmatch we'll just add it + * in, and let the server either set it properly, or ignore. + */ + if (value && typeof value != 'object' && child.instanceOf(et2_widget_selectbox_1.et2_selectbox)) { + var found = typeof child.options.select_options[value] != 'undefined'; + // options is array of objects with attribute value&label + if (jQuery.isArray(child.options.select_options)) { + for (var o = 0; o < child.options.select_options.length; ++o) { + if (child.options.select_options[o].value == value) { + found = true; + break; + } + } + } + if (!found) { + var old_options = child.options.select_options; + // Actual label is not available, obviously, or it would be there + old_options[value] = child.egw().lang("Loading"); + child.set_select_options(old_options); + } + } + child.set_value(value); + } + if (typeof child.get_value == "function" && child.id) { + // Put data in the proper place + var target = this; + value = child.get_value(); + // Split up indexes + var indexes = child.id.replace(/[/g, '[').split('['); + for (var i = 0; i < indexes.length; i++) { + indexes[i] = indexes[i].replace(/]/g, '').replace(']', ''); + if (i < indexes.length - 1) { + if (typeof target[indexes[i]] == "undefined") + target[indexes[i]] = {}; + target = target[indexes[i]]; + } + else { + target[indexes[i]] = value; + } + } + } + }, filters); + // Letter search + if (this.nextmatch.options.settings.lettersearch) { + jQuery("td", this.lettersearch).removeClass("lettersearch_active"); + jQuery(filters.searchletter ? "td#" + filters.searchletter : "td.lettersearch[id='']", this.lettersearch).addClass("lettersearch_active"); + // Set activeFilters to current value + filters.searchletter = jQuery("td.lettersearch_active", this.lettersearch).attr("id") || false; + } + // Reset flag + this.update_in_progress = false; + }; + /** + * Help out nextmatch / widget stuff by checking to see if sender is part of header + * + * @param {et2_widget} _sender + */ + et2_nextmatch_header_bar.prototype.getDOMNode = function (_sender) { + var filters = [this.category, this.filter, this.filter2]; + for (var i = 0; i < filters.length; i++) { + if (_sender == filters[i]) { + // Give them the filter div + return this.filter_div[0]; + } + } + if (_sender == this.et2_searchbox) + return this.search_box[0]; + if (_sender.id == 'export') + return this.right_div[0]; + if (_sender && _sender._type == "template") { + for (var i = 0; i < this.headers.length; i++) { + if (_sender.id == this.headers[i].id && _sender._parent == this) + return i == 2 ? this.header_row[0] : this.header_div[0]; + } + } + return null; + }; + /** + * Bind all the inputs in the header sub-templates to update the filters + * on change, and update current filter with the inputs' current values + * + * @param {et2_template} sub_header + */ + et2_nextmatch_header_bar.prototype._bindHeaderInput = function (sub_header) { + var header = this; + var bind_change = function (_widget) { + // Previously set change function + var widget_change = _widget.change; + var change = function (_node) { + // Call previously set change function + var result = widget_change.call(_widget, _node); + // Update filters, if we're not already doing so + if ((result || typeof result === 'undefined') && _widget.isDirty() && !header.update_in_progress) { + // Update dirty + _widget._oldValue = _widget.getValue(); + // Widget will not have an entry in getValues() because nulls + // are not returned, we remove it from activeFilters + if (_widget._oldValue == null) { + var path = _widget.getArrayMgr('content').explodeKey(_widget.id); + if (path.length > 0) { + var entry = header.nextmatch.activeFilters; + var i = 0; + for (; i < path.length - 1; i++) { + entry = entry[path[i]]; + } + delete entry[path[i]]; + } + header.nextmatch.applyFilters(header.nextmatch.activeFilters); + } + else { + // Not null is easy, just get values + var value_1 = this.getInstanceManager().getValues(sub_header); + header.nextmatch.applyFilters(value_1[header.nextmatch.id]); + } + } + // In case this gets bound twice, it's important to return + return true; + }; + _widget.change = change; + // Set activeFilters to current value + // Use an array mgr to hande non-simple IDs + var value = {}; + value[_widget.id] = _widget._oldValue = _widget.getValue(); + var mgr = new et2_arrayMgr(value); + jQuery.extend(true, this.nextmatch.activeFilters, mgr.data); + }; + if (sub_header.instanceOf(et2_core_inputWidget_1.et2_inputWidget)) { + bind_change.call(this, sub_header); + } + else { + sub_header.iterateOver(bind_change, this, et2_core_inputWidget_1.et2_inputWidget); + } + }; + et2_nextmatch_header_bar._attributes = { + "filter_label": { + "name": "Filter label", + "type": "string", + "description": "Label for filter", + "default": "", + "translate": true + }, + "filter_help": { + "name": "Filter help", + "type": "string", + "description": "Help message for filter", + "default": "", + "translate": true + }, + "filter": { + "name": "Filter value", + "type": "any", + "description": "Current value for filter", + "default": "" + }, + "no_filter": { + "name": "No filter", + "type": "boolean", + "description": "Remove filter", + "default": false + } + }; + return et2_nextmatch_header_bar; +}(et2_core_DOMWidget_1.et2_DOMWidget)); +et2_core_widget_1.et2_register_widget(et2_nextmatch_header_bar, ["nextmatch_header_bar"]); /** * Classes for the nextmatch sortheaders etc. * * @augments et2_baseWidget */ -var et2_nextmatch_header = (function(){ "use strict"; return et2_baseWidget.extend(et2_INextmatchHeader, -{ - attributes: { - "label": { - "name": "Caption", - "type": "string", - "description": "Caption for the nextmatch header", - "translate": true - } - }, - - /** - * Constructor - * - * @memberOf et2_nextmatch_header - */ - init: function() { - this._super.apply(this, arguments); - - this.labelNode = jQuery(document.createElement("span")); - this.nextmatch = null; - - this.setDOMNode(this.labelNode[0]); - }, - - destroy: function() { - this._super.apply(this, arguments); - }, - - /** - * Set nextmatch is the function which has to be implemented for the - * et2_INextmatchHeader interface. - * - * @param {et2_nextmatch} _nextmatch - */ - setNextmatch: function(_nextmatch) { - this.nextmatch = _nextmatch; - }, - - set_label: function(_value) { - this.label = _value; - - this.labelNode.text(_value); - - // add class if label is empty - this.labelNode.toggleClass('et2_label_empty', !_value); - } -});}).call(this); -et2_register_widget(et2_nextmatch_header, ['nextmatch-header']); - +var et2_nextmatch_header = /** @class */ (function (_super) { + __extends(et2_nextmatch_header, _super); + /** + * Constructor + * + * @memberOf et2_nextmatch_header + */ + function et2_nextmatch_header(_parent, _attrs, _child) { + var _this = _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_nextmatch_header._attributes, _child || {})) || this; + _this.labelNode = jQuery(document.createElement("span")); + _this.nextmatch = null; + _this.setDOMNode(_this.labelNode[0]); + return _this; + } + /** + * Set nextmatch is the function which has to be implemented for the + * et2_INextmatchHeader interface. + * + * @param {et2_nextmatch} _nextmatch + */ + et2_nextmatch_header.prototype.setNextmatch = function (_nextmatch) { + this.nextmatch = _nextmatch; + }; + et2_nextmatch_header.prototype.set_label = function (_value) { + this.label = _value; + this.labelNode.text(_value); + // add class if label is empty + this.labelNode.toggleClass('et2_label_empty', !_value); + }; + et2_nextmatch_header._attributes = { + "label": { + "name": "Caption", + "type": "string", + "description": "Caption for the nextmatch header", + "translate": true + } + }; + return et2_nextmatch_header; +}(et2_core_baseWidget_1.et2_baseWidget)); +exports.et2_nextmatch_header = et2_nextmatch_header; +et2_core_widget_1.et2_register_widget(et2_nextmatch_header, ['nextmatch-header']); /** * Extend header to process customfields * * @augments et2_customfields_list + * + * TODO This should extend customfield widget when it's ready, put the whole column in constructor() back too */ -var et2_nextmatch_customfields = (function(){ "use strict"; return et2_customfields_list.extend(et2_INextmatchHeader, -{ - attributes: { - 'customfields': { - 'name': 'Custom fields', - 'description': 'Auto filled' - }, - 'fields': { - 'name': "Visible fields", - "description": "Auto filled" - } - }, - - /** - * Constructor - * - * @memberOf et2_nextmatch_customfields - */ - init: function() { - this.nextmatch = null; - this._super.apply(this, arguments); - - // Specifically take the whole column - this.table.css("width", "100%"); - }, - - destroy: function() { - this.nextmatch = null; - this._super.apply(this, arguments); - }, - - transformAttributes: function(_attrs) { - this._super.apply(this, arguments); - - // Add in settings that are objects - if(!_attrs.customfields) - { - // Check for custom stuff (unlikely) - var data = this.getArrayMgr("modifications").getEntry(this.id); - // Check for global settings - if(!data) data = this.getArrayMgr("modifications").getRoot().getEntry('~custom_fields~', true); - for(var key in data) - { - if(typeof data[key] === 'object' && ! _attrs[key]) _attrs[key] = data[key]; - } - } - }, - - setNextmatch: function(_nextmatch) { - this.nextmatch = _nextmatch; - this.loadFields(); - }, - - /** - * Build widgets for header - sortable for numeric, text, etc., filterables for selectbox, radio - */ - loadFields: function() { - if(this.nextmatch == null) - { - // not ready yet - return; - } - var columnMgr = this.nextmatch.dataview.getColumnMgr(); - var nm_column = null; - var set_fields = {}; - for(var i = 0; i < this.nextmatch.columns.length; i++) - { - if(this.nextmatch.columns[i].widget == this) - { - nm_column = columnMgr.columns[i]; - break; - } - } - if(!nm_column) return; - - // Check for global setting changes (visibility) - var global_data = this.getArrayMgr("modifications").getRoot().getEntry('~custom_fields~'); - if(global_data != null && global_data.fields) this.options.fields = global_data.fields; - - var apps = egw.link_app_list(); - for(var field_name in this.options.customfields) - { - var field = this.options.customfields[field_name]; - var cf_id = et2_customfields_list.prototype.prefix + field_name; - - - if(this.rows[field_name]) continue; - - // Table row - var row = jQuery(document.createElement("tr")) - .appendTo(this.tbody); - var cf = jQuery(document.createElement("td")) - .appendTo(row); - this.rows[cf_id] = cf[0]; - - // Create widget by type - var widget = null; - if(field.type == 'select' || field.type == 'select-account') - { - if(field.values && typeof field.values[''] !== 'undefined') - { - delete(field.values['']); - } - widget = et2_createWidget( - field.type == 'select-account' ? 'nextmatch-accountfilter' : "nextmatch-filterheader", - { - id: cf_id, - empty_label: field.label, - select_options: field.values - }, - this - ); - } - else if (apps[field.type]) - { - widget = et2_createWidget("nextmatch-entryheader", { - id: cf_id, - only_app: field.type, - blur: field.label - }, this); - } - else - { - widget = et2_createWidget("nextmatch-sortheader", { - id: cf_id, - label: field.label - }, this); - } - - // If this is already attached, widget needs to be finished explicitly - if(this.isAttached() && !widget.isAttached()) - { - widget.loadingFinished(); - } - // Check for column filter - if(!jQuery.isEmptyObject(this.options.fields) && ( - this.options.fields[field_name] == false || typeof this.options.fields[field_name] == 'undefined')) - { - cf.hide(); - } - else if (jQuery.isEmptyObject(this.options.fields)) - { - // If we're showing it make sure it's set, but only after - set_fields[field_name] = true; - } - } - jQuery.extend(this.options.fields, set_fields); - }, - - /** - * Override parent so we can update the nextmatch row too - * - * @param {array} _fields - */ - set_visible: function(_fields) { - this._super.apply(this, arguments); - - // Find data row, and do it too - var self = this; - if(this.nextmatch) - { - this.nextmatch.iterateOver( - function(widget) { - if(widget == self) return; - widget.set_visible(_fields); - }, this, et2_customfields_list - ); - } - }, - - /** - * Provide own column caption (column selection) - * - * If only one custom field, just use that, otherwise use "custom fields" - */ - _genColumnCaption: function() { - return egw.lang("Custom fields"); - }, - - /** - * Provide own column naming, including only selected columns - only useful - * to nextmatch itself, not for sending server-side - */ - _getColumnName: function() { - var name = this.id; - var visible = []; - for(var field_name in this.options.customfields) - { - if(jQuery.isEmptyObject(this.options.fields) || this.options.fields[field_name] == true) - { - visible.push(et2_customfields_list.prototype.prefix + field_name); - jQuery(this.rows[field_name]).show(); - } - else if (typeof this.rows[field_name] != "undefined") - { - jQuery(this.rows[field_name]).hide(); - } - } - - if(visible.length) { - name +="_"+ visible.join("_"); - } - else - { - // None hidden means all visible - jQuery(this.rows[field_name]).parent().parent().children().show(); - } - - // Update global custom fields column(s) - widgets will check on their own - - // Check for custom stuff (unlikely) - var data = this.getArrayMgr("modifications").getEntry(this.id); - // Check for global settings - if(!data) data = this.getArrayMgr("modifications").getRoot().getEntry('~custom_fields~', true) || {}; - if(!data.fields) data.fields = {}; - for(var field in this.options.customfields) - { - data.fields[field] = (this.options.fields == null || typeof this.options.fields[field] == 'undefined' ? false : this.options.fields[field]); - } - return name; - } -});}).call(this); -et2_register_widget(et2_nextmatch_customfields, ['nextmatch-customfields']); - +var et2_nextmatch_customfields = /** @class */ (function (_super) { + __extends(et2_nextmatch_customfields, _super); + /** + * Constructor + * + * @memberOf et2_nextmatch_customfields + */ + function et2_nextmatch_customfields(_parent, _attrs, _child) { + var _this = _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_nextmatch_customfields._attributes, _child || {})) || this; + // Specifically take the whole column + _this.table.css("width", "100%"); + return _this; + } + et2_nextmatch_customfields.prototype.destroy = function () { + this.nextmatch = null; + _super.prototype.destroy.call(this); + }; + et2_nextmatch_customfields.prototype.transformAttributes = function (_attrs) { + _super.prototype.transformAttributes.call(this, _attrs); + // Add in settings that are objects + if (!_attrs.customfields) { + // Check for custom stuff (unlikely) + var data = this.getArrayMgr("modifications").getEntry(this.id); + // Check for global settings + if (!data) + data = this.getArrayMgr("modifications").getRoot().getEntry('~custom_fields~', true); + for (var key in data) { + if (typeof data[key] === 'object' && !_attrs[key]) + _attrs[key] = data[key]; + } + } + }; + et2_nextmatch_customfields.prototype.setNextmatch = function (_nextmatch) { + this.nextmatch = _nextmatch; + this.loadFields(); + }; + /** + * Build widgets for header - sortable for numeric, text, etc., filterables for selectbox, radio + */ + et2_nextmatch_customfields.prototype.loadFields = function () { + if (this.nextmatch == null) { + // not ready yet + return; + } + var columnMgr = this.nextmatch.dataview.getColumnMgr(); + var nm_column = null; + var set_fields = {}; + for (var i = 0; i < this.nextmatch.columns.length; i++) { + // @ts-ignore + if (this.nextmatch.columns[i].widget == this) { + nm_column = columnMgr.columns[i]; + break; + } + } + if (!nm_column) + return; + // Check for global setting changes (visibility) + var global_data = this.getArrayMgr("modifications").getRoot().getEntry('~custom_fields~'); + if (global_data != null && global_data.fields) + this.options.fields = global_data.fields; + var apps = egw.link_app_list(); + for (var field_name in this.options.customfields) { + var field = this.options.customfields[field_name]; + var cf_id = et2_extension_customfields_1.et2_customfields_list.PREFIX + field_name; + if (this.rows[field_name]) + continue; + // Table row + var row = jQuery(document.createElement("tr")) + .appendTo(this.tbody); + var cf = jQuery(document.createElement("td")) + .appendTo(row); + this.rows[cf_id] = cf[0]; + // Create widget by type + var widget = null; + if (field.type == 'select' || field.type == 'select-account') { + if (field.values && typeof field.values[''] !== 'undefined') { + delete (field.values['']); + } + widget = et2_createWidget(field.type == 'select-account' ? 'nextmatch-accountfilter' : "nextmatch-filterheader", { + id: cf_id, + empty_label: field.label, + select_options: field.values + }, this); + } + else if (apps[field.type]) { + widget = et2_createWidget("nextmatch-entryheader", { + id: cf_id, + only_app: field.type, + blur: field.label + }, this); + } + else { + widget = et2_createWidget("nextmatch-sortheader", { + id: cf_id, + label: field.label + }, this); + } + // If this is already attached, widget needs to be finished explicitly + if (this.isAttached() && !widget.isAttached()) { + widget.loadingFinished(); + } + // Check for column filter + if (!jQuery.isEmptyObject(this.options.fields) && (this.options.fields[field_name] == false || typeof this.options.fields[field_name] == 'undefined')) { + cf.hide(); + } + else if (jQuery.isEmptyObject(this.options.fields)) { + // If we're showing it make sure it's set, but only after + set_fields[field_name] = true; + } + } + jQuery.extend(this.options.fields, set_fields); + }; + /** + * Override parent so we can update the nextmatch row too + * + * @param {array} _fields + */ + et2_nextmatch_customfields.prototype.set_visible = function (_fields) { + _super.prototype.set_visible.call(this, _fields); + // Find data row, and do it too + var self = this; + if (this.nextmatch) { + this.nextmatch.iterateOver(function (widget) { + if (widget == self) + return; + widget.set_visible(_fields); + }, this, et2_extension_customfields_1.et2_customfields_list); + } + }; + /** + * Provide own column caption (column selection) + * + * If only one custom field, just use that, otherwise use "custom fields" + */ + et2_nextmatch_customfields.prototype._genColumnCaption = function () { + return egw.lang("Custom fields"); + }; + /** + * Provide own column naming, including only selected columns - only useful + * to nextmatch itself, not for sending server-side + */ + et2_nextmatch_customfields.prototype._getColumnName = function () { + var name = this.id; + var visible = []; + for (var field_name in this.options.customfields) { + if (jQuery.isEmptyObject(this.options.fields) || this.options.fields[field_name] == true) { + visible.push(et2_extension_customfields_1.et2_customfields_list.PREFIX + field_name); + jQuery(this.rows[field_name]).show(); + } + else if (typeof this.rows[field_name] != "undefined") { + jQuery(this.rows[field_name]).hide(); + } + } + if (visible.length) { + name += "_" + visible.join("_"); + } + else if (this.rows) { + // None hidden means all visible + jQuery(this.rows[field_name]).parent().parent().children().show(); + } + // Update global custom fields column(s) - widgets will check on their own + // Check for custom stuff (unlikely) + var data = this.getArrayMgr("modifications").getEntry(this.id); + // Check for global settings + if (!data) + data = this.getArrayMgr("modifications").getRoot().getEntry('~custom_fields~', true) || {}; + if (!data.fields) + data.fields = {}; + for (var field in this.options.customfields) { + data.fields[field] = (this.options.fields == null || typeof this.options.fields[field] == 'undefined' ? false : this.options.fields[field]); + } + return name; + }; + et2_nextmatch_customfields._attributes = { + 'customfields': { + 'name': 'Custom fields', + 'description': 'Auto filled' + }, + 'fields': { + 'name': "Visible fields", + "description": "Auto filled" + } + }; + return et2_nextmatch_customfields; +}(et2_extension_customfields_1.et2_customfields_list)); +exports.et2_nextmatch_customfields = et2_nextmatch_customfields; +et2_core_widget_1.et2_register_widget(et2_nextmatch_customfields, ['nextmatch-customfields']); /** * @augments et2_nextmatch_header */ -var et2_nextmatch_sortheader = (function(){ "use strict"; return et2_nextmatch_header.extend(et2_INextmatchSortable, -{ - attributes: { - "sortmode": { - "name": "Sort order", - "type": "string", - "description": "Default sort order", - "translate": false - } - }, - legacyOptions: ['sortmode'], - - /** - * Constructor - * - * @memberOf et2_nextmatch_sortheader - */ - init: function() { - this._super.apply(this, arguments); - - this.sortmode = "none"; - - this.labelNode.addClass("nextmatch_sortheader none"); - }, - - click: function() { - if (this.nextmatch && this._super.apply(this, arguments)) - { - // Send default sort mode if not sorted, otherwise send undefined to calculate - this.nextmatch.sortBy(this.id, this.sortmode == "none" ? !(this.options.sortmode.toUpperCase() == "DESC") : undefined); - return true; - } - - return false; - }, - - /** - * Wrapper to join up interface * framework - * - * @param {string} _mode - */ - set_sortmode: function(_mode) - { - // Set via nextmatch after setup - if(this.nextmatch) return; - - this.setSortmode(_mode); - }, - - /** - * Function which implements the et2_INextmatchSortable function. - * - * @param {string} _mode - */ - setSortmode: function(_mode) { - // Remove the last sortmode class and add the new one - this.labelNode.removeClass(this.sortmode) - .addClass(_mode); - - this.sortmode = _mode; - } - -});}).call(this); -et2_register_widget(et2_nextmatch_sortheader, ['nextmatch-sortheader']); - +// @ts-ignore +var et2_nextmatch_sortheader = /** @class */ (function (_super) { + __extends(et2_nextmatch_sortheader, _super); + /** + * Constructor + * + * @memberOf et2_nextmatch_sortheader + */ + function et2_nextmatch_sortheader(_parent, _attrs, _child) { + var _this = _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_nextmatch_sortheader._attributes, _child || {})) || this; + _this.sortmode = "none"; + _this.labelNode.addClass("nextmatch_sortheader none"); + return _this; + } + et2_nextmatch_sortheader.prototype.click = function (_event) { + if (this.nextmatch && _super.prototype.click.call(this, _event)) { + // Send default sort mode if not sorted, otherwise send undefined to calculate + this.nextmatch.sortBy(this.id, this.sortmode == "none" ? !(this.options.sortmode.toUpperCase() == "DESC") : undefined); + return true; + } + return false; + }; + /** + * Wrapper to join up interface * framework + * + * @param {string} _mode + */ + et2_nextmatch_sortheader.prototype.set_sortmode = function (_mode) { + // Set via nextmatch after setup + if (this.nextmatch) + return; + this.setSortmode(_mode); + }; + /** + * Function which implements the et2_INextmatchSortable function. + * + * @param {string} _mode + */ + et2_nextmatch_sortheader.prototype.setSortmode = function (_mode) { + // Remove the last sortmode class and add the new one + this.labelNode.removeClass(this.sortmode) + .addClass(_mode); + this.sortmode = _mode; + }; + et2_nextmatch_sortheader._attributes = { + "sortmode": { + "name": "Sort order", + "type": "string", + "description": "Default sort order", + "translate": false + } + }; + return et2_nextmatch_sortheader; +}(et2_nextmatch_header)); +exports.et2_nextmatch_sortheader = et2_nextmatch_sortheader; +et2_core_widget_1.et2_register_widget(et2_nextmatch_sortheader, ['nextmatch-sortheader']); /** - * @augments et2_selectbox + * Filter from a provided list of options */ -var et2_nextmatch_filterheader = (function(){ "use strict"; return et2_selectbox.extend([et2_INextmatchHeader, et2_IResizeable], -{ - /** - * Override to add change handler - * - * @memberOf et2_nextmatch_filterheader - */ - createInputWidget: function() { - // Make sure there's an option for all - if(!this.options.empty_label && (!this.options.select_options || !this.options.select_options[""])) - { - this.options.empty_label = this.options.label ? this.options.label : egw.lang("All"); - } - this._super.apply(this, arguments); - - this.input.change(this, function(event) { - if(typeof event.data.nextmatch == 'undefined') - { - // Not fully set up yet - return; - } - var col_filter = {}; - col_filter[event.data.id] = event.data.input.val(); - // Set value so it's there for response (otherwise it gets cleared if options are updated) - event.data.set_value(event.data.input.val()); - - event.data.nextmatch.applyFilters({col_filter: col_filter}); - }); - - }, - - /** - * Set nextmatch is the function which has to be implemented for the - * et2_INextmatchHeader interface. - * - * @param {et2_nextmatch} _nextmatch - */ - setNextmatch: function(_nextmatch) { - this.nextmatch = _nextmatch; - - // Set current filter value from nextmatch settings - if(this.nextmatch.activeFilters.col_filter && typeof this.nextmatch.activeFilters.col_filter[this.id] != "undefined") - { - this.set_value(this.nextmatch.activeFilters.col_filter[this.id]); - - // Make sure it's set in the nextmatch - _nextmatch.activeFilters.col_filter[this.id] = this.getValue(); - } - }, - - // Make sure selectbox is not longer than the column - resize: function() { - this.input.css("max-width",jQuery(this.parentNode).innerWidth() + "px"); - } - -});}).call(this); -et2_register_widget(et2_nextmatch_filterheader, ['nextmatch-filterheader']); - +var et2_nextmatch_filterheader = /** @class */ (function (_super) { + __extends(et2_nextmatch_filterheader, _super); + function et2_nextmatch_filterheader() { + return _super !== null && _super.apply(this, arguments) || this; + } + /** + * Override to add change handler + */ + et2_nextmatch_filterheader.prototype.createInputWidget = function () { + // Make sure there's an option for all + if (!this.options.empty_label && (!this.options.select_options || !this.options.select_options[""])) { + this.options.empty_label = this.options.label ? this.options.label : egw.lang("All"); + } + _super.prototype.createInputWidget.call(this); + jQuery(this.getInputNode()).change(this, function (event) { + if (typeof event.data.nextmatch == 'undefined') { + // Not fully set up yet + return; + } + var col_filter = {}; + col_filter[event.data.id] = event.data.input.val(); + // Set value so it's there for response (otherwise it gets cleared if options are updated) + event.data.set_value(event.data.input.val()); + event.data.nextmatch.applyFilters({ col_filter: col_filter }); + }); + }; + /** + * Set nextmatch is the function which has to be implemented for the + * et2_INextmatchHeader interface. + * + * @param {et2_nextmatch} _nextmatch + */ + et2_nextmatch_filterheader.prototype.setNextmatch = function (_nextmatch) { + this.nextmatch = _nextmatch; + // Set current filter value from nextmatch settings + if (this.nextmatch.activeFilters.col_filter && typeof this.nextmatch.activeFilters.col_filter[this.id] != "undefined") { + this.set_value(this.nextmatch.activeFilters.col_filter[this.id]); + // Make sure it's set in the nextmatch + _nextmatch.activeFilters.col_filter[this.id] = this.getValue(); + } + }; + // Make sure selectbox is not longer than the column + et2_nextmatch_filterheader.prototype.resize = function () { + this.input.css("max-width", jQuery(this.parentNode).innerWidth() + "px"); + }; + return et2_nextmatch_filterheader; +}(et2_widget_selectbox_1.et2_selectbox)); +exports.et2_nextmatch_filterheader = et2_nextmatch_filterheader; +et2_core_widget_1.et2_register_widget(et2_nextmatch_filterheader, ['nextmatch-filterheader']); /** - * @augments et2_selectAccount + * Filter by account */ -var et2_nextmatch_accountfilterheader = (function(){ "use strict"; return et2_selectAccount.extend([et2_INextmatchHeader, et2_IResizeable], -{ - /** - * Override to add change handler - * - * @memberOf et2_nextmatch_accountfilterheader - */ - createInputWidget: function() { - // Make sure there's an option for all - if(!this.options.empty_label && !this.options.select_options[""]) - { - this.options.empty_label = this.options.label ? this.options.label : egw.lang("All"); - } - this._super.apply(this, arguments); - - this.input.change(this, function(event) { - if(typeof event.data.nextmatch == 'undefined') - { - // Not fully set up yet - return; - } - var col_filter = {}; - col_filter[event.data.id] = event.data.getValue(); - event.data.nextmatch.applyFilters({col_filter: col_filter}); - }); - - }, - - /** - * Set nextmatch is the function which has to be implemented for the - * et2_INextmatchHeader interface. - * - * @param {et2_nextmatch} _nextmatch - */ - setNextmatch: function(_nextmatch) { - this.nextmatch = _nextmatch; - - // Set current filter value from nextmatch settings - if(this.nextmatch.activeFilters.col_filter && this.nextmatch.activeFilters.col_filter[this.id]) - { - this.set_value(this.nextmatch.activeFilters.col_filter[this.id]); - } - }, - // Make sure selectbox is not longer than the column - resize: function() { - var max = jQuery(this.parentNode).innerWidth() - 4; - var surroundings = this.getSurroundings()._widgetSurroundings; - for(var i = 0; i < surroundings.length; i++) - { - max -= jQuery(surroundings[i]).outerWidth(); - } - this.input.css("max-width",max + "px"); - } - -});}).call(this); -et2_register_widget(et2_nextmatch_accountfilterheader, ['nextmatch-accountfilter']); - +var et2_nextmatch_accountfilterheader = /** @class */ (function (_super) { + __extends(et2_nextmatch_accountfilterheader, _super); + function et2_nextmatch_accountfilterheader() { + return _super !== null && _super.apply(this, arguments) || this; + } + /** + * Override to add change handler + * + */ + et2_nextmatch_accountfilterheader.prototype.createInputWidget = function () { + // Make sure there's an option for all + if (!this.options.empty_label && !this.options.select_options[""]) { + this.options.empty_label = this.options.label ? this.options.label : egw.lang("All"); + } + _super.prototype.createInputWidget.call(this, this, arguments); + this.input.change(this, function (event) { + if (typeof event.data.nextmatch == 'undefined') { + // Not fully set up yet + return; + } + var col_filter = {}; + col_filter[event.data.id] = event.data.getValue(); + event.data.nextmatch.applyFilters({ col_filter: col_filter }); + }); + }; + /** + * Set nextmatch is the function which has to be implemented for the + * et2_INextmatchHeader interface. + * + * @param {et2_nextmatch} _nextmatch + */ + et2_nextmatch_accountfilterheader.prototype.setNextmatch = function (_nextmatch) { + this.nextmatch = _nextmatch; + // Set current filter value from nextmatch settings + if (this.nextmatch.activeFilters.col_filter && this.nextmatch.activeFilters.col_filter[this.id]) { + this.set_value(this.nextmatch.activeFilters.col_filter[this.id]); + } + }; + // Make sure selectbox is not longer than the column + et2_nextmatch_accountfilterheader.prototype.resize = function () { + var max = jQuery(this.parentNode).innerWidth() - 4; + var surroundings = this.getSurroundings()._widgetSurroundings; + for (var i = 0; i < surroundings.length; i++) { + max -= jQuery(surroundings[i]).outerWidth(); + } + this.input.css("max-width", max + "px"); + }; + return et2_nextmatch_accountfilterheader; +}(et2_selectAccount)); +exports.et2_nextmatch_accountfilterheader = et2_nextmatch_accountfilterheader; +et2_core_widget_1.et2_register_widget(et2_nextmatch_accountfilterheader, ['nextmatch-accountfilter']); /** * Filter allowing multiple values to be selected, base on a taglist instead * of a regular selectbox * * @augments et2_taglist */ -var et2_nextmatch_taglistheader = (function(){ "use strict"; return et2_taglist.extend([et2_INextmatchHeader, et2_IResizeable], -{ - attributes: { - autocomplete_url: { default: ''}, - multiple: { default: 'toggle'}, - onchange: { - default: function(event) { - if(typeof this.nextmatch === 'undefined') - { - // Not fully set up yet - return; - } - var col_filter = {}; - col_filter[this.id] = this.getValue(); - // Set value so it's there for response (otherwise it gets cleared if options are updated) - //event.data.set_value(event.data.input.val()); - - this.nextmatch.applyFilters({col_filter: col_filter}); - } - }, - rows: { default: 2}, - class: {default: 'nm_filterheader_taglist'} - }, - - /** - * Override to add change handler - * - * @memberOf et2_nextmatch_filterheader - */ - createInputWidget: function() { - // Make sure there's an option for all - if(!this.options.empty_label && (!this.options.select_options || !this.options.select_options[""])) - { - this.options.empty_label = this.options.label ? this.options.label : egw.lang("All"); - } - this._super.apply(this, arguments); - }, - - /** - * Disable toggle if there are 2 or less options - * @param {Object[]} options - */ - set_select_options: function(options) - { - if(options && options.length <= 2 && this.options.multiple == 'toggle') - { - this.set_multiple(false); - } - this._super.apply(this, arguments); - }, - - /** - * Set nextmatch is the function which has to be implemented for the - * et2_INextmatchHeader interface. - * - * @param {et2_nextmatch} _nextmatch - */ - setNextmatch: function(_nextmatch) { - this.nextmatch = _nextmatch; - - // Set current filter value from nextmatch settings - if(this.nextmatch.activeFilters.col_filter && typeof this.nextmatch.activeFilters.col_filter[this.id] != "undefined") - { - this.set_value(this.nextmatch.activeFilters.col_filter[this.id]); - - // Make sure it's set in the nextmatch - _nextmatch.activeFilters.col_filter[this.id] = this.getValue(); - } - }, - - // Make sure selectbox is not longer than the column - resize: function() { - this.div.css("height",''); - this.div.css("max-width",jQuery(this.parentNode).innerWidth() + "px"); - this._super.apply(this, arguments); - } - -});}).call(this); -et2_register_widget(et2_nextmatch_taglistheader, ['nextmatch-taglistheader']); - +var et2_nextmatch_taglistheader = /** @class */ (function (_super) { + __extends(et2_nextmatch_taglistheader, _super); + function et2_nextmatch_taglistheader() { + return _super !== null && _super.apply(this, arguments) || this; + } + /** + * Override to add change handler + * + * @memberOf et2_nextmatch_filterheader + */ + et2_nextmatch_taglistheader.prototype.createInputWidget = function () { + // Make sure there's an option for all + if (!this.options.empty_label && (!this.options.select_options || !this.options.select_options[""])) { + this.options.empty_label = this.options.label ? this.options.label : egw.lang("All"); + } + _super.prototype.createInputWidget.call(this); + }; + /** + * Disable toggle if there are 2 or less options + * @param {Object[]} options + */ + et2_nextmatch_taglistheader.prototype.set_select_options = function (options) { + if (options && options.length <= 2 && this.options.multiple == 'toggle') { + this.set_multiple(false); + } + _super.prototype.set_select_options.call(this, options); + }; + /** + * Set nextmatch is the function which has to be implemented for the + * et2_INextmatchHeader interface. + * + * @param {et2_nextmatch} _nextmatch + */ + et2_nextmatch_taglistheader.prototype.setNextmatch = function (_nextmatch) { + this.nextmatch = _nextmatch; + // Set current filter value from nextmatch settings + if (this.nextmatch.activeFilters.col_filter && typeof this.nextmatch.activeFilters.col_filter[this.id] != "undefined") { + this.set_value(this.nextmatch.activeFilters.col_filter[this.id]); + // Make sure it's set in the nextmatch + _nextmatch.activeFilters.col_filter[this.id] = this.getValue(); + } + }; + // Make sure selectbox is not longer than the column + et2_nextmatch_taglistheader.prototype.resize = function () { + this.div.css("height", ''); + this.div.css("max-width", jQuery(this.parentNode).innerWidth() + "px"); + _super.prototype.resize.call(this); + }; + et2_nextmatch_taglistheader._attributes = { + autocomplete_url: { default: '' }, + multiple: { default: 'toggle' }, + onchange: { + // @ts-ignore + default: function (event) { + if (typeof this.nextmatch === 'undefined') { + // Not fully set up yet + return; + } + var col_filter = {}; + col_filter[this.id] = this.getValue(); + // Set value so it's there for response (otherwise it gets cleared if options are updated) + //event.data.set_value(event.data.input.val()); + this.nextmatch.applyFilters({ col_filter: col_filter }); + } + }, + rows: { default: 2 }, + class: { default: 'nm_filterheader_taglist' } + }; + return et2_nextmatch_taglistheader; +}(et2_taglist)); +et2_core_widget_1.et2_register_widget(et2_nextmatch_taglistheader, ['nextmatch-taglistheader']); /** - * @augments et2_link_entry + * Nextmatch filter that can filter for a selected entry */ -var et2_nextmatch_entryheader = (function(){ "use strict"; return et2_link_entry.extend(et2_INextmatchHeader, -{ - /** - * Override to add change handler - * - * @memberOf et2_nextmatch_entryheader - * @param {object} event - * @param {object} selected - */ - onchange: function(event, selected) { - var col_filter = {}; - col_filter[this.id] = this.get_value(); - this.nextmatch.applyFilters.call(this.nextmatch, {col_filter: col_filter}); - }, - - /** - * Override to always return a string appname:id (or just id) for simple (one real selection) - * cases, parent returns an object. If multiple are selected, or anything other than app and - * id, the original parent value is returned. - */ - getValue: function() { - var value = this._super.apply(this, arguments); - if(typeof value == "object" && value != null) - { - if(!value.app || !value.id) return null; - - // If array with just one value, use a string instead for legacy server handling - if(typeof value.id == 'object' && value.id.shift && value.id.length == 1) - { - value.id = value.id.shift(); - } - // If simple value, format it legacy string style, otherwise - // we return full value - if(typeof value.id == 'string') - { - value = value.app +":"+value.id; - } - } - return value; - }, - - /** - * Set nextmatch is the function which has to be implemented for the - * et2_INextmatchHeader interface. - * - * @param {et2_nextmatch} _nextmatch - */ - setNextmatch: function(_nextmatch) { - this.nextmatch = _nextmatch; - - // Set current filter value from nextmatch settings - if(this.nextmatch.options.settings.col_filter && this.nextmatch.options.settings.col_filter[this.id]) - { - this.set_value(this.nextmatch.options.settings.col_filter[this.id]); - - if(this.getValue() != this.nextmatch.activeFilters.col_filter[this.id]) - { - this.nextmatch.activeFilters.col_filter[this.id] = this.getValue(); - } - - // Tell framework to ignore, or it will reset it to ''/empty when it does loadingFinished() - this.attributes.value.ignore = true; - //this.attributes.select_options.ignore = true; - } - var self = this; - // Fire on lost focus, clear filter if user emptied box - } -});}).call(this); -et2_register_widget(et2_nextmatch_entryheader, ['nextmatch-entryheader']); - +var et2_nextmatch_entryheader = /** @class */ (function (_super) { + __extends(et2_nextmatch_entryheader, _super); + function et2_nextmatch_entryheader() { + return _super !== null && _super.apply(this, arguments) || this; + } + /** + * Override to add change handler + * + * @memberOf et2_nextmatch_entryheader + * @param {object} event + * @param {object} selected + */ + et2_nextmatch_entryheader.prototype.onchange = function (event, selected) { + var col_filter = {}; + col_filter[this.id] = this.get_value(); + this.nextmatch.applyFilters.call(this.nextmatch, { col_filter: col_filter }); + }; + /** + * Override to always return a string appname:id (or just id) for simple (one real selection) + * cases, parent returns an object. If multiple are selected, or anything other than app and + * id, the original parent value is returned. + */ + et2_nextmatch_entryheader.prototype.getValue = function () { + var value = _super.prototype.getValue.call(this); + if (typeof value == "object" && value != null) { + if (!value.app || !value.id) + return null; + // If array with just one value, use a string instead for legacy server handling + if (typeof value.id == 'object' && value.id.shift && value.id.length == 1) { + value.id = value.id.shift(); + } + // If simple value, format it legacy string style, otherwise + // we return full value + if (typeof value.id == 'string') { + value = value.app + ":" + value.id; + } + } + return value; + }; + /** + * Set nextmatch is the function which has to be implemented for the + * et2_INextmatchHeader interface. + * + * @param {et2_nextmatch} _nextmatch + */ + et2_nextmatch_entryheader.prototype.setNextmatch = function (_nextmatch) { + this.nextmatch = _nextmatch; + // Set current filter value from nextmatch settings + if (this.nextmatch.options.settings.col_filter && this.nextmatch.options.settings.col_filter[this.id]) { + this.set_value(this.nextmatch.options.settings.col_filter[this.id]); + if (this.getValue() != this.nextmatch.activeFilters.col_filter[this.id]) { + this.nextmatch.activeFilters.col_filter[this.id] = this.getValue(); + } + // Tell framework to ignore, or it will reset it to ''/empty when it does loadingFinished() + this.attributes.value.ignore = true; + //this.attributes.select_options.ignore = true; + } + // Fire on lost focus, clear filter if user emptied box + }; + return et2_nextmatch_entryheader; +}(et2_link_entry)); +et2_core_widget_1.et2_register_widget(et2_nextmatch_entryheader, ['nextmatch-entryheader']); /** * @augments et2_nextmatch_filterheader */ -var et2_nextmatch_customfilter = (function(){ "use strict"; return et2_nextmatch_filterheader.extend( -{ - attributes: { - "widget_type": { - "name": "Actual type", - "type": "string", - "description": "The actual type of widget you should use", - "no_lang": 1 - }, - "widget_options": { - "name": "Actual options", - "type": "any", - "description": "The options for the actual widget", - "no_lang": 1, - "default": {} - } - }, - legacyOptions: ["widget_type","widget_options"], - - real_node: null, - - /** - * Constructor - * - * @param _parent - * @param _attrs - * @memberOf et2_nextmatch_customfilter - */ - init: function(_parent, _attrs) { - - switch(_attrs.widget_type) - { - case "link-entry": - _attrs.type = 'nextmatch-entryheader'; - break; - default: - if(_attrs.widget_type.indexOf('select') === 0) - { - _attrs.type = 'nextmatch-filterheader'; - } - else - { - _attrs.type = _attrs.widget_type; - } - } - jQuery.extend(_attrs.widget_options,{id: this.id}); - - _attrs.id = ''; - this._super.apply(this, arguments); - this.real_node = et2_createWidget(_attrs.type, _attrs.widget_options, this._parent); - var select_options = []; - var correct_type = _attrs.type; - this.real_node._type = _attrs.widget_type; - et2_selectbox.find_select_options(this.real_node, select_options, _attrs); - this.real_node._type = correct_type; - if(typeof this.real_node.set_select_options === 'function') - { - this.real_node.set_select_options(select_options); - } - }, - - // Just pass the real DOM node through, in case anybody asks - getDOMNode: function(_sender) { - return this.real_node ? this.real_node.getDOMNode(_sender) : null; - }, - - // Also need to pass through real children - getChildren: function() { - return this.real_node.getChildren() || []; - }, - setNextmatch: function(_nextmatch) - { - if(this.real_node && this.real_node.setNextmatch) - { - return this.real_node.setNextmatch(_nextmatch); - } - } -});}).call(this); -et2_register_widget(et2_nextmatch_customfilter, ['nextmatch-customfilter']); +var et2_nextmatch_customfilter = /** @class */ (function (_super) { + __extends(et2_nextmatch_customfilter, _super); + /** + * Constructor + * + * @param _parent + * @param _attrs + * @param _child + * @memberOf et2_nextmatch_customfilter + */ + function et2_nextmatch_customfilter(_parent, _attrs, _child) { + var _this = _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_nextmatch_customfilter._attributes, _child || {})) || this; + switch (_attrs.widget_type) { + case "link-entry": + _attrs.type = 'nextmatch-entryheader'; + break; + default: + if (_attrs.widget_type.indexOf('select') === 0) { + _attrs.type = 'nextmatch-filterheader'; + } + else { + _attrs.type = _attrs.widget_type; + } + } + jQuery.extend(_attrs.widget_options, { id: _this.id }); + _attrs.id = ''; + _this = _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_nextmatch_customfilter._attributes, _child || {})) || this; + _this.real_node = et2_createWidget(_attrs.type, _attrs.widget_options, _this.getParent()); + var select_options = []; + var correct_type = _attrs.type; + _this.real_node['type'] = _attrs.widget_type; + et2_widget_selectbox_1.et2_selectbox.find_select_options(_this.real_node, select_options, _attrs); + _this.real_node["_type"] = correct_type; + if (typeof _this.real_node.set_select_options === 'function') { + _this.real_node.set_select_options(select_options); + } + return _this; + } + // Just pass the real DOM node through, in case anybody asks + et2_nextmatch_customfilter.prototype.getDOMNode = function (_sender) { + return this.real_node ? this.real_node.getDOMNode(_sender) : null; + }; + // Also need to pass through real children + et2_nextmatch_customfilter.prototype.getChildren = function () { + return this.real_node.getChildren() || []; + }; + et2_nextmatch_customfilter.prototype.setNextmatch = function (_nextmatch) { + if (this.real_node && this.real_node.instanceOf(et2_INextmatchHeader)) { + return this.real_node.setNextmatch(_nextmatch); + } + }; + et2_nextmatch_customfilter._attributes = { + "widget_type": { + "name": "Actual type", + "type": "string", + "description": "The actual type of widget you should use", + "no_lang": 1 + }, + "widget_options": { + "name": "Actual options", + "type": "any", + "description": "The options for the actual widget", + "no_lang": 1, + "default": {} + } + }; + return et2_nextmatch_customfilter; +}(et2_nextmatch_filterheader)); +et2_core_widget_1.et2_register_widget(et2_nextmatch_customfilter, ['nextmatch-customfilter']); +//# sourceMappingURL=et2_extension_nextmatch.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_extension_nextmatch.ts b/api/js/etemplate/et2_extension_nextmatch.ts new file mode 100644 index 0000000000..72ad16f01e --- /dev/null +++ b/api/js/etemplate/et2_extension_nextmatch.ts @@ -0,0 +1,4080 @@ +/** + * EGroupware eTemplate2 - JS Nextmatch object + * + * @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 + * @version $Id$ + * + +/*egw:uses + + // Include the action system + egw_action.egw_action; + egw_action.egw_action_popup; + egw_action.egw_action_dragdrop; + egw_action.egw_menu_dhtmlx; + + // Include some core classes + et2_core_widget; + et2_core_interfaces; + et2_core_DOMWidget; + + // Include all widgets the nextmatch extension will create + et2_widget_template; + et2_widget_grid; + et2_widget_selectbox; + et2_widget_selectAccount; + et2_widget_taglist; + et2_extension_customfields; + + // Include all nextmatch subclasses + et2_extension_nextmatch_rowProvider; + et2_extension_nextmatch_controller; + et2_widget_dynheight; + + // Include the grid classes + et2_dataview; + +*/ + +import './et2_core_common'; +import './et2_core_interfaces'; +import {ClassWithAttributes} from "./et2_core_inheritance"; +import {et2_register_widget, et2_widget, WidgetConfig} from "./et2_core_widget"; +import {et2_DOMWidget} from "./et2_core_DOMWidget"; +import {et2_baseWidget} from "./et2_core_baseWidget"; +import {et2_inputWidget} from "./et2_core_inputWidget"; +import {et2_selectbox} from "./et2_widget_selectbox"; +import {et2_nextmatch_rowProvider} from "./et2_extension_nextmatch_rowProvider"; +import {et2_nextmatch_controller} from "./et2_extension_nextmatch_controller"; +import {et2_dataview} from "./et2_dataview"; +import {et2_dataview_column} from "./et2_dataview_model_columns"; +import {et2_customfields_list} from "./et2_extension_customfields"; + +//import {et2_selectAccount} from "./et2_widget_SelectAccount"; + +/** + * Interface all special nextmatch header elements have to implement. + */ +export interface et2_INextmatchHeader { + + /** + * The 'setNextmatch' function is called by the parent nextmatch widget + * and tells the nextmatch header widgets which widget they should direct + * their 'sort', 'search' or 'filter' calls to. + * + * @param {et2_nextmatch} _nextmatch + */ + setNextmatch(nextmatch : et2_nextmatch) : void +} +var et2_INextmatchHeader = "et2_INextmatchHeader"; +function implements_et2_INextmatchHeader(obj : et2_widget) +{ + return implements_methods(obj, ["setNextmatch"]); +} + +export interface et2_INextmatchSortable +{ + setSortmode(_sort_mode) : void +} +var et2_INextmatchSortable = "et2_INextmatchSortable"; +function implements_et2_INextmatchSortable(obj : et2_widget) +{ + return implements_methods(obj, ["setSortmode"]); +} + +/** + * Class which implements the "nextmatch" XET-Tag + * + * NM header is build like this in DOM + * + * +- nextmatch_header -----+------------+----------+--------+---------+--------------+-----------+-------+ + * + header_left | search.. | header_row | category | filter | filter2 | header_right | favorites | count | + * +-------------+----------+------------+----------+--------+---------+--------------+-----------+-------+ + * + * everything left incl. standard filters is floated left: + * +- nextmatch_header -----+------------+----------+--------+---------+ + * + header_left | search.. | header_row | category | filter | filter2 | + * +-------------+----------+------------+----------+--------+---------+ + * everything from header_right on is floated right: + * +--------------+-----------+-------+ + * | header_right | favorites | count | + * +--------------+-----------+-------+ + * @augments et2_DOMWidget + */ +export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2_IInput, et2_IPrint +{ + static readonly _attributes = { + // These normally set in settings, but broken out into attributes to allow run-time changes + "template": { + "name": "Template", + "type": "string", + "description": "The id of the template which contains the grid layout." + }, + "hide_header": { + "name": "Hide header", + "type": "boolean", + "description": "Hide the header", + "default": false + }, + "header_left": { + "name": "Left custom template", + "type": "string", + "description": "Customise the nextmatch - left side. Provided template becomes a child of nextmatch, and any input widgets are automatically bound to refresh the nextmatch on change. Any inputs with an onChange attribute can trigger the nextmatch to refresh by returning true.", + "default": "" + }, + "header_right": { + "name": "Right custom template", + "type": "string", + "description": "Customise the nextmatch - right side. Provided template becomes a child of nextmatch, and any input widgets are automatically bound to refresh the nextmatch on change. Any inputs with an onChange attribute can trigger the nextmatch to refresh by returning true.", + "default": "" + }, + "header_row": { + "name": "Inline custom template", + "type": "string", + "description": "Customise the nextmatch - inline, after row count. Provided template becomes a child of nextmatch, and any input widgets are automatically bound to refresh the nextmatch on change. Any inputs with an onChange attribute can trigger the nextmatch to refresh by returning true.", + "default": "" + }, + "no_filter": { + "name": "No filter", + "type": "boolean", + "description": "Hide the first filter", + "default": et2_no_init + }, + "no_filter2": { + "name": "No filter2", + "type": "boolean", + "description": "Hide the second filter", + "default": et2_no_init + }, + "view": { + "name": "View", + "type": "string", + "description": "Display entries as either 'row' or 'tile'. A matching template must also be set after changing this.", + "default": et2_no_init + }, + "onselect": { + "name": "onselect", + "type": "js", + "default": et2_no_init, + "description": "JS code which gets executed when rows are selected. Can also be a app.appname.func(selected) style method" + }, + "onfiledrop": { + "name": "onFileDrop", + "type": "js", + "default": et2_no_init, + "description": "JS code that gets executed when a _file_ is dropped on a row. Other drop interactions are handled by the action system. Return false to prevent the default link action." + }, + "settings": { + "name": "Settings", + "type": "any", + "description": "The nextmatch settings", + "default": {} + } + }; + + // Currently active filters + activeFilters: { + search? : string, + filter? : any, + filter2? : any, + col_filter: {}, + selectcols?: string[], + searchletter?: string + }; + + // DOM / jQuery stuff + private div: JQuery; + private innerDiv: JQuery; + private dynheight: any; + private blank: JQuery; + + // Popup to select columns + private selectPopup: any; + + public static legacyOptions = ["template","hide_header","header_left","header_right"]; + + private template: any; + columns: {widget: et2_widget}[]; + private sortedColumnsList: string[]; + + // If we need the nextmatch to have a value, keep it here. + // Normally this is used in actions, and is the action and selected rows. + private value: any; + + // Big old bag of settings + private settings: any; + + // Current view, either row or tile. We store it here as controllers are + // recreated when the template changes. + view: string; + + // Sub-objects used for actual work + private readonly header: et2_nextmatch_header_bar; + dataview: any; + private controller: any; + private rowProvider: any; + + + // Flag for an update is currently being done, to avoid a loop + private update_in_progress: boolean; + + // Window timer for automatically refreshing + private _autorefresh_timer: number; + + // When printing, we change the layout around. Keep some values so it can be restored after + private print: { + old_height: number, + row_selector: string, + orientation_style: HTMLStyleElement + }; + + /** + * Constructor + * + * @memberOf et2_nextmatch + */ + constructor(_parent?, _attrs? : WidgetConfig, _child? : object) { + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_nextmatch._attributes, _child || {})); + + this.activeFilters = {col_filter:{}}; + this.columns = []; + // keeps sorted columns + this.sortedColumnsList = []; + + // Directly set current col_filters from settings + jQuery.extend(this.activeFilters.col_filter, this.options.settings.col_filter); + + /* + Process selected custom fields here, so that the settings are correctly + set before the row template is parsed + */ + const prefs = this._getPreferences(); + const cfs = {}; + for(let i = 0; i < prefs.visible.length; i++) + { + if(prefs.visible[i].indexOf(et2_nextmatch_customfields.PREFIX) == 0) + { + cfs[prefs.visible[i].substr(1)] = !prefs.negated; + } + } + const global_data = this.getArrayMgr("modifications").getRoot().getEntry('~custom_fields~'); + if(typeof global_data == 'object' && global_data != null) + { + global_data.fields = cfs; + } + + this.div = jQuery(document.createElement("div")) + .addClass("et2_nextmatch"); + + + this.header = et2_createWidget("nextmatch_header_bar", {}, this); + this.innerDiv = jQuery(document.createElement("div")) + .appendTo(this.div); + + // Create the dynheight component which dynamically scales the inner + // container. + this.dynheight = this._getDynheight(); + + // Create the outer grid container + this.dataview = new et2_dataview(this.innerDiv, this.egw()); + + // Blank placeholder + this.blank = jQuery(document.createElement("div")) + .appendTo(this.dataview.table); + + // We cannot create the grid controller now, as this depends on the grid + // instance, which can first be created once we have the columns + this.controller = null; + this.rowProvider = null; + + } + + /** + * Destroys all + */ + destroy() + { + // Stop auto-refresh + if(this._autorefresh_timer) + { + window.clearInterval(this._autorefresh_timer); + this._autorefresh_timer = null; + } + // Unbind handler used for toggling autorefresh + jQuery(this.getInstanceManager().DOMContainer.parentNode).off('show.et2_nextmatch'); + jQuery(this.getInstanceManager().DOMContainer.parentNode).off('hide.et2_nextmatch'); + + // Free the grid components + this.dataview.destroy(); + if(this.rowProvider) + { + this.rowProvider.destroy(); + } + if(this.controller) + { + this.controller.destroy(); + } + this.dynheight.destroy(); + + super.destroy(); + } + + getController() + { + return this.controller; + } + + /** + * Loads the nextmatch settings + * + * @param {object} _attrs + */ + transformAttributes(_attrs) + { + super.transformAttributes(_attrs); + + if (this.id) + { + const entry = this.getArrayMgr("content").data; + _attrs["settings"] = {}; + + if (entry) + { + _attrs["settings"] = entry; + + // Make sure there's an action var parameter + if(_attrs["settings"]["actions"] && !_attrs.settings["action_var"]) + { + _attrs.settings.action_var = "action"; + } + + // Merge settings mess into attributes + for(let attr in this.attributes) + { + if(_attrs.settings[attr]) + { + _attrs[attr] = _attrs.settings[attr]; + delete _attrs.settings[attr]; + } + } + } + } + } + + doLoadingFinished() + { + super.doLoadingFinished(); + + if(!this.dynheight) + { + this.dynheight = this._getDynheight(); + } + + // Register handler for dropped files, if possible + if(this.options.settings.row_id) + { + // Appname should be first part of the template name + const split = this.options.template.split('.'); + const appname = split[0]; + + // Check link registry + if(this.egw().link_get_registry(appname)) + { + const self = this; + // Register a handler + // @ts-ignore + jQuery(this.div) + .on('dragenter','.egwGridView_grid tr',function(e) { + // Figure out _which_ row + const row = self.controller.getRowByNode(this); + + if(!row || !row.uid) + { + return false; + } + e.stopPropagation(); e.preventDefault(); + + // Indicate acceptance + if(row.controller && row.controller._selectionMgr) + { + row.controller._selectionMgr.setFocused(row.uid,true); + } + return false; + }) + .on('dragexit','.egwGridView_grid tr', function() { + self.controller._selectionMgr.setFocused(); + }) + .on('dragover','.egwGridView_grid tr',false).attr("dropzone","copy") + + .on('drop', '.egwGridView_grid tr',function(e) { + self.handle_drop(e,this); + return false; + }); + } + } + // stop invalidation in no visible tabs + jQuery(this.getInstanceManager().DOMContainer.parentNode).on('hide.et2_nextmatch', jQuery.proxy(function() { + if(this.controller && this.controller._grid) + { + this.controller._grid.doInvalidate = false; + } + },this)); + jQuery(this.getInstanceManager().DOMContainer.parentNode).on('show.et2_nextmatch', jQuery.proxy(function() { + if(this.controller && this.controller._grid) + { + this.controller._grid.doInvalidate = true; + } + },this)); + + return true; + } + + /** + * Implements the et2_IResizeable interface - lets the dynheight manager + * update the width and height and then update the dataview container. + */ + resize() + { + if (this.dynheight) + { + this.dynheight.update(function(_w, _h) { + this.dataview.resize(_w, _h); + }, this); + } + } + + /** + * Sorts the nextmatch widget by the given ID. + * + * @param {string} _id is the id of the data entry which should be sorted. + * @param {boolean} _asc if true, the elements are sorted ascending, otherwise + * descending. If not set, the sort direction will be determined + * automatically. + * @param {boolean} _update true/undefined: call applyFilters, false: only set sort + */ + sortBy( _id, _asc, _update? : boolean) + { + if (typeof _update == "undefined") + { + _update = true; + } + + // Create the "sort" entry in the active filters if it did not exist + // yet. + if (typeof this.activeFilters["sort"] == "undefined") + { + this.activeFilters["sort"] = { + "id": null, + "asc": true + }; + } + + // Determine the sort direction automatically if it is not set + if (typeof _asc == "undefined") + { + _asc = true; + if (this.activeFilters["sort"].id == _id) + { + _asc = !this.activeFilters["sort"].asc; + } + } + + // Set the sortmode display + this.iterateOver(function(_widget) { + _widget.setSortmode((_widget.id == _id) ? (_asc ? "asc": "desc") : "none"); + }, this, et2_INextmatchSortable); + + if (_update) + { + this.applyFilters({sort: { id: _id, asc: _asc}}); + } + else + { + // Update the entry in the activeFilters object + this.activeFilters["sort"] = { + "id": _id, + "asc": _asc + }; + } + } + + /** + * Removes the sort entry from the active filters object and thus returns to + * the natural sort order. + */ + resetSort() + { + // Check whether the nextmatch widget is currently sorted + if (typeof this.activeFilters["sort"] != "undefined") + { + // Reset the sort mode + this.iterateOver(function(_widget) { + _widget.setSortmode("none"); + }, this, et2_INextmatchSortable); + + // Delete the "sort" filter entry + this.applyFilters({sort: undefined}); + } + } + + /** + * Apply current or modified filters on NM widget (updating rows accordingly) + * + * @param _set filter(s) to set eg. { filter: '' } to reset filter in NM header + */ + applyFilters( _set? : object | any) + { + let changed = false; + let keep_selection = false; + + // Avoid loops cause by change events + if(this.update_in_progress) return; + this.update_in_progress = true; + + // Cleared explicitly + if(typeof _set != 'undefined' && jQuery.isEmptyObject(_set)) + { + changed = true; + this.activeFilters = {col_filter: {}}; + } + if(typeof this.activeFilters == "undefined") + { + this.activeFilters = {col_filter: {}}; + } + if(typeof this.activeFilters.col_filter == "undefined") + { + this.activeFilters.col_filter = {}; + } + + if (typeof _set == 'object') + { + for(let s in _set) + { + if (s == 'col_filter') + { + // allow apps setState() to reset all col_filter by using undefined or null for it + // they can not pass {} for _set / state.state, if they need to set something + if (_set.col_filter === undefined || _set.col_filter === null) + { + this.activeFilters.col_filter = {}; + changed = true; + } + else + { + for(let c in _set.col_filter) + { + if (this.activeFilters.col_filter[c] !== _set.col_filter[c]) + { + if (_set.col_filter[c]) + { + this.activeFilters.col_filter[c] = _set.col_filter[c]; + } + else + { + delete this.activeFilters.col_filter[c]; + } + changed = true; + } + } + } + } + else if (s === 'selected') + { + changed = true; + keep_selection = true; + this.controller._selectionMgr.resetSelection(); + this.controller._objectManager.clear(); + for(let i in _set.selected) + { + this.controller._selectionMgr.setSelected(_set.selected[i].indexOf('::') > 0 ? _set.selected[i] : this.controller.dataStorePrefix + '::'+_set.selected[i],true); + } + delete _set.selected; + } + else if (this.activeFilters[s] !== _set[s]) + { + this.activeFilters[s] = _set[s]; + changed = true; + } + } + } + + this.egw().debug("info", "Changing nextmatch filters to ", this.activeFilters); + + // Keep the selection after applying filters, but only if unchanged + if(!changed || keep_selection) + { + this.controller.keepSelection(); + } + else + { + // Do not keep selection + this.controller._selectionMgr.resetSelection(); + this.controller._objectManager.clear(); + this.controller.keepSelection(); + } + + // Update the filters in the grid controller + this.controller.setFilters(this.activeFilters); + + // Update the header + this.header.setFilters(this.activeFilters); + + // Update any column filters + this.iterateOver(function(column) { + // Skip favorites - it implements et2_INextmatchHeader, but we don't want it in the filter + if(typeof column.id != "undefined" && column.id.indexOf('favorite') == 0) return; + + if(typeof column.set_value != "undefined" && column.id) + { + column.set_value(typeof this[column.id] == "undefined" || this[column.id] == null ? "" : this[column.id]); + } + if (column.id && typeof column.get_value == "function") + { + this[column.id] = column.get_value(); + } + }, this.activeFilters.col_filter, et2_INextmatchHeader); + + // Trigger an update + this.controller.update(true); + + if(changed) + { + // Highlight matching favorite in sidebox + if(this.getInstanceManager().app) + { + const appname = this.getInstanceManager().app; + if(app[appname] && app[appname].highlight_favorite) + { + app[appname].highlight_favorite(); + } + } + } + + this.update_in_progress = false; + } + + /** + * Refresh given rows for specified change + * + * Change type parameters allows for quicker refresh then complete server side reload: + * - 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 + * + * @param {string[]|string} _row_ids rows to refresh + * @param {?string} _type "update", "edit", "delete" or "add" + * + * @see jsapi.egw_refresh() + * @fires refresh from the widget itself + */ + refresh( _row_ids, _type) + { + // Framework trying to refresh, but nextmatch not fully initialized + if(this.controller === null || !this.div) + { + return; + } + if (!this.div.is(':visible')) // run refresh, once we become visible again + { + jQuery(this.getInstanceManager().DOMContainer.parentNode).one('show.et2_nextmatch', + // Important to use anonymous function instead of just 'this.refresh' because + // of the parameters passed + jQuery.proxy(function() {this.refresh();},this) + ); + return; + } + if (typeof _type == 'undefined') _type = 'edit'; + if (typeof _row_ids == 'string' || typeof _row_ids == 'number') _row_ids = [_row_ids]; + if (typeof _row_ids == "undefined" || _row_ids === null) + { + this.applyFilters(); + + // Trigger an event so app code can act on it + jQuery(this).triggerHandler("refresh",[this]); + + return; + } + + if(_type == "delete") + { + // Record current & next index + var uid = _row_ids[0].toString().indexOf(this.controller.dataStorePrefix) == 0 ? _row_ids[0] : this.controller.dataStorePrefix + "::" + _row_ids[0]; + const entry = this.controller._selectionMgr._getRegisteredRowsEntry(uid); + let next = (entry.ao ? entry.ao.getNext(_row_ids.length) : null); + if(next == null || !next.id || next.id == uid) + { + // No next, select previous + next = (entry.ao?entry.ao.getPrevious(1):null); + } + + // Stop automatic updating + this.dataview.grid.doInvalidate = false; + for(var i = 0; i < _row_ids.length; i++) + { + uid = _row_ids[i].toString().indexOf(this.controller.dataStorePrefix) == 0 ? _row_ids[i] : this.controller.dataStorePrefix + "::" + _row_ids[i]; + + // Delete from internal references + this.controller.deleteRow(uid); + } + + // Select & focus next row + if(next && next.id) + { + this.controller._selectionMgr.setSelected(next.id,true); + this.controller._selectionMgr.setFocused(next.id,true); + } + + // Update the count + const total = this.dataview.grid._total - _row_ids.length; + // This will remove the last row! + // That's OK, because grid adds one in this.controller.deleteRow() + this.dataview.grid.setTotalCount(total); + // Re-enable automatic updating + this.dataview.grid.doInvalidate = true; + this.dataview.grid.invalidate(); + } + + id_loop: + for(var i = 0; i < _row_ids.length; i++) + { + var uid = _row_ids[i].toString().indexOf(this.controller.dataStorePrefix) == 0 ? _row_ids[i] : this.controller.dataStorePrefix + "::" + _row_ids[i]; + switch(_type) + { + case "update": + if(!this.egw().dataRefreshUID(uid)) + { + // Could not update just that row + this.applyFilters(); + break id_loop; + } + break; + case "delete": + // Handled above, more code to execute after loop + break; + case "edit": + case "add": + default: + // Trigger refresh + this.applyFilters(); + break id_loop; + } + } + // Trigger an event so app code can act on it + jQuery(this).triggerHandler("refresh",[this,_row_ids,_type]); + } + + /** + * Gets the selection + * + * @return Object { ids: [UIDs], inverted: boolean} + */ + getSelection() : {ids : string[], all : boolean} + { + const selected = this.controller && this.controller._selectionMgr ? this.controller._selectionMgr.getSelected() : null; + if(typeof selected == "object" && selected != null) + { + return selected; + } + return {ids:[],all:false}; + } + + /** + * Event handler for when the selection changes + * + * If the onselect attribute was set to a string with javascript code, it will + * be executed "legacy style". You can get the selected values with getSelection(). + * If the onselect attribute is in app.appname.function style, it will be called + * with the nextmatch and an array of selected row IDs. + * + * The array can be empty, if user cleared the selection. + * + * @param action ActionObject From action system. Ignored. + * @param senders ActionObjectImplemetation From action system. Ignored. + */ + onselect( action,senders) + { + // Execute the JS code connected to the event handler + if (typeof this.options.onselect == 'function') + { + return this.options.onselect.call(this, this.getSelection().ids, this); + } + } + + /** + * Nextmatch needs a namespace + */ + protected _createNamespace(): boolean + { + return true; + } + + /** + * Create the dynamic height so nm fills all available space + * + * @returns {undefined} + */ + _getDynheight() + { + // Find the parent container, either a tab or the main container + const tab = this.get_tab_info(); + + if(!tab) + { + return new et2_dynheight(this.getInstanceManager().DOMContainer, this.innerDiv, 100); + } + + else if (tab && tab.contentDiv) + { + return new et2_dynheight(tab.contentDiv, this.innerDiv, 100); + } + + return false; + } + + /** + * Generates the column caption for the given column widget + * + * @param {et2_widget} _widget + */ + _genColumnCaption( _widget) + { + let result = null; + + if(typeof _widget._genColumnCaption == "function") return _widget._genColumnCaption(); + const self = this; + + _widget.iterateOver(function(_widget) { + const label = self.egw().lang(_widget.options.label || _widget.options.empty_label || ''); + if (!label) return; // skip empty, undefined or null labels + if (!result) + { + result = label; + } + else + { + result += ", " + label; + } + }, this, et2_INextmatchHeader); + + return result; + } + + /** + * Generates the column name (internal) for the given column widget + * Used in preferences to refer to the columns by name instead of position + * + * See _getColumnCaption() for human fiendly captions + * + * @param {et2_widget} _widget + */ + _getColumnName( _widget) + { + if(typeof _widget._getColumnName == 'function') return _widget._getColumnName(); + + const name = _widget.id; + const child_names = []; + const children = _widget.getChildren(); + for(let i = 0; i < children.length; i++) { + if(children[i].id) child_names.push(children[i].id); + } + + const colName = name + (name != "" && child_names.length > 0 ? "_" : "") + child_names.join("_"); + if(colName == "") { + this.egw().debug("info", "Unable to generate nm column name for ", _widget); + } + return colName; + } + + + /** + * Retrieve the user's preferences for this nextmatch merged with defaults + * Column display, column size, etc. + */ + _getPreferences() + { + // Read preference or default for column visibility + let negated = false; + let columnPreference = ""; + if(this.options.settings.default_cols) + { + negated = this.options.settings.default_cols[0] == "!"; + columnPreference = negated ? this.options.settings.default_cols.substring(1) : this.options.settings.default_cols; + } + if(this.options.settings.selectcols && this.options.settings.selectcols.length) + { + columnPreference = this.options.settings.selectcols; + negated = false; + } + if(!this.options.settings.columnselection_pref) + { + // Set preference name so changes are saved + this.options.settings.columnselection_pref = this.options.template; + } + + let app = ''; + let list = []; + if(this.options.settings.columnselection_pref) { + let pref = {}; + list = et2_csvSplit(this.options.settings.columnselection_pref, 2, "."); + if(this.options.settings.columnselection_pref.indexOf('nextmatch') == 0) + { + app = list[0].substring('nextmatch'.length+1); + pref = egw.preference(this.options.settings.columnselection_pref, app); + } + else + { + app = list[0]; + // 'nextmatch-' prefix is there in preference name, but not in setting, so add it in + pref = egw.preference("nextmatch-"+this.options.settings.columnselection_pref, app); + } + if(pref) + { + negated = (pref[0] == "!"); + columnPreference = negated ? (pref).substring(1) : pref; + } + } + + let columnDisplay = []; + // If no column preference or default set, use all columns + if(typeof columnPreference =="string" && columnPreference.length == 0) + { + columnDisplay = []; + negated = true; + } + + columnDisplay = typeof columnPreference === "string" + ? et2_csvSplit(columnPreference,null,",") : columnPreference; + + // Adjusted column sizes + let size = {}; + if(this.options.settings.columnselection_pref && app) + { + let size_pref = this.options.settings.columnselection_pref + "-size"; + + // If columnselection pref is missing prefix, add it in + if(size_pref.indexOf('nextmatch') == -1) + { + size_pref = 'nextmatch-'+size_pref; + } + size = this.egw().preference(size_pref, app); + } + if(!size) size = {}; + + // Column order + const order = {}; + for(let i = 0; i < columnDisplay.length; i++) + { + order[columnDisplay[i]] = i; + } + return { + visible: columnDisplay, + visible_negated: negated, + negated: negated, + size: size, + order: order + }; + } + + /** + * Apply stored user preferences to discovered columns + * + * @param {array} _row + * @param {array} _colData + */ + _applyUserPreferences( _row, _colData) + { + const prefs = this._getPreferences(); + const columnDisplay = prefs.visible; + const size = prefs.size; + const negated = prefs.visible_negated; + const order = prefs.order; + let colName = ''; + + // Add in display preferences + if(columnDisplay && columnDisplay.length > 0) + { + RowLoop: + for(let i = 0; i < _row.length; i++) + { + colName = ''; + if(_row[i].disabled === true) + { + _colData[i].visible = false; + continue; + } + + // Customfields needs special processing + if(_row[i].widget.instanceOf(et2_nextmatch_customfields)) + { + // Find cf field + for(var j = 0; j < columnDisplay.length; j++) + { + if(columnDisplay[j].indexOf(_row[i].widget.id) == 0) { + _row[i].widget.options.fields = {}; + for(let k = j; k < columnDisplay.length; k++) + { + if(columnDisplay[k].indexOf(_row[i].widget.prefix) == 0) + { + _row[i].widget.options.fields[columnDisplay[k].substr(1)] = true; + } + } + // Resets field visibility too + _row[i].widget._getColumnName(); + _colData[i].visible = !(negated || jQuery.isEmptyObject(_row[i].widget.options.fields)); + break; + } + } + // Disable if there are no custom fields + if(jQuery.isEmptyObject(_row[i].widget.customfields)) + { + _colData[i].visible = false; + continue; + } + colName = _row[i].widget.id; + } + else + { + colName = this._getColumnName(_row[i].widget); + } + if(!colName) continue; + + if(size[colName]) + { + // Make sure percentages stay percentages, and forget any preference otherwise + if(_colData[i].width.charAt(_colData[i].width.length - 1) == "%") + { + _colData[i].width = typeof size[colName] == 'string' && size[colName].charAt(size[colName].length - 1) == "%" ? size[colName] : _colData[i].width; + } + else + { + _colData[i].width = parseInt(size[colName])+'px'; + } + } + if(!negated) + { + _colData[i].order = typeof order[colName] === 'undefined' ? i : order[colName]; + } + for(var j = 0; j < columnDisplay.length; j++) + { + if(columnDisplay[j] == colName) + { + _colData[i].visible = !negated; + + continue RowLoop; + } + } + _colData[i].visible = negated; + } + } + + _colData.sort(function(a,b) { + return a.order - b.order; + }); + _row.sort(function(a,b) { + if(typeof a.colData !== 'undefined' && typeof b.colData !== 'undefined') + { + return a.colData.order - b.colData.order; + } + else if (typeof a.order !== 'undefined' && typeof b.order !== 'undefined') + { + return a.order - b.order; + } + }); + } + + /** + * Take current column display settings and store them in this.egw().preferences + * for next time + */ + _updateUserPreferences() + { + const colMgr = this.dataview.getColumnMgr(); + let app = ""; + if(!this.options.settings.columnselection_pref) { + this.options.settings.columnselection_pref = this.options.template; + } + + const visibility = colMgr.getColumnVisibilitySet(); + const colDisplay = []; + const colSize = {}; + const custom_fields = []; + + // visibility is indexed by internal ID, widget is referenced by position, preference needs name + for(var i = 0; i < colMgr.columns.length; i++) + { + // @ts-ignore + const widget = this.columns[i].widget; + let colName = this._getColumnName(widget); + if(colName) { + // Server side wants each cf listed as a seperate column + if(widget.instanceOf(et2_nextmatch_customfields)) + { + // Just the ID for server side, not the whole nm name - some apps use it to skip custom fields + colName = widget.id; + for(let name in widget.options.fields) { + if(widget.options.fields[name]) custom_fields.push(et2_nextmatch_customfields.PREFIX+name); + } + } + if(visibility[colMgr.columns[i].id].visible) colDisplay.push(colName); + + // When saving sizes, only save columns with explicit values, preserving relative vs fixed + // Others will be left to flex if width changes or more columns are added + if(colMgr.columns[i].relativeWidth) + { + colSize[colName] = (colMgr.columns[i].relativeWidth * 100) + "%"; + } + else if (colMgr.columns[i].fixedWidth) + { + colSize[colName] = colMgr.columns[i].fixedWidth; + } + } else if (colMgr.columns[i].fixedWidth || colMgr.columns[i].relativeWidth) { + this.egw().debug("info", "Could not save column width - no name", colMgr.columns[i].id); + } + } + + const list = et2_csvSplit(this.options.settings.columnselection_pref, 2, "."); + let pref = this.options.settings.columnselection_pref; + if(pref.indexOf('nextmatch') == 0) + { + app = list[0].substring('nextmatch'.length+1); + } + else + { + app = list[0]; + // 'nextmatch-' prefix is there in preference name, but not in setting, so add it in + pref = "nextmatch-"+this.options.settings.columnselection_pref; + } + + // Server side wants each cf listed as a seperate column + jQuery.merge(colDisplay, custom_fields); + + // Update query value, so data source can use visible columns to exclude expensive sub-queries + const oldCols = this.activeFilters.selectcols ? this.activeFilters.selectcols : []; + + this.activeFilters.selectcols = this.sortedColumnsList ? this.sortedColumnsList : colDisplay; + + // We don't need to re-query if they've removed a column + const changed = []; + ColLoop: + for(var i = 0; i < colDisplay.length; i++) + { + for(let j = 0; j < oldCols.length; j++) { + if(colDisplay[i] == oldCols[j]) continue ColLoop; + } + changed.push(colDisplay[i]); + } + + // If a custom field column was added, throw away cache to deal with + // efficient apps that didn't send all custom fields in the first request + const cf_added = jQuery(changed).filter(jQuery(custom_fields)).length > 0; + + // Save visible columns + // 'nextmatch-' prefix is there in preference name, but not in setting, so add it in + this.egw().set_preference(app, pref, this.activeFilters.selectcols.join(","), + // Use callback after the preference gets set to trigger refresh, in case app + // isn't looking at selectcols and just uses preference + cf_added ? jQuery.proxy(function() {if(this.controller) this.controller.update(true);}, this):null + ); + + // Save adjusted column sizes + this.egw().set_preference(app, pref+"-size", colSize); + + // No significant change (just normal columns shown) and no need to wait, + // but the grid still needs to be redrawn if a custom field was removed because + // the cell content changed. This is a cheaper refresh than the callback, + // this.controller.update(true) + if((changed.length || custom_fields.length) && !cf_added) this.applyFilters(); + } + + _parseHeaderRow( _row, _colData) + { + + // Make sure there's a widget - cols disabled in template can be missing them, and the header really likes to have a widget + + for (var x = 0; x < _row.length; x++) + { + if(!_row[x].widget) + { + _row[x].widget = et2_createWidget("label", {}); + } + } + + // Get column display preference + this._applyUserPreferences(_row, _colData); + + // Go over the header row and create the column entries + this.columns = new Array(_row.length); + const columnData = new Array(_row.length); + + // No action columns in et2 + let remove_action_index = null; + + for (var x = 0; x < _row.length; x++) + { + this.columns[x] = jQuery.extend({ + "order": _colData[x] && typeof _colData[x].order !== 'undefined' ? _colData[x].order : x, + "widget": _row[x].widget + },_colData[x]); + + let visibility = (!_colData[x] || _colData[x].visible) ? + et2_dataview_column.ET2_COL_VISIBILITY_VISIBLE : + et2_dataview_column.ET2_COL_VISIBILITY_INVISIBLE; + if(_colData[x].disabled && _colData[x].disabled !=='' && + this.getArrayMgr("content").parseBoolExpression(_colData[x].disabled)) + { + visibility = et2_dataview_column.ET2_COL_VISIBILITY_DISABLED; + } + columnData[x] = { + "id": "col_" + x, + // @ts-ignore + "order": this.columns[x].order, + "caption": this._genColumnCaption(_row[x].widget), + "visibility": visibility, + "width": _colData[x] ? _colData[x].width : 0 + }; + if(_colData[x].width === 'auto') + { + // Column manager does not understand 'auto', which grid widget + // uses if width is not set + columnData[x].width = '100%'; + } + if(_colData[x].minWidth) + { + columnData[x].minWidth = _colData[x].minWidth; + } + if(_colData[x].maxWidth) + { + columnData[x].maxWidth = _colData[x].maxWidth; + } + + // No action columns in et2 + const colName = this._getColumnName(_row[x].widget); + if(colName == 'actions' || colName == 'legacy_actions' || colName == 'legacy_actions_check_all') + { + remove_action_index = x; + + } + else if (!colName) + { + // Unnamed column cannot be toggled or saved + columnData[x].visibility = et2_dataview_grid.ET2_COL_VISIBILITY_ALWAYS_NOSELECT; + } + + } + + // Remove action column + if(remove_action_index != null) + { + this.columns.splice(remove_action_index,remove_action_index); + columnData.splice(remove_action_index,remove_action_index); + _colData.splice(remove_action_index,remove_action_index); + } + + // Create the column manager and update the grid container + this.dataview.setColumns(columnData); + + for (var x = 0; x < _row.length; x++) + { + // Append the widget to this container + this.addChild(_row[x].widget); + } + + // Create the nextmatch row provider + this.rowProvider = new et2_nextmatch_rowProvider( + this.dataview.rowProvider, this._getSubgrid, this); + + // Register handler to update preferences when column properties are changed + const self = this; + this.dataview.onUpdateColumns = function() { + // Use apply to make sure context is there + self._updateUserPreferences.apply(self); + + // Allow column widgets a chance to resize + self.iterateOver(function(widget) {widget.resize();}, self, et2_IResizeable); + }; + + // Register handler for column selection popup, or disable + if(this.selectPopup) + { + this.selectPopup.remove(); + this.selectPopup = null; + } + if(this.options.settings.no_columnselection) + { + this.dataview.selectColumnsClick = function() {return false;}; + jQuery('span.selectcols',this.dataview.headTr).hide(); + } + else + { + jQuery('span.selectcols',this.dataview.headTr).show(); + this.dataview.selectColumnsClick = function(event) { + self._selectColumnsClick(event); + }; + } + } + + _parseDataRow( _row, _rowData, _colData) + { + const columnWidgets = new Array(this.columns.length); + + _row.sort(function(a,b) { + return a.colData.order - b.colData.order; + }); + + for (let x = 0; x < columnWidgets.length; x++) + { + if (typeof _row[x] != "undefined" && _row[x].widget) + { + columnWidgets[x] = _row[x].widget; + + // Append the widget to this container + this.addChild(_row[x].widget); + } + else + { + columnWidgets[x] = _row[x].widget; + } + // Pass along column alignment + if(_row[x].align && columnWidgets[x]) + { + columnWidgets[x].align = _row[x].align; + } + } + + this.rowProvider.setDataRowTemplate(columnWidgets, _rowData, this); + + // Create the grid controller + this.controller = new et2_nextmatch_controller( + null, + this.egw(), + this.getInstanceManager().etemplate_exec_id, + this, + null, + this.dataview.grid, + this.rowProvider, + this.options.settings.action_links, + null, + this.options.actions + ); + + // Need to trigger empty row the first time + if(total == 0) this.controller._emptyRow(); + + // Set data cache prefix to either provided custom or auto + if(!this.options.settings.dataStorePrefix && this.options.settings.get_rows) + { + // Use jsapi data module to update + let list = this.options.settings.get_rows.split('.', 2); + if (list.length < 2) list = this.options.settings.get_rows.split('_'); // support "app_something::method" + this.options.settings.dataStorePrefix = list[0]; + } + this.controller.setPrefix(this.options.settings.dataStorePrefix); + + // Set the view + this.controller._view = this.view; + + // Load the initial order + /*this.controller.loadInitialOrder(this._getInitialOrder( + this.options.settings.rows, this.options.settings.row_id + ));*/ + + // Set the initial row count + var total = typeof this.options.settings.total != "undefined" ? + this.options.settings.total : 0; + // This triggers an invalidate, which updates the grid + this.dataview.grid.setTotalCount(total); + + // Insert any data sent from server, so invalidate finds data already + if(this.options.settings.rows && this.options.settings.num_rows) + { + this.controller.loadInitialData( + this.options.settings.dataStorePrefix, + this.options.settings.row_id, + this.options.settings.rows + ); + // Remove, to prevent duplication + delete this.options.settings.rows; + } + } + + _parseGrid( _grid) + { + // Search the rows for a header-row - if one is found, parse it + for (let y = 0; y < _grid.rowData.length; y++) + { + // Parse the first row as a header, need header to parse the data rows + if (_grid.rowData[y]["class"] == "th" || y == 0) + { + this._parseHeaderRow(_grid.cells[y], _grid.colData); + } + else + { + this._parseDataRow(_grid.cells[y], _grid.rowData[y], + _grid.colData); + } + } + this.dataview.table.resize(); + } + + _getSubgrid( _row, _data, _controller) + { + // Fetch the id of the element described by _data, this will be the + // parent_id of the elements in the subgrid + const rowId = _data.content[this.options.settings.row_id]; + + // Create a new grid with the row as parent and the dataview grid as + // parent grid + const grid = new et2_dataview_grid(_row, this.dataview.grid); + + // Create a new controller for the grid + const controller = new et2_nextmatch_controller( + _controller, + this.egw(), + this.getInstanceManager().etemplate_exec_id, + this, + rowId, + grid, + this.rowProvider, + this.options.settings.action_links, + _controller.getObjectManager() + ); + controller.update(); + + // Register inside the destruction callback of the grid + grid.setDestroyCallback(function () { + controller.destroy(); + }); + + return grid; + } + + _getInitialOrder( _rows, _rowId) + { + + const _order = []; + + // Get the length of the non-numerical rows arra + let len = 0; + for (let key in _rows) { + if (!isNaN(parseInt(key)) && parseInt(key) > len) + len = parseInt(key); + } + + // Iterate over the rows + for (let i = 0; i < len; i++) + { + // Get the uid from the data + const uid = this.egw().app_name() + '::' + _rows[i][_rowId]; + + // Store the data for that uid + this.egw().dataStoreUID(uid, _rows[i]); + + // Push the uid onto the order array + _order.push(uid); + } + + return _order; + } + + _selectColumnsClick( e) + { + const self = this; + const columnMgr = this.dataview.getColumnMgr(); + + // ID for faking letter selection in column selection + const LETTERS = '~search_letter~'; + + const columns = {}; + const columns_selected = []; + + for (var i = 0; i < columnMgr.columns.length; i++) + { + var col = columnMgr.columns[i]; + const widget = this.columns[i].widget; + + if(col.visibility == et2_dataview_column.ET2_COL_VISIBILITY_DISABLED || + col.visibility == et2_dataview_column.ET2_COL_VISIBILITY_ALWAYS_NOSELECT) + { + continue; + } + if(col.caption) + { + columns[col.id] = col.caption; + if(col.visibility == et2_dataview_column.ET2_COL_VISIBILITY_VISIBLE) columns_selected.push(col.id); + } + // Custom fields get listed separately + if(widget.instanceOf(et2_nextmatch_customfields)) + { + if(jQuery.isEmptyObject((widget).customfields)) + { + // No customfields defined, don't show column + delete(columns[col.id]); + continue; + } + for(var field_name in (widget).customfields) + { + columns[et2_nextmatch_customfields.PREFIX+field_name] = " - "+ + (widget).customfields[field_name].label; + if(widget.options.fields[field_name]) columns_selected.push(et2_customfields_list.PREFIX+field_name); + } + } + } + + // Letter search + if(this.options.settings.lettersearch) + { + columns[LETTERS] = egw.lang('Search letter'); + if(this.header.lettersearch.is(':visible')) columns_selected.push(LETTERS); + } + + // Build the popup + if(!this.selectPopup) + { + const select = et2_createWidget("select", { + multiple: true, + rows: 8, + empty_label: this.egw().lang("select columns"), + selected_first: false, + value_class: "selcolumn_sortable_" + }, this); + select.set_select_options(columns); + select.set_value(columns_selected); + + const autoRefresh = et2_createWidget("select", { + "empty_label": "Refresh" + }, this); + autoRefresh.set_id("nm_autorefresh"); + autoRefresh.set_select_options({ + // Cause [unknown] problems with mail + //30: "30 seconds", + //60: "1 Minute", + 300: "5 Minutes", + 900: "15 Minutes", + 1800: "30 Minutes" + }); + autoRefresh.set_value(this._get_autorefresh()); + autoRefresh.set_statustext(egw.lang("Automatically refresh list")); + + const defaultCheck = et2_createWidget("select", {"empty_label": "Preference"}, this); + defaultCheck.set_id('nm_col_preference'); + defaultCheck.set_select_options({ + 'default': {label: 'Default',title:'Set these columns as the default'}, + 'reset': {label: 'Reset', title:"Reset all user's column preferences"}, + 'force': {label: 'Force', title:'Force column preference so users cannot change it'} + }); + defaultCheck.set_value(this.options.settings.columns_forced ? 'force': ''); + + const okButton = et2_createWidget("buttononly", {"background_image": true, image: "check"}, this); + okButton.set_label(this.egw().lang("ok")); + okButton.onclick = function() { + // Update visibility + const visibility = {}; + for (var i = 0; i < columnMgr.columns.length; i++) + { + const col = columnMgr.columns[i]; + if(col.caption && col.visibility !== et2_dataview_column.ET2_COL_VISIBILITY_ALWAYS_NOSELECT && + col.visibility !== et2_dataview_column.ET2_COL_VISIBILITY_DISABLED ) + { + visibility[col.id] = {visible: false}; + } + } + const value = select.getValue(); + + // Update & remove letter filter + if(self.header.lettersearch) + { + var show_letters = true; + if(value.indexOf(LETTERS) >= 0) + { + value.splice(value.indexOf(LETTERS),1); + } + else + { + show_letters = false; + } + self._set_lettersearch(show_letters); + } + + let column = 0; + for(var i = 0; i < value.length; i++) + { + // Handle skipped columns + while(value[i] != "col_"+column && column < columnMgr.columns.length) + { + column++; + } + if(visibility[value[i]]) + { + visibility[value[i]].visible = true; + } + // Custom fields are listed seperately in column list, but are only 1 column + if(self.columns[column] && self.columns[column].widget.instanceOf(et2_nextmatch_customfields)) { + const cf = self.columns[column].widget.options.customfields; + const visible = self.columns[column].widget.options.fields; + + // Turn off all custom fields + for(var field_name in cf) + { + visible[field_name] = false; + } + // Turn on selected custom fields - start from 0 in case they're not in order + for(let j = 0; j < value.length; j++) + { + if(value[j].indexOf(et2_customfields_list.PREFIX) != 0) continue; + visible[value[j].substring(1)] = true; + i++; + } + (self.columns[column].widget).set_visible(visible); + } + } + columnMgr.setColumnVisibilitySet(visibility); + + this.sortedColumnsList = []; + jQuery(select.getDOMNode()).find('li[class^="selcolumn_sortable_"]').each(function(i,v){ + const data_id = v.getAttribute('data-value'); + const value = select.getValue(); + if (data_id.match(/^col_/) && value.indexOf(data_id) != -1) + { + const col_id = data_id.replace('col_', ''); + const col_widget = self.columns[col_id].widget; + if (col_widget.customfields) + { + self.sortedColumnsList.push(col_widget.id); + for(let field_name in col_widget.customfields) + { + if(jQuery.isEmptyObject(col_widget.options.fields) || col_widget.options.fields[field_name] == true) + { + self.sortedColumnsList.push(et2_customfields_list.PREFIX + field_name); + } + } + } + else + { + self.sortedColumnsList.push(self._getColumnName(col_widget)); + } + } + }); + + // Hide popup + self.selectPopup.toggle(); + + self.dataview.updateColumns(); + + // Auto refresh + self._set_autorefresh(autoRefresh.get_value()); + + // Set default or clear forced + if(show_letters) + { + self.activeFilters.selectcols.push('lettersearch'); + } + self.getInstanceManager().submit(); + + self.selectPopup = null; + }; + + const cancelButton = et2_createWidget("buttononly", {"background_image": true, image: "cancel"}, this); + cancelButton.set_label(this.egw().lang("cancel")); + cancelButton.onclick = function() { + self.selectPopup.toggle(); + self.selectPopup = null; + }; + const $select = jQuery(select.getDOMNode()); + $select.find('.ui-multiselect-checkboxes').sortable({ + placeholder:'ui-fav-sortable-placeholder', + items:'li[class^="selcolumn_sortable_col"]', + cancel: 'li[class^="selcolumn_sortable_#"]', + cursor: "move", + tolerance: "pointer", + axis: 'y', + containment: "parent", + delay: 250, //(millisecond) delay before the sorting should start + beforeStop: function(event, ui) { + jQuery('li[class^="selcolumn_sortable_#"]', this).css({ + opacity: 1 + }); + }, + start: function(event, ui){ + jQuery('li[class^="selcolumn_sortable_#"]', this).css({ + opacity: 0.5 + }); + }, + sort: function (event, ui) + { + jQuery( this ).sortable("refreshPositions" ); + } + }); + $select.disableSelection(); + $select.find('li[class^="selcolumn_sortable_"]').each(function(i,v){ + // @ts-ignore + jQuery(v).attr('data-value',(jQuery(v).find('input')[0].value)) + }); + const $footerWrap = jQuery(document.createElement("div")) + .addClass('dialogFooterToolbar') + .append(okButton.getDOMNode()) + .append(cancelButton.getDOMNode()); + this.selectPopup = jQuery(document.createElement("div")) + .addClass("colselection ui-dialog ui-widget-content") + .append(select.getDOMNode()) + .append($footerWrap) + .appendTo(this.innerDiv); + + // Add autorefresh + $footerWrap.append(autoRefresh.getSurroundings().getDOMNode(autoRefresh.getDOMNode())); + + // Add default checkbox for admins + const apps = this.egw().user('apps'); + if(apps['admin']) + { + $footerWrap.append(defaultCheck.getSurroundings().getDOMNode(defaultCheck.getDOMNode())); + } + } + else + { + this.selectPopup.toggle(); + } + const t_position = jQuery(e.target).position(); + const s_position = this.div.position(); + const max_height = this.getDOMNode().getElementsByClassName('egwGridView_outer')[0]['tBodies'][0].clientHeight - + (2 * this.selectPopup.find('.dialogFooterToolbar').height()); + this.selectPopup.find('.ui-multiselect-checkboxes').css('max-height',max_height); + this.selectPopup.css("top", t_position.top) + .css("left", s_position.left + this.div.width() - this.selectPopup.width()); + } + + /** + * Set the currently displayed columns, without updating user's preference + * + * @param {string[]} column_list List of column names + * @param {boolean} trigger_update =false - explicitly trigger an update + */ + set_columns(column_list : string[], trigger_update = false) + { + const columnMgr = this.dataview.getColumnMgr(); + const visibility = {}; + + // Initialize to false + for (var i = 0; i < columnMgr.columns.length; i++) + { + const col = columnMgr.columns[i]; + if(col.caption && col.visibility != et2_dataview_grid.ET2_COL_VISIBILITY_ALWAYS_NOSELECT ) + { + visibility[col.id] = {visible: false}; + } + } + for(var i = 0; i < this.columns.length; i++) + { + + let widget = this.columns[i].widget; + let colName = this._getColumnName(widget); + if(column_list.indexOf(colName) !== -1 && + typeof visibility[columnMgr.columns[i].id] !== 'undefined' + ) + { + visibility[columnMgr.columns[i].id].visible = true; + } + // Custom fields are listed seperately in column list, but are only 1 column + if(widget && widget.instanceOf(et2_nextmatch_customfields)) { + + // Just the ID for server side, not the whole nm name - some apps use it to skip custom fields + colName = widget.id; + if(column_list.indexOf(colName) !== -1) + { + visibility[columnMgr.columns[i].id].visible = true; + } + + const cf = this.columns[i].widget.options.customfields; + const visible = this.columns[i].widget.options.fields; + + // Turn off all custom fields + for(let field_name in cf) + { + visible[field_name] = false; + } + // Turn on selected custom fields - start from 0 in case they're not in order + for(let j = 0; j < column_list.length; j++) + { + if(column_list[j].indexOf(et2_customfields_list.PREFIX) != 0) continue; + visible[column_list[j].substring(1)] = true; + } + (widget).set_visible(visible); + } + } + columnMgr.setColumnVisibilitySet(visibility); + + // We don't want to update user's preference, so directly update + this.dataview._updateColumns(); + + // Allow column widgets a chance to resize + this.iterateOver(function(widget) {widget.resize();}, this, et2_IResizeable); + } + + /** + * Set the letter search preference, and update the UI + * + * @param {boolean} letters_on + */ + _set_lettersearch( letters_on) + { + if(letters_on) + { + this.header.lettersearch.show(); + } + else + { + this.header.lettersearch.hide(); + } + const lettersearch_preference = "nextmatch-" + this.options.settings.columnselection_pref + "-lettersearch"; + this.egw().set_preference(this.egw().app_name(),lettersearch_preference,letters_on); + } + + /** + * Set the auto-refresh time period, and starts the timer if not started + * + * @param time int Refresh period, in seconds + */ + _set_autorefresh( time) + { + // Store preference + const refresh_preference = "nextmatch-" + this.options.settings.columnselection_pref + "-autorefresh"; + const app = this.options.template.split("."); + if(this._get_autorefresh() != time) + { + this.egw().set_preference(app[0],refresh_preference,time); + } + + // Start / update timer + if (this._autorefresh_timer) + { + window.clearInterval(this._autorefresh_timer); + delete this._autorefresh_timer; + } + if(time > 0) + { + this._autorefresh_timer = setInterval(jQuery.proxy(this.controller.update, this.controller), time * 1000); + + // Bind to tab show/hide events, so that we don't bother refreshing in the background + jQuery(this.getInstanceManager().DOMContainer.parentNode).on('hide.et2_nextmatch', jQuery.proxy(function(e) { + // Stop + window.clearInterval(this._autorefresh_timer); + jQuery(e.target).off(e); + + // If the autorefresh time is up, bind once to trigger a refresh + // (if needed) when tab is activated again + this._autorefresh_timer = setTimeout(jQuery.proxy(function() { + // Check in case it was stopped / destroyed since + if(!this._autorefresh_timer || !this.getInstanceManager()) return; + + jQuery(this.getInstanceManager().DOMContainer.parentNode).one('show.et2_nextmatch', + // Important to use anonymous function instead of just 'this.refresh' because + // of the parameters passed + jQuery.proxy(function() {this.refresh();},this) + ); + },this), time*1000); + },this)); + jQuery(this.getInstanceManager().DOMContainer.parentNode).on('show.et2_nextmatch', jQuery.proxy(function(e) { + // Start normal autorefresh timer again + this._set_autorefresh(this._get_autorefresh()); + jQuery(e.target).off(e); + },this)); + } + } + + /** + * Get the auto-refresh timer + * + * @return int Refresh period, in secods + */ + _get_autorefresh( ) + { + const refresh_preference = "nextmatch-" + this.options.settings.columnselection_pref + "-autorefresh"; + const app = this.options.template.split("."); + return this.egw().preference(refresh_preference,app[0]); + } + + /** + * When the template attribute is set, the nextmatch widget tries to load + * that template and to fetch the grid which is inside of it. It then calls + * + * @param {string} template_name Full template name in the form app.template[.template] + */ + set_template( template_name : string) { + const template = et2_createWidget("template", {"id": template_name}, this); + if (this.template) { + // Stop early to prevent unneeded processing, and prevent infinite + // loops if the server changes the template in get_rows + if (this.template == template_name) { + return; + } + + // Free the grid components - they'll be re-created as the template is processed + this.dataview.destroy(); + this.rowProvider.destroy(); + this.controller.destroy(); + + // Free any children from previous template + // They may get left behind because of how detached nodes are processed + // We don't use iterateOver because it checks sub-children + for (let i = this._children.length - 1; i >= 0; i--) { + const _node = this._children[i]; + if (_node != this.header) { + this.removeChild(_node); + _node.destroy(); + } + } + + // Clear this setting if it's the same as the template, or + // the columns will not be loaded + if (this.template == this.options.settings.columnselection_pref) { + this.options.settings.columnselection_pref = template_name; + } + this.dataview = new et2_dataview(this.innerDiv, this.egw()); + } + + // Create the template + if (template_name) + { + } + + if (!template) + { + this.egw().debug("error", "Error while loading definition template for " + + "nextmatch widget.",template_name); + return; + } + + if(this.options.disabled) + { + return; + } + + // Deferred parse function - template might not be fully loaded + const parse = function (template) { + // Keep the name of the template, as we'll free up the widget after parsing + this.template = template_name; + + // Fetch the grid element and parse it + const definitionGrid = template.getChildren()[0]; + if (definitionGrid && definitionGrid instanceof et2_grid) { + this._parseGrid(definitionGrid); + } else { + this.egw().debug("error", "Nextmatch widget expects a grid to be the " + + "first child of the defined template."); + return; + } + + // Free the template again, but don't remove it + setTimeout(function () { + template.destroy(); + }, 1); + + // Call the "setNextmatch" function of all registered + // INextmatchHeader widgets. This updates this.activeFilters.col_filters according + // to what's in the template. + this.iterateOver(function (_node) { + _node.setNextmatch(this); + }, this, et2_INextmatchHeader); + + // Set filters to current values + // TODO this.controller.setFilters(this.activeFilters); + + // If no data was sent from the server, and num_rows is 0, the nm will be empty. + // This triggers a cache check. + if (!this.options.settings.num_rows && this.controller) { + this.controller.update(); + } + + // Load the default sort order + if (this.options.settings.order && this.options.settings.sort) { + this.sortBy(this.options.settings.order, + this.options.settings.sort == "ASC", false); + } + + // Start auto-refresh + this._set_autorefresh(this._get_autorefresh()); + }; + + // Template might not be loaded yet, defer parsing + const promise = []; + template.loadingFinished(promise); + + // Wait until template (& children) are done + jQuery.when.apply(null, promise).done( + jQuery.proxy(function() { + parse.call(this, template); + if(!this.dynheight) + { + this.dynheight = this._getDynheight(); + } + this.dynheight.initialized = false; + this.resize(); + }, this) + ); + } + + // Some accessors to match conventions + set_hide_header( hide : boolean) + { + (hide ? this.header.div.hide() : this.header.div.show()); + } + + set_header_left( template : string) + { + this.header._build_header("left",template); + } + set_header_right( template : string) + { + this.header._build_header("right",template); + } + set_header_row( template : string) + { + this.header._build_header("row",template); + } + set_no_filter( bool, filter_name) + { + if(typeof filter_name == 'undefined') + { + filter_name = 'filter'; + } + this.options['no_'+filter_name] = bool; + + let filter = this.header[filter_name]; + if(filter) + { + filter.set_disabled(bool); + } + else if (bool) + { + filter = this.header._build_select(filter_name, 'select', + this.settings[filter_name], this.settings[filter_name+'_no_lang']); + } + } + set_no_filter2( bool) + { + this.set_no_filter(bool,'filter2'); + } + + /** + * Directly change filter value, with no server query. + * + * This allows the server app code to change filter value, and have it + * updated in the client UI. + * + * @param {String|number} value + */ + set_filter( value) + { + const update = this.update_in_progress; + this.update_in_progress = true; + + this.activeFilters.filter = value; + + // Update the header + this.header.setFilters(this.activeFilters); + + this.update_in_progress = update; + } + + /** + * Directly change filter2 value, with no server query. + * + * This allows the server app code to change filter2 value, and have it + * updated in the client UI. + * + * @param {String|number} value + */ + set_filter2( value) + { + const update = this.update_in_progress; + this.update_in_progress = true; + + this.activeFilters.filter2 = value; + + // Update the header + this.header.setFilters(this.activeFilters); + + this.update_in_progress = update; + } + + /** + * If nextmatch starts disabled, it will need a resize after being shown + * to get all the sizing correct. Override the parent to add the resize + * when enabling. + * + * @param {boolean} _value + */ + set_disabled(_value : boolean) + { + const previous = this.disabled; + super.set_disabled(_value); + + if(previous && !_value) + { + this.resize(); + } + } + + /** + * Actions are handled by the controller, so ignore these during init. + * + * @param {object} actions + */ + set_actions( actions : object[]) + { + if(actions != this.options.actions && this.controller != null && this.controller._actionManager) + { + for(let i = this.controller._actionManager.children.length - 1; i >= 0; i--) + { + this.controller._actionManager.children[i].remove(); + } + this.options.actions = actions; + this.options.settings.action_links = this.controller._actionLinks = this._get_action_links(actions); + + this.controller._initActions(actions); + } + } + + /** + * Switch view between row and tile. + * This should be followed by a call to change the template to match, which + * will cause a reload of the grid using the new settings. + * + * @param {string} view Either 'tile' or 'row' + */ + set_view(view : "tile" | "row") + { + // Restrict to the only 2 accepted values + if(view == 'tile') + { + this.view = 'tile'; + } + else + { + this.view = 'row'; + } + } + + /** + * Set a different / additional handler for dropped files. + * + * File dropping doesn't work with the action system, so we handle it in the + * nextmatch by linking automatically to the target row. This allows an additional handler. + * It should accept a row UID and a File[], and return a boolean Execute the default (link) action + * + * @param {String|Function} handler + */ + set_onfiledrop( handler) + { + this.options.onfiledrop = handler; + } + + /** + * Handle drops of files by linking to the row, if possible. + * + * HTML5 / native file drops conflict with jQueryUI draggable, which handles + * all our drop actions. So we side-step the issue by registering an additional + * drop handler on the rows parent. If the row/actions itself doesn't handle + * the drop, it should bubble and get handled here. + * + * @param {object} event + * @param {object} target + */ + handle_drop( event, target) + { + // Check to see if we can handle the link + // First, find the UID + const row = this.controller.getRowByNode(target); + const uid = row.uid || null; + + // Get the file information + let files = []; + if(event.originalEvent && event.originalEvent.dataTransfer && + event.originalEvent.dataTransfer.files && event.originalEvent.dataTransfer.files.length > 0) + { + files = event.originalEvent.dataTransfer.files; + } + else + { + return false; + } + + // Exectute the custom handler code + if (this.options.onfiledrop && !this.options.onfiledrop.call(this, uid, files)) + { + return false; + } + event.stopPropagation(); + event.preventDefault(); + + if(!row || !row.uid) return false; + + // Link the file to the row + // just use a link widget, it's all already done + const split = uid.split('::'); + const link_value = { + to_app: split.shift(), + to_id: split.join('::') + }; + // Create widget and mangle to our needs + const link = et2_createWidget("link-to", {value: link_value}, this); + link.loadingFinished(); + link.file_upload.set_drop_target(false); + + if(row.row.tr) + { + // Ignore most of the UI, just use the status indicators + const status = jQuery(document.createElement("div")) + .addClass('et2_link_to') + .width(row.row.tr.width()) + .position({my: "left top", at: "left top", of: row.row.tr}) + .append(link.status_span) + .append(link.file_upload.progress) + .appendTo(row.row.tr); + + // Bind to link event so we can remove when done + link.div.on('link.et2_link_to', function(e, linked) { + if(!linked) + { + jQuery("li.success", link.file_upload.progress) + .removeClass('success').addClass('validation_error'); + } + else + { + // Update row + link._parent.refresh(uid,'edit'); + } + // Fade out nicely + status.delay(linked ? 1 : 2000) + .fadeOut(500, function() { + link.destroy(); + status.remove(); + }); + + }); + } + + // Upload and link - this triggers the upload, which triggers the link, which triggers the cleanup and refresh + link.file_upload.set_value(files); + } + + getDOMNode( _sender?) + { + if (_sender == this || typeof _sender === 'undefined') + { + return this.div[0]; + } + if (_sender == this.header) + { + return this.header.div[0]; + } + for (let i = 0; i < this.columns.length; i++) + { + if (this.columns[i] && this.columns[i].widget && _sender == this.columns[i].widget) + { + return this.dataview.getHeaderContainerNode(i); + } + } + + // Let header have a chance + if(_sender && _sender._parent && _sender._parent == this) + { + return this.header.getDOMNode(_sender); + } + + return null; + } + + // Input widget + + /** + * Get the current 'value' for the nextmatch + */ + getValue( ) + { + const _ids = this.getSelection(); + + // Translate the internal uids back to server uids + const idsArr = _ids.ids; + for (let i = 0; i < idsArr.length; i++) + { + idsArr[i] = idsArr[i].split("::").pop(); + } + const value = { + "selected": idsArr + }; + jQuery.extend(value, this.activeFilters, this.value); + return value; + } + resetDirty( ) + {} + isDirty() { return typeof this.value !== 'undefined';} + isValid( ) { return true;} + set_value(_value) + { + this.value = _value; + } + + // Printing + /** + * Prepare for printing + * + * We check for un-loaded rows, and ask the user what they want to do about them. + * If they want to print them all, we ask the server and print when they're loaded. + */ + beforePrint( ) + { + // Add the class, if needed + this.div.addClass('print'); + + // Trigger resize, so we can fit on a page + this.dynheight.outerNode.css('max-width',this.div.css('max-width')); + this.resize(); + // Reset height to auto (after width resize) so there's no restrictions + this.dynheight.innerNode.css('height', 'auto'); + + // Check for rows that aren't loaded yet, or lots of rows + const range = this.controller._grid.getIndexRange(); + this.print.old_height = this.controller._grid._scrollHeight; + const loaded_count = range.bottom - range.top + 1; + const total = this.controller._grid.getTotalCount(); + + // Defer the printing to ask about columns & rows + const defer = jQuery.Deferred(); + + + let pref = this.options.settings.columnselection_pref; + if(pref.indexOf('nextmatch') == 0) + { + pref = 'nextmatch-'+pref; + } + const app = this.getInstanceManager().app; + + const columns = {}; + const columnMgr = this.dataview.getColumnMgr(); + pref += '_print'; + const columns_selected = []; + + // Get column names + for (let i = 0; i < columnMgr.columns.length; i++) + { + const col = columnMgr.columns[i]; + const widget = this.columns[i].widget; + let colName = this._getColumnName(widget); + + if(col.caption && col.visibility !== et2_dataview_grid.ET2_COL_VISIBILITY_ALWAYS_NOSELECT && + col.visibility !== et2_dataview_grid.ET2_COL_VISIBILITY_DISABLED) + { + columns[colName] = col.caption; + if(col.visibility === et2_dataview_grid.ET2_COL_VISIBILITY_VISIBLE) columns_selected.push(colName); + } + // Custom fields get listed separately + if(widget.instanceOf(et2_nextmatch_customfields)) + { + delete(columns[colName]); + colName = widget.id; + if(col.visibility === et2_dataview_grid.ET2_COL_VISIBILITY_VISIBLE && ! + jQuery.isEmptyObject((widget).customfields) + ) + { + columns[colName] = col.caption; + for(let field_name in (widget).customfields) + { + columns[et2_nextmatch_customfields.PREFIX+field_name] = " - "+(widget).customfields[field_name].label; + if(widget.options.fields[field_name] && columns_selected.indexOf(colName) >= 0) + { + columns_selected.push(et2_nextmatch_customfields.PREFIX+field_name); + } + } + } + } + } + + // Preference exists? Set it now + if(this.egw().preference(pref,app)) + { + this.set_columns(jQuery.extend([],this.egw().preference(pref,app))); + } + + const callback = jQuery.proxy(function (button, value) { + if (button === et2_dialog.CANCEL_BUTTON) { + // Give dialog a chance to close, or it will be in the print + window.setTimeout(function () { + defer.reject(); + }, 0); + return; + } + + // Set CSS for orientation + this.div.addClass(value.orientation); + this.egw().set_preference(app, pref + '_orientation', value.orientation); + + + // Try to tell browser about orientation + const css = '@page { size: ' + value.orientation + '; }', + head = document.head || document.getElementsByTagName('head')[0], + style = document.createElement('style'); + + style.type = 'text/css'; + style.media = 'print'; + + // @ts-ignore + if (style.styleSheet) { + // @ts-ignore + style.styleSheet.cssText = css; + } else { + style.appendChild(document.createTextNode(css)); + } + + head.appendChild(style); + this.print.orientation_style = style; + + // Trigger resize, so we can fit on a page + this.dynheight.outerNode.css('max-width', this.div.css('max-width')); + + // Handle columns + this.set_columns(value.columns); + this.egw().set_preference(app, pref, value.columns); + + let rows = parseInt(value.row_count); + if (rows > total) { + rows = total; + } + + // If they want the whole thing, style it as all + if (button === et2_dialog.OK_BUTTON && rows == this.controller._grid.getTotalCount()) { + // Add the class, gives more reliable sizing + this.div.addClass('print'); + // Show it all + jQuery('.egwGridView_scrollarea', this.div).css('height', 'auto'); + } + // We need more rows + if (button === 'dialog[all]' || rows > loaded_count) { + let count = 0; + let fetchedCount = 0; + let cancel = false; + const nm = this; + const dialog = et2_dialog.show_dialog( + // Abort the long task if they canceled the data load + function () { + count = total; + cancel = true; + window.setTimeout(function () { + defer.reject(); + }, 0); + }, + egw.lang('Loading'), egw.lang('please wait...'), {}, [ + {"button_id": et2_dialog.CANCEL_BUTTON, "text": 'cancel', id: 'dialog[cancel]', image: 'cancel'} + ] + ); + + // dataFetch() is asynchronous, so all these requests just get fired off... + // 200 rows chosen arbitrarily to reduce requests. + do { + const ctx = { + "self": this.controller, + "start": count, + "count": Math.min(rows, 200), + "lastModification": this.controller._lastModification + }; + if (nm.controller.dataStorePrefix) { + // @ts-ignore + ctx.prefix = nm.controller.dataStorePrefix; + } + nm.controller.dataFetch({start: count, num_rows: Math.min(rows, 200)}, function (data) { + // Keep track + if (data && data.order) { + fetchedCount += data.order.length; + } + nm.controller._fetchCallback.apply(this, arguments); + + if (fetchedCount >= rows) { + if (cancel) { + dialog.destroy(); + defer.reject(); + return; + } + // Use CSS to hide all but the requested rows + // Prevents us from showing more than requested, if actual height was less than average + nm.print_row_selector = ".egwGridView_grid > tbody > tr:not(:nth-child(-n+" + rows + "))"; + egw.css(nm.print_row_selector, 'display: none'); + + // No scrollbar in print view + jQuery('.egwGridView_scrollarea', this.div).css('overflow-y', 'hidden'); + // Show it all + jQuery('.egwGridView_scrollarea', this.div).css('height', 'auto'); + + // Grid needs to redraw before it can be printed, so wait + window.setTimeout(jQuery.proxy(function () { + dialog.destroy(); + + // Should be OK to print now + defer.resolve(); + }, nm), et2_dataview_grid.ET2_GRID_INVALIDATE_TIMEOUT); + + } + + }, ctx); + count += 200; + } while (count < rows); + nm.controller._grid.setScrollHeight(nm.controller._grid.getAverageHeight() * (rows + 1)); + } else { + // Don't need more rows, limit to requested and finish + + // Show it all + jQuery('.egwGridView_scrollarea', this.div).css('height', 'auto'); + + // Use CSS to hide all but the requested rows + // Prevents us from showing more than requested, if actual height was less than average + this.print_row_selector = ".egwGridView_grid > tbody > tr:not(:nth-child(-n+" + rows + "))"; + egw.css(this.print_row_selector, 'display: none'); + + // No scrollbar in print view + jQuery('.egwGridView_scrollarea', this.div).css('overflow-y', 'hidden'); + // Give dialog a chance to close, or it will be in the print + window.setTimeout(function () { + defer.resolve(); + }, 0); + } + }, this); + var value = { + content: { + row_count: Math.min(100,total), + columns: this.egw().preference(pref,app) || columns_selected, + orientation: this.egw().preference(pref+'_orientation',app) + }, + sel_options: { + columns: columns + } + }; + this._create_print_dialog.call(this, value, callback); + + return defer; + } + + /** + * Create and show the print dialog, which calls the provided callback when + * done. Broken out for overriding if needed. + * + * @param {Object} value Current settings and preferences, passed to the dialog for + * the template + * @param {Object} value.content + * @param {Object} value.sel_options + * + * @param {function(int, Object)} callback - Process the dialog response, + * format things according to the specified orientation and fetch any needed + * rows. + * + */ + _create_print_dialog(value, callback) + { + let base_url = this.getInstanceManager().template_base_url; + if (base_url.substr(base_url.length - 1) == '/') base_url = base_url.slice (0, -1); // otherwise we generate a url //api/templates, which is wrong + const tab = this.get_tab_info(); + // Get title for print dialog from settings or tab, if available + const title = this.options.settings.label ? this.options.settings.label : (tab ? tab.label : ''); + const dialog = et2_createWidget("dialog", { + // If you use a template, the second parameter will be the value of the template, as if it were submitted. + callback: callback, // return false to prevent dialog closing + buttons: et2_dialog.BUTTONS_OK_CANCEL, + title: this.egw().lang('Print') + ' ' + this.egw().lang(title), + template: this.egw().link(base_url + '/api/templates/default/nm_print_dialog.xet'), + value: value + }); + } + + /** + * Try to clean up the mess we made getting ready for printing + * in beforePrint() + */ + afterPrint( ) + { + + this.div.removeClass('print landscape portrait'); + jQuery(this.print.orientation_style).remove(); + delete this.print.orientation_style; + + // Put scrollbar back + jQuery('.egwGridView_scrollarea',this.div).css('overflow-y',''); + + // Correct size of grid, and trigger resize to fix it + this.controller._grid.setScrollHeight(this.print.old_height); + delete this.print.old_height; + + // Remove CSS rule hiding extra rows + if(this.print.row_selector) + { + egw.css(this.print.row_selector, ''); + delete this.print.row_selector; + } + + // Restore columns + let pref: string | object | boolean = []; + const app = this.getInstanceManager().app; + if(this.options.settings.columnselection_pref.indexOf('nextmatch') == 0) + { + pref = egw.preference(this.options.settings.columnselection_pref, app); + } + else + { + // 'nextmatch-' prefix is there in preference name, but not in setting, so add it in + pref = egw.preference("nextmatch-"+this.options.settings.columnselection_pref, app); + } + if(pref) + { + if(typeof pref === 'string') pref = (pref).split(','); + // @ts-ignore + this.set_columns(pref,app); + } + this.dynheight.outerNode.css('max-width','inherit'); + this.resize(); + } +} +et2_register_widget(et2_nextmatch, ["nextmatch"]); + +/** + * Standard nextmatch header bar, containing filters, search, record count, letter filters, etc. + * + * Unable to use an existing template for this because parent (nm) doesn't, and template widget doesn't + * actually load templates from the server. + * @augments et2_DOMWidget + */ +class et2_nextmatch_header_bar extends et2_DOMWidget implements et2_INextmatchHeader +{ + static readonly _attributes: any = { + "filter_label": { + "name": "Filter label", + "type": "string", + "description": "Label for filter", + "default": "", + "translate": true + }, + "filter_help": { + "name": "Filter help", + "type": "string", + "description": "Help message for filter", + "default": "", + "translate": true + }, + "filter": { + "name": "Filter value", + "type": "any", + "description": "Current value for filter", + "default": "" + }, + "no_filter": { + "name": "No filter", + "type": "boolean", + "description": "Remove filter", + "default": false + } + }; + headers: {id: string}[] | et2_widget[]; + et2_searchbox: et2_inputWidget; + private favorites: et2_DOMWidget; // Actually favorite + + private nextmatch: et2_nextmatch; + div: JQuery; + private update_in_progress: boolean; + + header_div: JQuery; + private header_row: JQuery; + private filter_div: JQuery; + private row_div: JQuery; + private fav_span: JQuery; + private toggle_header: JQuery; + lettersearch: JQuery; + + private delete_action: JQuery; + private action_header: JQuery; + + private search_box: JQuery; + private category: any; + private filter: et2_selectbox; + private filter2: et2_selectbox; + private right_div: JQuery; + private count: JQuery; + private count_total: JQuery; + + /** + * Constructor + * + * @param _parent + * @param _attrs + * @param _child + */ + constructor(_parent : et2_nextmatch, _attrs? : WidgetConfig, _child? : object) + { + super(_parent, [_parent,_parent.options.settings], ClassWithAttributes.extendAttributes(et2_nextmatch_header_bar._attributes, _child || {})); + this.nextmatch = _parent; + this.div = jQuery(document.createElement("div")) + .addClass("nextmatch_header"); + this._createHeader(); + + // Flag to avoid loops while updating filters + this.update_in_progress = false; + } + + destroy( ) + { + this.nextmatch = null; + + super.destroy(); + this.div = null; + } + + setNextmatch( nextmatch) + { + const create_once = (this.nextmatch == null); + this.nextmatch = nextmatch; + if(create_once) + { + this._createHeader(); + } + + // Bind row count + this.nextmatch.dataview.grid.setInvalidateCallback(function () { + this.count_total.text(this.nextmatch.dataview.grid.getTotalCount() + ""); + }, this); + } + + /** + * Actions are handled by the controller, so ignore these + * + * @param {object} actions + */ + set_actions( actions : object[]) + {} + + _createHeader( ) + { + + let button; + const self = this; + const nm_div = this.nextmatch.getDOMNode(); + const settings = this.nextmatch.options.settings; + + this.div.prependTo(nm_div); + + // Left & Right (& row) headers + this.headers = [ + {id:this.nextmatch.options.header_left}, + {id:this.nextmatch.options.header_right}, + {id:this.nextmatch.options.header_row} + ]; + + // The rest of the header + this.header_div = this.row_div = jQuery(document.createElement("div")) + .addClass("nextmatch_header_row") + .appendTo(this.div); + this.filter_div = jQuery(document.createElement("div")) + .addClass('filtersContainer') + .appendTo(this.row_div); + + // Search + this.search_box = jQuery(document.createElement("div")) + .addClass('search') + .prependTo(egwIsMobile()?this.nextmatch.getDOMNode():this.row_div); + + // searchbox widget options + const searchbox_options = { + id: "search", + overlay: (typeof settings.searchbox != 'undefined' && typeof settings.searchbox.overlay != 'undefined') ? settings.searchbox.overlay : false, + onchange: function () { + self.nextmatch.applyFilters({search: this.get_value()}); + }, + value: settings.search, + fix: !egwIsMobile() + }; + // searchbox widget + this.et2_searchbox = et2_createWidget('searchbox', searchbox_options,this); + + // Set activeFilters to current value + this.nextmatch.activeFilters.search = settings.search; + + this.et2_searchbox.set_value(settings.search); + /** + * Mobile theme specific part for nm header + * nm header has very different behaivior for mobile theme and basically + * it has its own markup separately from nm header in normal templates. + */ + if (egwIsMobile()) + { + this.search_box.addClass('nm-mob-header'); + jQuery(this.div).css({display:'inline-block'}).addClass('nm_header_hide'); + + //indicates appname in header + jQuery(document.createElement('div')) + .addClass('nm_appname_header') + .text(egw.lang(egw.app_name())) + .appendTo(this.search_box); + + this.delete_action = jQuery(document.createElement('div')) + .addClass('nm_delete_action') + .prependTo(this.search_box); + // toggle header + // add new button + this.fav_span = jQuery(document.createElement('div')) + .addClass('nm_favorites_div') + .prependTo(this.search_box); + // toggle header menu + this.toggle_header = jQuery(document.createElement('button')) + .addClass('nm_toggle_header') + .click(function(){ + jQuery(self.div).toggleClass('nm_header_hide'); + jQuery(this).toggleClass('nm_toggle_header_on'); + window.setTimeout(function(){self.nextmatch.resize();},800); + }) + .prependTo(this.search_box); + // Context menu + this.action_header = jQuery(document.createElement('button')) + .addClass('nm_action_header') + .hide() + .click (function(e){ + // @ts-ignore + jQuery('tr.selected',self.nextmatch.getDOMNode()).trigger({type:'contextmenu',which:3,originalEvent:e}); + }) + .prependTo(this.search_box); + } + + // Add category + if(!settings.no_cat) { + if (typeof settings.cat_id_label == 'undefined') settings.cat_id_label = ''; + this.category = this._build_select('cat_id', settings.cat_is_select ? + 'select' : 'select-cat', settings.cat_id, settings.cat_is_select !== true, { + multiple: false, + tags: true, + class: "select-cat", + value_class: settings.cat_id_class + }); + } + + // Filter 1 + if(!settings.no_filter) { + this.filter = this._build_select('filter', 'select', settings.filter, settings.filter_no_lang); + } + + // Filter 2 + if(!settings.no_filter2) { + this.filter2 = this._build_select('filter2', 'select', settings.filter2, + settings.filter2_no_lang, { + multiple: false, + tags: settings.filter2_tags, + class: "select-cat", + value_class: settings.filter2_class + }); + } + + // Other stuff + this.right_div = jQuery(document.createElement("div")) + .addClass('header_row_right').appendTo(this.row_div); + + // Record count + this.count = jQuery(document.createElement("span")) + .addClass("header_count ui-corner-all"); + + // Need to figure out how to update this as grid scrolls + // this.count.append("? - ? ").append(egw.lang("of")).append(" "); + this.count_total = jQuery(document.createElement("span")) + .appendTo(this.count) + .text(settings.total + ""); + this.count.prependTo(this.right_div); + + // Favorites + this._setup_favorites(settings['favorites']); + + // Export + if(typeof settings.csv_fields != "undefined" && settings.csv_fields != false) + { + let definition = settings.csv_fields; + if(settings.csv_fields === true) + { + definition = egw.preference('nextmatch-export-definition', this.nextmatch.egw().app_name()); + } + let button = et2_createWidget("buttononly", {id: "export", "statustext": "Export", image: "download", "background_image": true}, this); + jQuery(button.getDOMNode()) + .click(this.nextmatch, function(event) { + // @ts-ignore + egw_openWindowCentered2( egw.link('/index.php', { + 'menuaction': 'importexport.importexport_export_ui.export_dialog', + 'appname': event.data.egw().getAppName(), + 'definition': definition + }), '_blank', 850, 440, 'yes'); + }); + } + + // Another place to customize nextmatch + this.header_row = jQuery(document.createElement("div")) + .addClass('header_row').appendTo(this.right_div); + + // Letter search + const current_letter = this.nextmatch.options.settings.searchletter ? + this.nextmatch.options.settings.searchletter : + (this.nextmatch.activeFilters ? this.nextmatch.activeFilters.searchletter : false); + if(this.nextmatch.options.settings.lettersearch || current_letter) + { + this.lettersearch = jQuery(document.createElement("table")) + .addClass('nextmatch_lettersearch') + .css("width", "100%") + .appendTo(this.div); + const tbody = jQuery(document.createElement("tbody")).appendTo(this.lettersearch); + const row = jQuery(document.createElement("tr")).appendTo(tbody); + + // Capitals, A-Z + const letters = this.egw().lang('ABCDEFGHIJKLMNOPQRSTUVWXYZ').split(''); + for(let i in letters) { + button = jQuery(document.createElement("td")) + .addClass("lettersearch") + .appendTo(row) + .attr("id", letters[i]) + .text(letters[i]); + if(letters[i] == current_letter) button.addClass("lettersearch_active"); + } + button = jQuery(document.createElement("td")) + .addClass("lettersearch") + .appendTo(row) + .attr("id", "") + .text(egw.lang("all")); + if(!current_letter) button.addClass("lettersearch_active"); + + this.lettersearch.click(this.nextmatch, function(event) { + // this is the lettersearch table + jQuery("td",this).removeClass("lettersearch_active"); + jQuery(event.target).addClass("lettersearch_active"); + event.data.applyFilters({searchletter: event.target.id || false}); + }); + // Set activeFilters to current value + this.nextmatch.activeFilters.searchletter = current_letter; + } + // Apply letter search preference + const lettersearch_preference = "nextmatch-" + this.nextmatch.options.settings.columnselection_pref + "-lettersearch"; + if(this.lettersearch && !egw.preference(lettersearch_preference,this.nextmatch.egw().app_name())) + { + this.lettersearch.hide(); + } + } + + + /** + * Build & bind to a sub-template into the header + * + * @param {string} location One of left, right, or row + * @param {string} template_name Name of the template to load into the location + */ + _build_header( location : "left" | "right" | "row", template_name : string) + { + const id = location == "left" ? 0 : (location == "right" ? 1 : 2); + const existing = this.headers[id]; + // @ts-ignore + if(existing && existing._type) + { + if(existing.id == template_name) return; + (existing).destroy(); + this.headers[id] = null; + } + if(!template_name) return; + + // Load the template + const self = this; + const header = et2_createWidget("template", {"id": template_name}, this); + this.headers[id] = header; + const deferred = []; + header.loadingFinished(deferred); + + // Wait until all child widgets are loaded, then bind + jQuery.when.apply(jQuery,deferred).then(function() { + // fix order in DOM by reattaching templates in correct position + switch (id) { + case 0: // header_left: prepend + jQuery(header.getDOMNode()).prependTo(self.header_div); + break; + case 1: // header_right: before favorites and count + jQuery(header.getDOMNode()).prependTo(self.header_div.find('div.header_row_right')); + break; + case 2: // header_row: after search + window.setTimeout(function(){ // otherwise we might end up after filters + jQuery(header.getDOMNode()).insertAfter(self.header_div.find('div.search')); + }, 1); + break; + } + self._bindHeaderInput(header); + }); + } + + /** + * Build the selectbox filters in the header bar + * Sets value, options, labels, and change handlers + * + * @param {string} name + * @param {string} type + * @param {string} value + * @param {string} lang + * @param {object} extra + */ + _build_select( name: string, type: string, value: string, lang: string|boolean, extra?: object) : et2_selectbox + { + const widget_options = jQuery.extend({ + "id": name, + "label": this.nextmatch.options.settings[name + "_label"], + "no_lang": lang, + "disabled": this.nextmatch.options['no_' + name] + }, extra); + + // Set select options + // Check in content for options- + const mgr = this.nextmatch.getArrayMgr("content"); + let options = mgr.getEntry("options-" + name); + // Look in sel_options + if(!options) options = this.nextmatch.getArrayMgr("sel_options").getEntry(name); + // Check parent sel_options, because those are usually global and don't get passed down + if(!options) options = this.nextmatch.getArrayMgr("sel_options").getParentMgr()?.getEntry(name); + // Sometimes legacy stuff puts it in here + if(!options) options = mgr.getEntry('rows[sel_options]['+name+']'); + + // Maybe in a row, and options got stuck in ${row} instead of top level + const row_stuck = ['${row}', '{$row}']; + for(let i = 0; !options && i < row_stuck.length; i++) + { + let row_id = ''; + if((!options || options.length == 0) && ( + // perspectiveData.row in nm, data["${row}"] in an auto-repeat grid + this.nextmatch.getArrayMgr("sel_options").perspectiveData.row || this.nextmatch.getArrayMgr("sel_options").data[row_stuck[i]])) + { + row_id = name.replace(/[0-9]+/,row_stuck[i]); + options = this.nextmatch.getArrayMgr("sel_options").getEntry(row_id); + if(!options) + { + row_id = row_stuck[i] + "["+name+"]"; + options = this.nextmatch.getArrayMgr("sel_options").getEntry(row_id); + } + } + if(options) + { + this.egw().debug('warn', 'Nextmatch filter options in a weird place - "%s". Should be in sel_options[%s].',row_id,name); + } + } + // Legacy: Add in 'All' option for cat_id, if not provided. + if(name == 'cat_id' && options != null && (typeof options[''] == 'undefined' && typeof options[0] != 'undefined' && options[0].value != '')) + { + widget_options.empty_label = this.egw().lang('All categories'); + } + + // Create widget + const select = et2_createWidget(type, widget_options, this); + + if(options) select.set_select_options(options); + + // Set value + select.set_value(value); + + // Set activeFilters to current value + this.nextmatch.activeFilters[select.id] = select.get_value(); + + // Set onChange + const input = select.input; + + // Tell framework to ignore, or it will reset it to ''/empty when it does loadingFinished() + select.attributes.select_options.ignore = true; + + if (this.nextmatch.options.settings[name+"_onchange"]) + { + // Get the onchange function string + let onchange = this.nextmatch.options.settings[name + "_onchange"]; + + // Real submits cause all sorts of problems + if(onchange.match(/this\.form\.submit/)) + { + this.egw().debug("warn","%s tries to submit form, which is not allowed. Filter changes automatically refresh data with no reload.",name); + onchange = onchange.replace(/this\.form\.submit\([^)]*\);?/,'return true;'); + } + + // Connect it to the onchange event of the input element - may submit + select.change = et2_compileLegacyJS(onchange, this.nextmatch, select.getInputNode()); + this._bindHeaderInput(select); + } + else // default request changed rows with new filters, previous this.form.submit() + { + input.change(this.nextmatch, function(event) { + const set = {}; + set[name] = select.getValue(); + event.data.applyFilters(set); + }); + } + return select; + } + + /** + * Set up the favorites UI control + * + * @param filters Array|boolean The nextmatch setting for favorites. Either true, or a list of + * additional fields/settings to add in to the favorite. + */ + _setup_favorites( filters) + { + if(typeof filters == "undefined" || filters === false) + { + // No favorites configured + return; + } + + const list = et2_csvSplit(this.options.get_rows, 2, "."); + const widget_options = { + default_pref: "nextmatch-" + this.nextmatch.options.settings.columnselection_pref + "-favorite", + app: list[0], + filters: filters, + sidebox_target: 'favorite_sidebox_' + list[0] + }; + this.favorites = et2_createWidget('favorites', widget_options, this); + + // Add into header + jQuery(this.favorites.getDOMNode(this.favorites)).prependTo(egwIsMobile()?this.search_box.find('.nm_favorites_div').show():this.right_div); + } + + /** + * Updates all the filter elements in the header + * + * Does not actually refresh the data, just sets values to match those given. + * Called by et2_nextmatch.applyFilters(). + * + * @param filters Array Key => Value pairs of current filters + */ + setFilters( filters) + { + + // Avoid loops cause by change events + if(this.update_in_progress) return; + this.update_in_progress = true; + + // Use an array mgr to hande non-simple IDs + const mgr = new et2_arrayMgr(filters); + + this.iterateOver(function(child) { + // Skip favorites, don't want them in the filter + if(typeof child.id != "undefined" && child.id.indexOf("favorite") == 0) return; + + let value : string | object = ''; + if(typeof child.set_value != "undefined" && child.id) + { + value = mgr.getEntry(child.id); + if (value == null) value = ''; + /** + * Sometimes a filter value is not in current options. This can + * happen in a saved favorite, for example, or if server changes + * some filter options, and the order doesn't work out. The normal behaviour + * is to warn & not set it, but for nextmatch we'll just add it + * in, and let the server either set it properly, or ignore. + */ + if(value && typeof value != 'object' && child.instanceOf(et2_selectbox)) + { + let found = typeof child.options.select_options[value] != 'undefined'; + // options is array of objects with attribute value&label + if (jQuery.isArray(child.options.select_options)) + { + for(let o=0; o < child.options.select_options.length; ++o) + { + if (child.options.select_options[o].value == value) + { + found = true; + break; + } + } + } + if (!found) + { + const old_options = child.options.select_options; + // Actual label is not available, obviously, or it would be there + old_options[value] = child.egw().lang("Loading"); + child.set_select_options(old_options); + } + } + child.set_value(value); + } + if(typeof child.get_value == "function" && child.id) + { + // Put data in the proper place + let target = this; + value = child.get_value(); + + // Split up indexes + const indexes = child.id.replace(/[/g, '[').split('['); + + for(let i = 0; i < indexes.length; i++) + { + indexes[i] = indexes[i].replace(/]/g,'').replace(']',''); + if (i < indexes.length-1) + { + if(typeof target[indexes[i]] == "undefined") target[indexes[i]] = {}; + target = target[indexes[i]]; + } + else + { + target[indexes[i]] = value; + } + } + } + }, filters); + + // Letter search + if(this.nextmatch.options.settings.lettersearch) + { + jQuery("td",this.lettersearch).removeClass("lettersearch_active"); + jQuery(filters.searchletter ? "td#"+filters.searchletter : "td.lettersearch[id='']",this.lettersearch).addClass("lettersearch_active"); + + // Set activeFilters to current value + filters.searchletter = jQuery("td.lettersearch_active",this.lettersearch).attr("id") || false; + } + + // Reset flag + this.update_in_progress = false; + } + + /** + * Help out nextmatch / widget stuff by checking to see if sender is part of header + * + * @param {et2_widget} _sender + */ + getDOMNode( _sender) + { + const filters = [this.category, this.filter, this.filter2]; + for(let i = 0; i < filters.length; i++) + { + if(_sender == filters[i]) + { + // Give them the filter div + return this.filter_div[0]; + } + } + if(_sender == this.et2_searchbox) return this.search_box[0]; + if(_sender.id == 'export') return this.right_div[0]; + + if(_sender && _sender._type == "template") + { + for(let i = 0; i < this.headers.length; i++) + { + if(_sender.id == this.headers[i].id && _sender._parent == this) return i == 2 ? this.header_row[0] : this.header_div[0]; + } + } + return null; + } + + /** + * Bind all the inputs in the header sub-templates to update the filters + * on change, and update current filter with the inputs' current values + * + * @param {et2_template} sub_header + */ + _bindHeaderInput( sub_header) + { + const header = this; + + const bind_change = function (_widget) { + // Previously set change function + const widget_change = _widget.change; + + let change = function (_node) { + // Call previously set change function + const result = widget_change.call(_widget, _node); + + // Update filters, if we're not already doing so + if ((result || typeof result === 'undefined') && _widget.isDirty() && !header.update_in_progress) { + // Update dirty + _widget._oldValue = _widget.getValue(); + + // Widget will not have an entry in getValues() because nulls + // are not returned, we remove it from activeFilters + if (_widget._oldValue == null) { + const path = _widget.getArrayMgr('content').explodeKey(_widget.id); + if (path.length > 0) { + let entry = header.nextmatch.activeFilters; + let i = 0; + for (; i < path.length - 1; i++) { + entry = entry[path[i]]; + } + delete entry[path[i]]; + } + header.nextmatch.applyFilters(header.nextmatch.activeFilters); + } else { + // Not null is easy, just get values + const value = this.getInstanceManager().getValues(sub_header); + header.nextmatch.applyFilters(value[header.nextmatch.id]); + } + } + // In case this gets bound twice, it's important to return + return true; + }; + + _widget.change = change; + + // Set activeFilters to current value + // Use an array mgr to hande non-simple IDs + var value = {}; + value[_widget.id] = _widget._oldValue = _widget.getValue(); + const mgr = new et2_arrayMgr(value); + jQuery.extend(true, this.nextmatch.activeFilters, mgr.data); + }; + if(sub_header.instanceOf(et2_inputWidget)) + { + bind_change.call(this, sub_header); + } + else + { + sub_header.iterateOver(bind_change, this, et2_inputWidget); + } + } +} +et2_register_widget(et2_nextmatch_header_bar, ["nextmatch_header_bar"]); + +/** + * Classes for the nextmatch sortheaders etc. + * + * @augments et2_baseWidget + */ +export class et2_nextmatch_header extends et2_baseWidget implements et2_INextmatchHeader +{ + static readonly _attributes: any = { + "label": { + "name": "Caption", + "type": "string", + "description": "Caption for the nextmatch header", + "translate": true + } + }; + protected labelNode: JQuery; + protected nextmatch: et2_nextmatch; + private label: string; + + /** + * Constructor + * + * @memberOf et2_nextmatch_header + */ + constructor(_parent?, _attrs? : WidgetConfig, _child? : object) { + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_nextmatch_header._attributes, _child || {})); + + + this.labelNode = jQuery(document.createElement("span")); + this.nextmatch = null; + + this.setDOMNode(this.labelNode[0]); + } + + /** + * Set nextmatch is the function which has to be implemented for the + * et2_INextmatchHeader interface. + * + * @param {et2_nextmatch} _nextmatch + */ + setNextmatch( _nextmatch) + { + this.nextmatch = _nextmatch; + } + + set_label( _value) + { + this.label = _value; + + this.labelNode.text(_value); + + // add class if label is empty + this.labelNode.toggleClass('et2_label_empty', !_value); + } +} +et2_register_widget(et2_nextmatch_header, ['nextmatch-header']); + +/** + * Extend header to process customfields + * + * @augments et2_customfields_list + * + * TODO This should extend customfield widget when it's ready, put the whole column in constructor() back too + */ +export class et2_nextmatch_customfields extends et2_customfields_list implements et2_INextmatchHeader +{ + static readonly _attributes: any = { + 'customfields': { + 'name': 'Custom fields', + 'description': 'Auto filled' + }, + 'fields': { + 'name': "Visible fields", + "description": "Auto filled" + } + }; + private nextmatch: et2_nextmatch; + + /** + * Constructor + * + * @memberOf et2_nextmatch_customfields + */ + constructor(_parent?, _attrs? : WidgetConfig, _child? : object) { + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_nextmatch_customfields._attributes, _child || {})); + + // Specifically take the whole column + this.table.css("width", "100%"); + } + + destroy( ) + { + this.nextmatch = null; + super.destroy(); + } + + transformAttributes( _attrs) + { + super.transformAttributes(_attrs); + + // Add in settings that are objects + if(!_attrs.customfields) + { + // Check for custom stuff (unlikely) + let data = this.getArrayMgr("modifications").getEntry(this.id); + // Check for global settings + if(!data) data = this.getArrayMgr("modifications").getRoot().getEntry('~custom_fields~', true); + for(let key in data) + { + if(typeof data[key] === 'object' && ! _attrs[key]) _attrs[key] = data[key]; + } + } + } + + setNextmatch( _nextmatch) + { + this.nextmatch = _nextmatch; + this.loadFields(); + } + + /** + * Build widgets for header - sortable for numeric, text, etc., filterables for selectbox, radio + */ + loadFields( ) + { + if(this.nextmatch == null) + { + // not ready yet + return; + } + let columnMgr = this.nextmatch.dataview.getColumnMgr(); + let nm_column = null; + const set_fields = {}; + for(let i = 0; i < this.nextmatch.columns.length; i++) + { + // @ts-ignore + if(this.nextmatch.columns[i].widget == this) + { + nm_column = columnMgr.columns[i]; + break; + } + } + if(!nm_column) return; + + // Check for global setting changes (visibility) + const global_data = this.getArrayMgr("modifications").getRoot().getEntry('~custom_fields~'); + if(global_data != null && global_data.fields) this.options.fields = global_data.fields; + + const apps = egw.link_app_list(); + for(let field_name in this.options.customfields) + { + const field = this.options.customfields[field_name]; + const cf_id = et2_customfields_list.PREFIX + field_name; + + + if(this.rows[field_name]) continue; + + // Table row + const row = jQuery(document.createElement("tr")) + .appendTo(this.tbody); + const cf = jQuery(document.createElement("td")) + .appendTo(row); + this.rows[cf_id] = cf[0]; + + // Create widget by type + let widget = null; + if(field.type == 'select' || field.type == 'select-account') + { + if(field.values && typeof field.values[''] !== 'undefined') + { + delete(field.values['']); + } + widget = et2_createWidget( + field.type == 'select-account' ? 'nextmatch-accountfilter' : "nextmatch-filterheader", + { + id: cf_id, + empty_label: field.label, + select_options: field.values + }, + this + ); + } + else if (apps[field.type]) + { + widget = et2_createWidget("nextmatch-entryheader", { + id: cf_id, + only_app: field.type, + blur: field.label + }, this); + } + else + { + widget = et2_createWidget("nextmatch-sortheader", { + id: cf_id, + label: field.label + }, this); + } + + // If this is already attached, widget needs to be finished explicitly + if(this.isAttached() && !widget.isAttached()) + { + widget.loadingFinished(); + } + // Check for column filter + if(!jQuery.isEmptyObject(this.options.fields) && ( + this.options.fields[field_name] == false || typeof this.options.fields[field_name] == 'undefined')) + { + cf.hide(); + } + else if (jQuery.isEmptyObject(this.options.fields)) + { + // If we're showing it make sure it's set, but only after + set_fields[field_name] = true; + } + } + jQuery.extend(this.options.fields, set_fields); + } + + /** + * Override parent so we can update the nextmatch row too + * + * @param {array} _fields + */ + set_visible( _fields) + { + super.set_visible(_fields); + + // Find data row, and do it too + const self = this; + if(this.nextmatch) + { + this.nextmatch.iterateOver( + function(widget) { + if(widget == self) return; + widget.set_visible(_fields); + }, this, et2_customfields_list + ); + } + } + + /** + * Provide own column caption (column selection) + * + * If only one custom field, just use that, otherwise use "custom fields" + */ + _genColumnCaption( ) + { + return egw.lang("Custom fields"); + } + + /** + * Provide own column naming, including only selected columns - only useful + * to nextmatch itself, not for sending server-side + */ + _getColumnName( ) + { + let name = this.id; + const visible = []; + for(var field_name in this.options.customfields) + { + if(jQuery.isEmptyObject(this.options.fields) || this.options.fields[field_name] == true) + { + visible.push(et2_customfields_list.PREFIX + field_name); + jQuery(this.rows[field_name]).show(); + } + else if (typeof this.rows[field_name] != "undefined") + { + jQuery(this.rows[field_name]).hide(); + } + } + + if(visible.length) { + name +="_"+ visible.join("_"); + } + else if (this.rows) + { + // None hidden means all visible + jQuery(this.rows[field_name]).parent().parent().children().show(); + } + + // Update global custom fields column(s) - widgets will check on their own + + // Check for custom stuff (unlikely) + let data = this.getArrayMgr("modifications").getEntry(this.id); + // Check for global settings + if(!data) data = this.getArrayMgr("modifications").getRoot().getEntry('~custom_fields~', true) || {}; + if(!data.fields) data.fields = {}; + for(let field in this.options.customfields) + { + data.fields[field] = (this.options.fields == null || typeof this.options.fields[field] == 'undefined' ? false : this.options.fields[field]); + } + return name; + } +} +et2_register_widget(et2_nextmatch_customfields, ['nextmatch-customfields']); + +/** + * @augments et2_nextmatch_header + */ +// @ts-ignore +export class et2_nextmatch_sortheader extends et2_nextmatch_header implements et2_INextmatchSortable +{ + static readonly _attributes: any = { + "sortmode": { + "name": "Sort order", + "type": "string", + "description": "Default sort order", + "translate": false + } + }; + legacyOptions: ['sortmode']; + private sortmode: string; + + /** + * Constructor + * + * @memberOf et2_nextmatch_sortheader + */ + constructor(_parent?, _attrs? : WidgetConfig, _child? : object) { + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_nextmatch_sortheader._attributes, _child || {})); + + this.sortmode = "none"; + + this.labelNode.addClass("nextmatch_sortheader none"); + } + + click( _event ) + { + if (this.nextmatch && super.click( _event )) + { + // Send default sort mode if not sorted, otherwise send undefined to calculate + this.nextmatch.sortBy(this.id, this.sortmode == "none" ? !(this.options.sortmode.toUpperCase() == "DESC") : undefined); + return true; + } + + return false; + } + + /** + * Wrapper to join up interface * framework + * + * @param {string} _mode + */ + set_sortmode(_mode) + { + // Set via nextmatch after setup + if(this.nextmatch) return; + + this.setSortmode(_mode); + } + + /** + * Function which implements the et2_INextmatchSortable function. + * + * @param {string} _mode + */ + setSortmode( _mode) + { + // Remove the last sortmode class and add the new one + this.labelNode.removeClass(this.sortmode) + .addClass(_mode); + + this.sortmode = _mode; + } + +} +et2_register_widget(et2_nextmatch_sortheader, ['nextmatch-sortheader']); + +/** + * Filter from a provided list of options + */ +export class et2_nextmatch_filterheader extends et2_selectbox implements et2_INextmatchHeader, et2_IResizeable +{ + private nextmatch: et2_nextmatch; + + /** + * Override to add change handler + */ + createInputWidget( ) + { + // Make sure there's an option for all + if(!this.options.empty_label && (!this.options.select_options || !this.options.select_options[""])) + { + this.options.empty_label = this.options.label ? this.options.label : egw.lang("All"); + } + super.createInputWidget(); + + jQuery(this.getInputNode()).change(this, function(event) { + if(typeof event.data.nextmatch == 'undefined') + { + // Not fully set up yet + return; + } + const col_filter = {}; + col_filter[event.data.id] = event.data.input.val(); + // Set value so it's there for response (otherwise it gets cleared if options are updated) + event.data.set_value(event.data.input.val()); + + event.data.nextmatch.applyFilters({col_filter: col_filter}); + }); + + } + + /** + * Set nextmatch is the function which has to be implemented for the + * et2_INextmatchHeader interface. + * + * @param {et2_nextmatch} _nextmatch + */ + setNextmatch( _nextmatch) + { + this.nextmatch = _nextmatch; + + // Set current filter value from nextmatch settings + if(this.nextmatch.activeFilters.col_filter && typeof this.nextmatch.activeFilters.col_filter[this.id] != "undefined") + { + this.set_value(this.nextmatch.activeFilters.col_filter[this.id]); + + // Make sure it's set in the nextmatch + _nextmatch.activeFilters.col_filter[this.id] = this.getValue(); + } + } + + // Make sure selectbox is not longer than the column + resize( ) + { + this.input.css("max-width",jQuery(this.parentNode).innerWidth() + "px"); + } + +} +et2_register_widget(et2_nextmatch_filterheader, ['nextmatch-filterheader']); + +/** + * Filter by account + */ +export class et2_nextmatch_accountfilterheader extends et2_selectAccount implements et2_INextmatchHeader, et2_IResizeable +{ + /** + * Override to add change handler + * + */ + createInputWidget( ) + { + // Make sure there's an option for all + if(!this.options.empty_label && !this.options.select_options[""]) + { + this.options.empty_label = this.options.label ? this.options.label : egw.lang("All"); + } + super.createInputWidget(this, arguments); + + this.input.change(this, function(event) { + if(typeof event.data.nextmatch == 'undefined') + { + // Not fully set up yet + return; + } + var col_filter = {}; + col_filter[event.data.id] = event.data.getValue(); + event.data.nextmatch.applyFilters({col_filter: col_filter}); + }); + + } + + /** + * Set nextmatch is the function which has to be implemented for the + * et2_INextmatchHeader interface. + * + * @param {et2_nextmatch} _nextmatch + */ + setNextmatch( _nextmatch) + { + this.nextmatch = _nextmatch; + + // Set current filter value from nextmatch settings + if(this.nextmatch.activeFilters.col_filter && this.nextmatch.activeFilters.col_filter[this.id]) + { + this.set_value(this.nextmatch.activeFilters.col_filter[this.id]); + } + } + // Make sure selectbox is not longer than the column + resize( ) + { + var max = jQuery(this.parentNode).innerWidth() - 4; + var surroundings = this.getSurroundings()._widgetSurroundings; + for(var i = 0; i < surroundings.length; i++) + { + max -= jQuery(surroundings[i]).outerWidth(); + } + this.input.css("max-width",max + "px"); + } + +} +et2_register_widget(et2_nextmatch_accountfilterheader, ['nextmatch-accountfilter']); + +/** + * Filter allowing multiple values to be selected, base on a taglist instead + * of a regular selectbox + * + * @augments et2_taglist + */ +class et2_nextmatch_taglistheader extends et2_taglist implements et2_INextmatchHeader, et2_IResizeable +{ + static readonly _attributes : any = { + autocomplete_url: { default: ''}, + multiple: { default: 'toggle'}, + onchange: { + // @ts-ignore + default: function(event) { + if(typeof this.nextmatch === 'undefined') + { + // Not fully set up yet + return; + } + var col_filter = {}; + col_filter[this.id] = this.getValue(); + // Set value so it's there for response (otherwise it gets cleared if options are updated) + //event.data.set_value(event.data.input.val()); + + this.nextmatch.applyFilters({col_filter: col_filter}); + } + }, + rows: { default: 2}, + class: {default: 'nm_filterheader_taglist'} + }; + private nextmatch: et2_nextmatch; + + /** + * Override to add change handler + * + * @memberOf et2_nextmatch_filterheader + */ + createInputWidget( ) + { + // Make sure there's an option for all + if(!this.options.empty_label && (!this.options.select_options || !this.options.select_options[""])) + { + this.options.empty_label = this.options.label ? this.options.label : egw.lang("All"); + } + super.createInputWidget(); + } + + /** + * Disable toggle if there are 2 or less options + * @param {Object[]} options + */ + set_select_options(options) + { + if(options && options.length <= 2 && this.options.multiple == 'toggle') + { + this.set_multiple(false); + } + super.set_select_options(options) + } + + /** + * Set nextmatch is the function which has to be implemented for the + * et2_INextmatchHeader interface. + * + * @param {et2_nextmatch} _nextmatch + */ + setNextmatch( _nextmatch) + { + this.nextmatch = _nextmatch; + + // Set current filter value from nextmatch settings + if(this.nextmatch.activeFilters.col_filter && typeof this.nextmatch.activeFilters.col_filter[this.id] != "undefined") + { + this.set_value(this.nextmatch.activeFilters.col_filter[this.id]); + + // Make sure it's set in the nextmatch + _nextmatch.activeFilters.col_filter[this.id] = this.getValue(); + } + } + + // Make sure selectbox is not longer than the column + resize( ) + { + this.div.css("height",''); + this.div.css("max-width",jQuery(this.parentNode).innerWidth() + "px"); + super.resize(); + } + +} +et2_register_widget(et2_nextmatch_taglistheader, ['nextmatch-taglistheader']); + +/** + * Nextmatch filter that can filter for a selected entry + */ +class et2_nextmatch_entryheader extends et2_link_entry implements et2_INextmatchHeader +{ + /** + * Override to add change handler + * + * @memberOf et2_nextmatch_entryheader + * @param {object} event + * @param {object} selected + */ + onchange( event, selected) + { + const col_filter = {}; + col_filter[this.id] = this.get_value(); + this.nextmatch.applyFilters.call(this.nextmatch, {col_filter: col_filter}); + } + + /** + * Override to always return a string appname:id (or just id) for simple (one real selection) + * cases, parent returns an object. If multiple are selected, or anything other than app and + * id, the original parent value is returned. + */ + getValue( ) + { + let value = super.getValue(); + if(typeof value == "object" && value != null) + { + if(!value.app || !value.id) return null; + + // If array with just one value, use a string instead for legacy server handling + if(typeof value.id == 'object' && value.id.shift && value.id.length == 1) + { + value.id = value.id.shift(); + } + // If simple value, format it legacy string style, otherwise + // we return full value + if(typeof value.id == 'string') + { + value = value.app +":"+value.id; + } + } + return value; + } + + /** + * Set nextmatch is the function which has to be implemented for the + * et2_INextmatchHeader interface. + * + * @param {et2_nextmatch} _nextmatch + */ + setNextmatch( _nextmatch) + { + this.nextmatch = _nextmatch; + + // Set current filter value from nextmatch settings + if(this.nextmatch.options.settings.col_filter && this.nextmatch.options.settings.col_filter[this.id]) + { + this.set_value(this.nextmatch.options.settings.col_filter[this.id]); + + if(this.getValue() != this.nextmatch.activeFilters.col_filter[this.id]) + { + this.nextmatch.activeFilters.col_filter[this.id] = this.getValue(); + } + + // Tell framework to ignore, or it will reset it to ''/empty when it does loadingFinished() + this.attributes.value.ignore = true; + //this.attributes.select_options.ignore = true; + } + // Fire on lost focus, clear filter if user emptied box + } +} +et2_register_widget(et2_nextmatch_entryheader, ['nextmatch-entryheader']); + +/** + * @augments et2_nextmatch_filterheader + */ +class et2_nextmatch_customfilter extends et2_nextmatch_filterheader +{ + static readonly _attributes: any = { + "widget_type": { + "name": "Actual type", + "type": "string", + "description": "The actual type of widget you should use", + "no_lang": 1 + }, + "widget_options": { + "name": "Actual options", + "type": "any", + "description": "The options for the actual widget", + "no_lang": 1, + "default": {} + } + }; + legacyOptions: ["widget_type","widget_options"]; + + real_node: et2_selectbox; + + /** + * Constructor + * + * @param _parent + * @param _attrs + * @param _child + * @memberOf et2_nextmatch_customfilter + */ + constructor(_parent? : et2_widget, _attrs? : WidgetConfig, _child? : object) + { + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_nextmatch_customfilter._attributes, _child || {})); + + switch(_attrs.widget_type) + { + case "link-entry": + _attrs.type = 'nextmatch-entryheader'; + break; + default: + if(_attrs.widget_type.indexOf('select') === 0) + { + _attrs.type = 'nextmatch-filterheader'; + } + else + { + _attrs.type = _attrs.widget_type; + } + } + jQuery.extend(_attrs.widget_options,{id: this.id}); + + _attrs.id = ''; + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_nextmatch_customfilter._attributes, _child || {})); + + this.real_node = et2_createWidget(_attrs.type, _attrs.widget_options, this.getParent()); + const select_options = []; + const correct_type = _attrs.type; + this.real_node['type'] = _attrs.widget_type; + et2_selectbox.find_select_options(this.real_node, select_options, _attrs); + this.real_node["_type"] = correct_type; + if(typeof this.real_node.set_select_options === 'function') + { + this.real_node.set_select_options(select_options); + } + } + + // Just pass the real DOM node through, in case anybody asks + getDOMNode( _sender) + { + return this.real_node ? this.real_node.getDOMNode(_sender) : null; + } + + // Also need to pass through real children + getChildren( ) + { + return this.real_node.getChildren() || []; + } + setNextmatch(_nextmatch : et2_nextmatch) + { + if(this.real_node && this.real_node.instanceOf(et2_INextmatchHeader)) + { + return (this.real_node).setNextmatch(_nextmatch); + } + } +} +et2_register_widget(et2_nextmatch_customfilter, ['nextmatch-customfilter']); diff --git a/api/js/etemplate/et2_extension_nextmatch_controller.js b/api/js/etemplate/et2_extension_nextmatch_controller.js index ae5bbd2907..2efd333310 100644 --- a/api/js/etemplate/et2_extension_nextmatch_controller.js +++ b/api/js/etemplate/et2_extension_nextmatch_controller.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - Class which contains a the data model for nextmatch widgets * @@ -8,735 +9,611 @@ * @author Andreas Stöckel * @copyright Stylite 2012 * @version $Id$ - */ + * /*egw:uses - et2_core_common; - et2_core_inheritance; + et2_core_common; + et2_core_inheritance; - et2_dataview_view_row; - et2_dataview_controller; - et2_dataview_interfaces; - et2_dataview_view_tile; + et2_dataview_view_row; + et2_dataview_controller; + et2_dataview_interfaces; + et2_dataview_view_tile; - et2_extension_nextmatch_actions; // Contains nm_action + et2_extension_nextmatch_actions; // Contains nm_action - egw_data; + egw_data; */ - +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 }); +var et2_dataview_view_row_1 = require("./et2_dataview_view_row"); +var et2_dataview_view_tile_1 = require("./et2_dataview_view_tile"); /** * @augments et2_dataview_controller */ -var et2_nextmatch_controller = (function(){ "use strict"; return et2_dataview_controller.extend(et2_IDataProvider, -{ - // Display constants - VIEW_ROW: 'row', - VIEW_TILE: 'tile', - - /** - * Initializes the nextmatch controller. - * - * @param _parentController is the parent nextmatch controller instance - * @param _egw is the api instance - * @param _execId is the execId of the etemplate - * @param _widget is the nextmatch-widget we are fetching data for. - * @param _grid is the grid the grid controller will be controlling - * @param _rowProvider is the nextmatch row provider instance. - * @param _objectManager is the parent object manager (if null, the object - * manager) will be created using - * @param _actionLinks contains the action links - * @param _actions contains the actions, may be null if an object manager - * is given. - * @memberOf et2_nextmatch_controller - */ - init: function (_parentController, _egw, _execId, _widget, _parentId, - _grid, _rowProvider, _actionLinks, _objectManager, _actions) { - - // Copy the egw reference - this.egw = _egw; - - // Keep a reference to the widget - this._widget = _widget; - - // Copy the given parameters - this._actionLinks = _actionLinks; - this._execId = _execId; - - // Get full widget ID, including path - var id = _widget.getArrayMgr('content').getPath(); - - if(typeof id == 'string') - { - this._widgetId = id; - } - else if (id.length === 1) - { - this._widgetId = id[0]; - } - else - { - this._widgetId = id.shift() + '[' + id.join('][') + ']'; - } - this._parentId = _parentId; - this._rowProvider = _rowProvider; - - // Initialize the action and the object manager - // _initActions calls _init_link_dnd, which uses this._actionLinks, - // so this must happen after the above parameter copying - if (!_objectManager) - { - this._initActions(_actions); - } - else - { - this._actionManager = null; - this._objectManager = _objectManager; - } - // Add our selection callback to selection manager - var self = this; - this._objectManager.setSelectedCallback = function() {self._selectCallback.apply(self,[this,arguments]);}; - - // Call the parent et2_dataview_controller constructor - this._super(_parentController, _grid, this, this._rowCallback, - this._linkCallback, this, this._objectManager); - - // We start with no filters - this._filters = {}; - - // Keep selection across filter changes - this.kept_selection = null; - this.kept_focus = null; - this.kept_expansion = []; - - // Directly use the API-Implementation of dataRegisterUID and - // dataUnregisterUID - this.dataUnregisterUID = _egw.dataUnregisterUID; - - // Default to rows - this._view = et2_nextmatch_controller.prototype.VIEW_ROW; - }, - - destroy: function () { - // If the actionManager variable is set, the object- and actionManager - // were created by this instance -- clear them - if (this._actionManager) - { - this._objectManager.remove(); - this._actionManager.remove(); - } - - this._super(); - }, - - /** - * Updates the filter instance. - */ - setFilters: function (_filters) { - // Update the filters - this._filters = _filters; - }, - - /** - * Keep the selection, if possible, across a data fetch and restore it - * after - */ - keepSelection: function() { - this.kept_selection = this._selectionMgr ? this._selectionMgr.getSelected() : null; - this.kept_focus = this._selectionMgr && this._selectionMgr._focusedEntry ? - this._selectionMgr._focusedEntry.uid || null : null; - - // Find expanded rows - var nm = this._widget; - var controller = this; - jQuery('.arrow.opened',this._widget.getDOMNode(this._widget)).each(function() { - var entry = controller.getRowByNode(this); - if(entry && entry.uid) - { - controller.kept_expansion.push(entry.uid); - } - }); - }, - - getObjectManager: function () { - return this._objectManager; - }, - - /** - * Deletes a row from the grid - * - * @param {string} uid - */ - deleteRow: function(uid) { - var entry = this._selectionMgr._getRegisteredRowsEntry(uid); - - // Unselect - this._selectionMgr.setSelected(uid,false); - - if(entry && entry.idx !== null) - { - // This will remove the row, but add an empty to the end. - // That's OK, because it will be removed when we update the row count - this._grid.deleteRow(entry.idx); - - // Trigger controller to remove from internals - this.egw.dataStoreUID(uid,null); - // Stop caring about this ID - this.egw.dataDeleteUID(uid); - // Remove from internal map - delete this._indexMap[entry.idx]; - - // Update the indices of all elements after the current one - for(var mapIndex = entry.idx + 1; typeof this._indexMap[mapIndex] !== 'undefined'; mapIndex++) - { - var entry = this._indexMap[mapIndex]; - entry.idx = mapIndex-1; - this._indexMap[mapIndex-1] = entry; - - // Update selection mgr too - if(entry.uid && typeof this._selectionMgr._registeredRows[entry.uid] !== 'undefined') - { - var reg = this._selectionMgr._getRegisteredRowsEntry(entry.uid); - reg.idx = entry.idx; - if(reg.ao && reg.ao._index) reg.ao._index = entry.idx; - } - } - // Remove last one, it was moved to mapIndex-1 before increment - delete this._indexMap[mapIndex-1]; - - // Not needed, they share by reference - // this._selectionMgr.setIndexMap(this._indexMap); - } - }, - - /** -- PRIVATE FUNCTIONS -- **/ - - /** - * Create a new row, either normal or tiled - * - * @param {type} ctx - * @returns {et2_dataview_container} - */ - _createRow: function(ctx) { - switch(this._view) - { - case et2_nextmatch_controller.prototype.VIEW_TILE: - var row = new et2_dataview_tile(this._grid); - // Try to overcome chrome rendering issue where float is not - // applied properly, leading to incomplete rows - window.setTimeout(function() { - if(!row.tr) return; - row.tr.css('float','none'); - window.setTimeout(function() { - if(!row.tr) return; - row.tr.css('float','left'); - },50); - },100); - return row; - case et2_nextmatch_controller.prototype.VIEW_ROW: - default: - return new et2_dataview_row(this._grid); - } - }, - - /** - * Initializes the action and the object manager. - */ - _initActions: function (_actions) { - // Generate a uid for the action and object manager - var uid = this._widget.id||this.egw.uid(); - - if(_actions == null) _actions = []; - - // 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(this._actionManager == null) - { - this._actionManager = gam.addAction("actionManager", uid); - } - this._actionManager.updateActions(_actions, this.egw.appName); - var data = this._actionManager.data; - if (data == 'undefined' || !data) - { - data = {}; - } - data.nextmatch = this._widget; - this._actionManager.set_data(data); - - // Set the default execute handler - var self = this; - this._actionManager.setDefaultExecute(function (_action, _senders, _target) { - // Get the selected ids descriptor object - var ids = self._selectionMgr.getSelected(); - - // Pass a reference to the actual widget - if (typeof _action.data == 'undefined' || !_action.data) _action.data = {}; - _action.data.nextmatch = self._widget; - - // Call the nm_action function with the ids - nm_action(_action, _senders, _target, ids); - }); - - // Set the 'Select All' handler - var select_all = this._actionManager.getActionById('select_all'); - if(select_all) - { - select_all.set_onExecute(jQuery.proxy(function(action, selected) { - this._selectionMgr.selectAll(); - }, this)); - } - - // Initialize the object manager - look for application - // object manager 1 level deep - var gom = egw_getObjectManager(this.egw.appName,true,1); - if(this._objectManager == null) - { - this._objectManager = gom.addObject( - new egwActionObjectManager(uid, this._actionManager)); - - this._objectManager.handleKeyPress = function(_keyCode, _shift, _ctrl, _alt) { - for(var i = 0; i < self._actionManager.children.length; i++) - { - if(typeof self._actionManager.children[i].shortcut === 'object' && - self._actionManager.children[i].shortcut && - _keyCode == self._actionManager.children[i].shortcut.keyCode) - { - return this.executeActionImplementation( - { - "keyEvent": { - "keyCode": _keyCode, - "shift": _shift, - "ctrl": _ctrl, - "alt": _alt - } - }, "popup", EGW_AO_EXEC_SELECTED); - } - } - return egwActionObject.prototype.handleKeyPress.call(this, _keyCode,_shift,_ctrl,_alt); - } - - } - this._objectManager.flags = this._objectManager.flags - | EGW_AO_FLAG_DEFAULT_FOCUS | EGW_AO_FLAG_IS_CONTAINER; - - this._init_links_dnd(this._actionManager); - - if(this._selectionMgr) - { - // Need to update the action links for every registered row too - for (var uid in this._selectionMgr._registeredRows) - { - // Get the corresponding entry from the registered rows array - var entry = this._selectionMgr._getRegisteredRowsEntry(uid); - if(entry.ao) - { - entry.ao.updateActionLinks(this._actionLinks); - } - } - } - }, - - /** - * Automatically add dnd support for linking - */ - _init_links_dnd: function() { - var mgr = this._actionManager; - var self = this; - - var drop_action = mgr.getActionById('egw_link_drop'); - var drag_action = mgr.getActionById('egw_link_drag'); - var drop_cancel = mgr.getActionById('egw_cancel_drop'); - - if(!this._actionLinks) - { - this._actionLinks = []; - } - - if (!drop_cancel) - { - // Create a generic cancel action in order to cancel drop action - // applied for all apps plus file and link action. - drop_cancel = mgr.addAction('drop', 'egw_cancel_drop', this.egw.lang('Cancel'), egw.image('cancel'), function(){},true); - drop_cancel.set_group('99'); - drop_cancel.acceptedTypes = drop_cancel.acceptedTypes.concat(Object.keys(egw.user('apps')).concat(['link', 'file'])); - this._actionLinks.push (drop_cancel.id); - } - - // Check if this app supports linking - if(!egw.link_get_registry(this.dataStorePrefix || this.egw.appName, 'query') || - egw.link_get_registry(this.dataStorePrefix || this.egw.appName, 'title')) - { - if(drop_action) - { - drop_action.remove(); - if(this._actionLinks.indexOf(drop_action.id) >= 0) - { - this._actionLinks.splice(this._actionLinks.indexOf(drop_action.id),1); - } - } - if(drag_action) - { - drag_action.remove(); - if(this._actionLinks.indexOf(drag_action.id) >= 0) - { - this._actionLinks.splice(this._actionLinks.indexOf(drag_action.id),1); - } - } - return; - } - - // Don't re-add - if(drop_action == null) - { - // Create the drop action that links entries - drop_action = mgr.addAction('drop', 'egw_link_drop', this.egw.lang('Create link'), egw.image('link'), function(action, source, dropped) { - // Extract link IDs - var links = []; - var id = ''; - for(var i = 0; i < source.length; i++) - { - if(!source[i].id) continue; - id = source[i].id.split('::'); - links.push({app: id[0] == 'filemanager' ? 'link' : id[0], id: id[1]}); - } - if(!links.length) - { - return; - } - - // Link the entries - self.egw.json("EGroupware\\Api\\Etemplate\\Widget\\Link::ajax_link", - dropped.id.split('::').concat([links]), - function(result) { - if(result) - { - for (var i=0; i < this._objectManager.selectedChildren.length; i++) - { - this._widget.refresh(this._objectManager.selectedChildren[i].id,'update'); - } - this._widget.egw().message('Linked'); - // Update the target to show the liks - this._widget.refresh(dropped.id,'update'); - } - }, - self, - true, - self - ).sendRequest(); - - },true); - } - if(this._actionLinks.indexOf(drop_action.id) < 0) - { - this._actionLinks.push(drop_action.id); - } - // Accept other links, and files dragged from the filemanager - // This does not handle files dragged from the desktop. They are - // handled by et2_nextmatch, since it needs DOM stuff - if(drop_action.acceptedTypes.indexOf('link') == -1) - { - drop_action.acceptedTypes.push('link'); - } - - // Don't re-add - if(drag_action == null) - { - // Create drag action that allows linking - drag_action = mgr.addAction('drag', 'egw_link_drag', this.egw.lang('link'), 'link', function(action, selected) { - // Drag helper - list titles. Arbitrarily limited to 10. - var helper = jQuery(document.createElement("div")); - for(var i = 0; i < selected.length && i < 10; i++) - { - var id = selected[i].id.split('::'); - var span = jQuery(document.createElement('span')).appendTo(helper); - self.egw.link_title(id[0],id[1], function(title) { - this.append(title); - this.append('
'); - }, span); - } - // As we wanted to have a general defaul helper interface, we return null here and not using customize helper for links - // TODO: Need to decide if we need to create a customized helper interface for links anyway - //return helper; - return null; - },true); - } - if(this._actionLinks.indexOf(drag_action.id) < 0) - { - this._actionLinks.push(drag_action.id); - } - drag_action.set_dragType('link'); - }, - - /** - * Set the data cache prefix - * Overridden from the parent to re-check automatically the added link dnd - * since the prefix is used in support detection. - */ - setPrefix: function(prefix) { - this._super.apply(this, arguments) - - this._init_links_dnd(this._actionManager); - }, - - /** - * Overwrites the inherited _destroyCallback function in order to be able - * to free the "rowWidget". - */ - _destroyCallback: function (_row) { - // Destroy any widget associated to the row - if (this.entry.widget) - { - this.entry.widget.free(); - this.entry.widget = null; - } - - // Call the inherited function - this._super.apply(this, arguments); - }, - - /** - * Creates the actual data row. - * - * @param _data is an array containing the row data - * @param _tr is the tr into which the data will be inserted - * @param _idx is the index of the row - * @param _entry is the internal row datastructure of the controller, in - * this special case used to store the rowWidget reference, so that it can - * be properly freed. - */ - _rowCallback: function (_data, _tr, _idx, _entry) { - // Let the row provider fill in the data row -- store the returned - // rowWidget inside the _entry - _entry.widget = this._rowProvider.getDataRow( - { "content": _data }, _tr, _idx, this); - }, - - /** - * Returns the names of action links for a given data row -- currently these are - * always the same links, as we controll enabled/disabled over the row - * classes, unless the row UID is "", then it's an 'empty' row. - * - * The empty row placeholder can still have actions, but nothing that requires - * an actual UID. - * - * @TODO: Currently empty row is just add, need to actually filter somehow. Here - * might not be the right place. - * - * @param _data Object The data for the row - * @param _idx int The row index - * @param _uid String The row's ID - * - * @return Array List of action names that valid for the row - */ - _linkCallback: function (_data, _idx, _uid) { - if(_uid.trim() != "") - { - return this._actionLinks; - } - - // No UID, so return a filtered list of actions that doesn't need a UID - var links = []; - try { - links = typeof this._widget.options.settings.placeholder_actions != 'undefined' ? - this._widget.options.settings.placeholder_actions : (this._widget.options.add ? ["add"] : []); - // Make sure that placeholder actions are defined and existed in client-side, - // otherwise do not set them as placeholder. for instance actions with - // attribute hideOnMobile do not get sent to client-side. - var action_search = function(current) { - if (typeof this._widget.options.actions[current] !== 'undefined') return true; - // Check children - for(var action in this._widget.options.actions) - { - action = this._widget.options.actions[action]; - if( action.children && action.children[current]) return true; - } - return false; - } - links = links.filter(action_search, this); - } catch (e) { - } - - return links; - }, - - - /** - * Overridden from the parent to also process any additional data that - * the data source adds, such as readonlys and additonal content. - * For example, non-numeric IDs in rows are added to the content manager - */ - _fetchCallback: function (_response) { - var nm = this.self._widget; - if(!nm) - { - // Nextmatch either not connected, or it tried to destroy this - // but the server returned something - return; - } - // Readonlys - // Other stuff - for(var i in _response.rows) - { - if(jQuery.isNumeric(i)) continue; - if(i == 'sel_options') - { - var mgr = nm.getArrayMgr(i); - for(var id in _response.rows.sel_options) - { - mgr.data[id] = _response.rows.sel_options[id]; - var select = nm.getWidgetById(id); - if(select && select.set_select_options) - { - select.set_select_options(_response.rows.sel_options[id]); - } - // Clear rowProvider internal cache so it uses new values - if(id == 'cat_id') - { - this.self._rowProvider.categories = null; - } - // update array mgr so select widgets in row also get refreshed options - nm.getParent().getArrayMgr('sel_options').data[id] = _response.rows.sel_options[id]; - } - } - else - { - var mgr = nm.getArrayMgr('content'); - mgr.data[i] = _response.rows[i]; - - // It's not enough to just update the data, the widgets need to - // be updated too, if there are matching widgets. - var widget = nm.getWidgetById(i); - if(widget && widget.set_value) - { - widget.set_value(mgr.getEntry(i)); - } - } - } - // Might be trying to disable a column - var col_refresh = false; - for(var column_index = 0; column_index < nm.columns.length; column_index++) - { - if(typeof nm.columns[column_index].disabled === 'string' && - nm.columns[column_index].disabled[0] === '@') - { - col_refresh = true; - nm.dataview.columnMgr.getColumnById('col_'+column_index) - .set_visibility( - nm.getArrayMgr('content').parseBoolExpression(nm.columns[column_index].disabled) ? - ET2_COL_VISIBILITY_DISABLED : - nm.columns[column_index].visible - ); - } - } - if(col_refresh) - { - nm.dataview.columnMgr.updated = true; - nm.dataview._updateColumns(); - } - - // If we're doing an autorefresh and the count decreases, preserve the - // selection or it will be lost when the grid rows are shuffled. Increases - // are fine though. - if(this.self && this.self.kept_selection == null && - !this.refresh && this.self._grid.getTotalCount() > _response.total) - { - this.self.keepSelection(); - } - - // Call the inherited function - this._super.apply(this, arguments); - - // Restore selection, if passed - if(this.self && this.self.kept_selection && this.self._selectionMgr) - { - if(this.self.kept_selection.all) - { - this.self._selectionMgr.selectAll(); - } - for(var i = (this.self.kept_selection.ids.length || 1)-1; i >= 0; i--) - { - // Only keep the selected if they came back in the fetch - if(_response.order.indexOf(this.self.kept_selection.ids[i]) >= 0) - { - this.self._selectionMgr.setSelected(this.self.kept_selection.ids[i],true); - this.self.kept_selection.ids.splice(i,1); - } - else - { - this.self.kept_selection.ids.splice(i,1); - } - } - if(this.self.kept_focus && _response.order.indexOf(this.self.kept_focus) >= 0) - { - this.self._selectionMgr.setFocused(this.self.kept_focus,true); - } - // Re-expanding rows handled in et2_extension_nextmatch_rowProvider - // Expansions might still be valid, so we don't clear them - if(this.self.kept_selection != null && typeof this.self.kept_selection.ids != 'undefined' && this.self.kept_selection.ids.length == 0) - { - this.self.kept_selection = null; - } - this.self.kept_focus = null; - } - }, - - /** - * Execute the select callback when the row selection changes - */ - _selectCallback: function(action,senders) - { - if(typeof senders == "undefined") - { - senders = []; - } - if(!this._widget) return; - - // inform mobile framework about nm selections, need to update status of header objects on selection - if (egwIsMobile()) framework.nm_onselect_ctrl(this._widget, action, senders); - - this._widget.onselect.call(this._widget, action,senders); - }, - - /** -- Implementation of et2_IDataProvider -- **/ - - - dataFetch: function (_queriedRange, _callback, _context) { - - // Merge the parent id into the _queriedRange if it is set - if (this._parentId !== null) - { - _queriedRange["parent_id"] = this._parentId; - } - - // sub-levels dont have there own _filters object, need to use the one from parent (or it's parents parent) - var obj = this; - while((typeof obj._filters == 'undefined' || jQuery.isEmptyObject(obj._filters)) && obj._parentController) - { - obj = obj._parentController; - } - - // Pass the fetch call to the API, multiplex the data about the - // nextmatch instance into the call. - this.egw.dataFetch( - this._widget.getInstanceManager().etemplate_exec_id || this._execId, - _queriedRange, - obj._filters, - this._widgetId, - _callback, - _context); - }, - - dataRegisterUID: function (_uid, _callback, _context) { - this.egw.dataRegisterUID(_uid, _callback, _context, - this._widget.getInstanceManager().etemplate_exec_id || this._execId, - this._widgetId - ); - }, - - dataUnregisterUID: function () { - // Overwritten in the constructor - } - -});}).call(this); - - +var et2_nextmatch_controller = /** @class */ (function (_super) { + __extends(et2_nextmatch_controller, _super); + /** + * Initializes the nextmatch controller. + * + * @param _parentController is the parent nextmatch controller instance + * @param _egw is the api instance + * @param _execId is the execId of the etemplate + * @param _widget is the nextmatch-widget we are fetching data for. + * @param _grid is the grid the grid controller will be controlling + * @param _rowProvider is the nextmatch row provider instance. + * @param _objectManager is the parent object manager (if null, the object + * manager) will be created using + * @param _actionLinks contains the action links + * @param _actions contains the actions, may be null if an object manager + * is given. + * @memberOf et2_nextmatch_controller + */ + function et2_nextmatch_controller(_parentController, _egw, _execId, _widget, _parentId, _grid, _rowProvider, _actionLinks, _objectManager, _actions) { + var _this = + // Call the parent et2_dataview_controller constructor + _super.call(this, _parentController, _grid) || this; + _this.setDataProvider(_this); + _this.setRowCallback(_this._rowCallback); + _this.setLinkCallback(_this._linkCallback); + _this.setContext(_this); + // Copy the egw reference + _this.egw = _egw; + // Keep a reference to the widget + _this._widget = _widget; + // Copy the given parameters + _this._actionLinks = _actionLinks; + _this._execId = _execId; + // Get full widget ID, including path + var id = _widget.getArrayMgr('content').getPath(); + if (typeof id == 'string') { + _this._widgetId = id; + } + else if (id.length === 1) { + _this._widgetId = id[0]; + } + else { + _this._widgetId = id.shift() + '[' + id.join('][') + ']'; + } + _this._parentId = _parentId; + _this._rowProvider = _rowProvider; + // Initialize the action and the object manager + // _initActions calls _init_link_dnd, which uses this._actionLinks, + // so this must happen after the above parameter copying + if (!_objectManager) { + _this._initActions(_actions); + } + else { + _this._actionManager = null; + _this._objectManager = _objectManager; + } + _this.setActionObjectManager(_this._objectManager); + // Add our selection callback to selection manager + var self = _this; + _this._objectManager.setSelectedCallback = function () { self._selectCallback.apply(self, [this, arguments]); }; + // We start with no filters + _this._filters = {}; + // Keep selection across filter changes + _this.kept_selection = null; + _this.kept_focus = null; + _this.kept_expansion = []; + // Directly use the API-Implementation of dataRegisterUID and + // dataUnregisterUID + _this.dataUnregisterUID = _egw.dataUnregisterUID; + // Default to rows + _this._view = et2_nextmatch_controller.VIEW_ROW; + return _this; + } + et2_nextmatch_controller.prototype.destroy = function () { + // If the actionManager variable is set, the object- and actionManager + // were created by this instance -- clear them + if (this._actionManager) { + this._objectManager.remove(); + this._actionManager.remove(); + } + _super.prototype.destroy.call(this); + }; + /** + * Updates the filter instance. + */ + et2_nextmatch_controller.prototype.setFilters = function (_filters) { + // Update the filters + this._filters = _filters; + }; + /** + * Keep the selection, if possible, across a data fetch and restore it + * after + */ + et2_nextmatch_controller.prototype.keepSelection = function () { + this.kept_selection = this._selectionMgr ? this._selectionMgr.getSelected() : null; + this.kept_focus = this._selectionMgr && this._selectionMgr._focusedEntry ? + this._selectionMgr._focusedEntry.uid || null : null; + // Find expanded rows + var nm = this._widget; + var controller = this; + jQuery('.arrow.opened', this._widget.getDOMNode(this._widget)).each(function () { + var entry = controller.getRowByNode(this); + if (entry && entry.uid) { + controller.kept_expansion.push(entry.uid); + } + }); + }; + et2_nextmatch_controller.prototype.getObjectManager = function () { + return this._objectManager; + }; + /** + * Deletes a row from the grid + * + * @param {string} uid + */ + et2_nextmatch_controller.prototype.deleteRow = function (uid) { + var entry = this._selectionMgr._getRegisteredRowsEntry(uid); + // Unselect + this._selectionMgr.setSelected(uid, false); + if (entry && entry.idx !== null) { + // This will remove the row, but add an empty to the end. + // That's OK, because it will be removed when we update the row count + this._grid.deleteRow(entry.idx); + // Trigger controller to remove from internals + this.egw.dataStoreUID(uid, null); + // Stop caring about this ID + this.egw.dataDeleteUID(uid); + // Remove from internal map + delete this._indexMap[entry.idx]; + // Update the indices of all elements after the current one + for (var mapIndex = entry.idx + 1; typeof this._indexMap[mapIndex] !== 'undefined'; mapIndex++) { + var entry = this._indexMap[mapIndex]; + entry.idx = mapIndex - 1; + this._indexMap[mapIndex - 1] = entry; + // Update selection mgr too + if (entry.uid && typeof this._selectionMgr._registeredRows[entry.uid] !== 'undefined') { + var reg = this._selectionMgr._getRegisteredRowsEntry(entry.uid); + reg.idx = entry.idx; + if (reg.ao && reg.ao._index) + reg.ao._index = entry.idx; + } + } + // Remove last one, it was moved to mapIndex-1 before increment + delete this._indexMap[mapIndex - 1]; + // Not needed, they share by reference + // this._selectionMgr.setIndexMap(this._indexMap); + } + }; + /** -- PRIVATE FUNCTIONS -- **/ + /** + * Create a new row, either normal or tiled + * + * @param {type} ctx + * @returns {et2_dataview_container} + */ + et2_nextmatch_controller.prototype._createRow = function (ctx) { + switch (this._view) { + case et2_nextmatch_controller.VIEW_TILE: + var row = new et2_dataview_view_tile_1.et2_dataview_tile(this._grid); + // Try to overcome chrome rendering issue where float is not + // applied properly, leading to incomplete rows + window.setTimeout(function () { + if (!row.tr) + return; + row.tr.css('float', 'none'); + window.setTimeout(function () { + if (!row.tr) + return; + row.tr.css('float', 'left'); + }, 50); + }, 100); + return row; + case et2_nextmatch_controller.VIEW_ROW: + default: + return new et2_dataview_view_row_1.et2_dataview_row(this._grid); + } + }; + /** + * Initializes the action and the object manager. + */ + et2_nextmatch_controller.prototype._initActions = function (_actions) { + // Generate a uid for the action and object manager + var uid = this._widget.id || this.egw.uid(); + if (_actions == null) + _actions = []; + // 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 (this._actionManager == null) { + this._actionManager = gam.addAction("actionManager", uid); + } + this._actionManager.updateActions(_actions, this.egw.appName); + var data = this._actionManager.data; + if (data == 'undefined' || !data) { + data = {}; + } + data.nextmatch = this._widget; + this._actionManager.set_data(data); + // Set the default execute handler + var self = this; + this._actionManager.setDefaultExecute(function (_action, _senders, _target) { + // Get the selected ids descriptor object + var ids = self._selectionMgr.getSelected(); + // Pass a reference to the actual widget + if (typeof _action.data == 'undefined' || !_action.data) + _action.data = {}; + _action.data.nextmatch = self._widget; + // Call the nm_action function with the ids + nm_action(_action, _senders, _target, ids); + }); + // Set the 'Select All' handler + var select_all = this._actionManager.getActionById('select_all'); + if (select_all) { + select_all.set_onExecute(jQuery.proxy(function (action, selected) { + this._selectionMgr.selectAll(); + }, this)); + } + // Initialize the object manager - look for application + // object manager 1 level deep + var gom = egw_getObjectManager(this.egw.appName, true, 1); + if (this._objectManager == null) { + this._objectManager = gom.addObject(new egwActionObjectManager(uid, this._actionManager)); + this._objectManager.handleKeyPress = function (_keyCode, _shift, _ctrl, _alt) { + for (var i = 0; i < self._actionManager.children.length; i++) { + if (typeof self._actionManager.children[i].shortcut === 'object' && + self._actionManager.children[i].shortcut && + _keyCode == self._actionManager.children[i].shortcut.keyCode) { + return this.executeActionImplementation({ + "keyEvent": { + "keyCode": _keyCode, + "shift": _shift, + "ctrl": _ctrl, + "alt": _alt + } + }, "popup", EGW_AO_EXEC_SELECTED); + } + } + return egwActionObject.prototype.handleKeyPress.call(this, _keyCode, _shift, _ctrl, _alt); + }; + } + this._objectManager.flags = this._objectManager.flags + | EGW_AO_FLAG_DEFAULT_FOCUS | EGW_AO_FLAG_IS_CONTAINER; + this._init_links_dnd(); + if (this._selectionMgr) { + // Need to update the action links for every registered row too + for (var uid in this._selectionMgr._registeredRows) { + // Get the corresponding entry from the registered rows array + var entry = this._selectionMgr._getRegisteredRowsEntry(uid); + if (entry.ao) { + entry.ao.updateActionLinks(this._actionLinks); + } + } + } + }; + /** + * Automatically add dnd support for linking + */ + et2_nextmatch_controller.prototype._init_links_dnd = function () { + var mgr = this._actionManager; + var self = this; + var drop_action = mgr.getActionById('egw_link_drop'); + var drag_action = mgr.getActionById('egw_link_drag'); + var drop_cancel = mgr.getActionById('egw_cancel_drop'); + if (!this._actionLinks) { + this._actionLinks = []; + } + if (!drop_cancel) { + // Create a generic cancel action in order to cancel drop action + // applied for all apps plus file and link action. + drop_cancel = mgr.addAction('drop', 'egw_cancel_drop', this.egw.lang('Cancel'), egw.image('cancel'), function () { }, true); + drop_cancel.set_group('99'); + drop_cancel.acceptedTypes = drop_cancel.acceptedTypes.concat(Object.keys(egw.user('apps')).concat(['link', 'file'])); + this._actionLinks.push(drop_cancel.id); + } + // Check if this app supports linking + if (!egw.link_get_registry(this.dataStorePrefix || this.egw.appName, 'query') || + egw.link_get_registry(this.dataStorePrefix || this.egw.appName, 'title')) { + if (drop_action) { + drop_action.remove(); + if (this._actionLinks.indexOf(drop_action.id) >= 0) { + this._actionLinks.splice(this._actionLinks.indexOf(drop_action.id), 1); + } + } + if (drag_action) { + drag_action.remove(); + if (this._actionLinks.indexOf(drag_action.id) >= 0) { + this._actionLinks.splice(this._actionLinks.indexOf(drag_action.id), 1); + } + } + return; + } + // Don't re-add + if (drop_action == null) { + // Create the drop action that links entries + drop_action = mgr.addAction('drop', 'egw_link_drop', this.egw.lang('Create link'), egw.image('link'), function (action, source, dropped) { + // Extract link IDs + var links = []; + var id = ''; + for (var i = 0; i < source.length; i++) { + if (!source[i].id) + continue; + id = source[i].id.split('::'); + links.push({ app: id[0] == 'filemanager' ? 'link' : id[0], id: id[1] }); + } + if (!links.length) { + return; + } + // Link the entries + self.egw.json("EGroupware\\Api\\Etemplate\\Widget\\Link::ajax_link", dropped.id.split('::').concat([links]), function (result) { + if (result) { + for (var i = 0; i < this._objectManager.selectedChildren.length; i++) { + this._widget.refresh(this._objectManager.selectedChildren[i].id, 'update'); + } + this._widget.egw().message('Linked'); + // Update the target to show the liks + this._widget.refresh(dropped.id, 'update'); + } + }, self, true, self).sendRequest(); + }, true); + } + if (this._actionLinks.indexOf(drop_action.id) < 0) { + this._actionLinks.push(drop_action.id); + } + // Accept other links, and files dragged from the filemanager + // This does not handle files dragged from the desktop. They are + // handled by et2_nextmatch, since it needs DOM stuff + if (drop_action.acceptedTypes.indexOf('link') == -1) { + drop_action.acceptedTypes.push('link'); + } + // Don't re-add + if (drag_action == null) { + // Create drag action that allows linking + drag_action = mgr.addAction('drag', 'egw_link_drag', this.egw.lang('link'), 'link', function (action, selected) { + // Drag helper - list titles. Arbitrarily limited to 10. + var helper = jQuery(document.createElement("div")); + for (var i = 0; i < selected.length && i < 10; i++) { + var id = selected[i].id.split('::'); + var span = jQuery(document.createElement('span')).appendTo(helper); + self.egw.link_title(id[0], id[1], function (title) { + this.append(title); + this.append('
'); + }, span); + } + // As we wanted to have a general defaul helper interface, we return null here and not using customize helper for links + // TODO: Need to decide if we need to create a customized helper interface for links anyway + //return helper; + return null; + }, true); + } + if (this._actionLinks.indexOf(drag_action.id) < 0) { + this._actionLinks.push(drag_action.id); + } + drag_action.set_dragType('link'); + }; + /** + * Set the data cache prefix + * Overridden from the parent to re-check automatically the added link dnd + * since the prefix is used in support detection. + */ + et2_nextmatch_controller.prototype.setPrefix = function (prefix) { + _super.prototype.setPrefix.call(this, prefix); + this._init_links_dnd(); + }; + /** + * Overwrites the inherited _destroyCallback function in order to be able + * to free the "rowWidget". + */ + et2_nextmatch_controller.prototype._destroyCallback = function (_row) { + // Destroy any widget associated to the row + if (this.entry.widget) { + this.entry.widget.destroy(); + this.entry.widget = null; + } + // Call the inherited function + _super.prototype._destroyCallback.call(this, _row); + }; + /** + * Creates the actual data row. + * + * @param _data is an array containing the row data + * @param _tr is the tr into which the data will be inserted + * @param _idx is the index of the row + * @param _entry is the internal row datastructure of the controller, in + * this special case used to store the rowWidget reference, so that it can + * be properly freed. + */ + et2_nextmatch_controller.prototype._rowCallback = function (_data, _tr, _idx, _entry) { + // Let the row provider fill in the data row -- store the returned + // rowWidget inside the _entry + _entry.widget = this._rowProvider.getDataRow({ "content": _data }, _tr, _idx, this); + }; + /** + * Returns the names of action links for a given data row -- currently these are + * always the same links, as we controll enabled/disabled over the row + * classes, unless the row UID is "", then it's an 'empty' row. + * + * The empty row placeholder can still have actions, but nothing that requires + * an actual UID. + * + * @TODO: Currently empty row is just add, need to actually filter somehow. Here + * might not be the right place. + * + * @param _data Object The data for the row + * @param _idx int The row index + * @param _uid String The row's ID + * + * @return Array List of action names that valid for the row + */ + et2_nextmatch_controller.prototype._linkCallback = function (_data, _idx, _uid) { + if (_uid.trim() != "") { + return this._actionLinks; + } + // No UID, so return a filtered list of actions that doesn't need a UID + var links = []; + try { + links = typeof this._widget.options.settings.placeholder_actions != 'undefined' ? + this._widget.options.settings.placeholder_actions : (this._widget.options.add ? ["add"] : []); + // Make sure that placeholder actions are defined and existed in client-side, + // otherwise do not set them as placeholder. for instance actions with + // attribute hideOnMobile do not get sent to client-side. + var action_search = function (current) { + if (typeof this._widget.options.actions[current] !== 'undefined') + return true; + // Check children + for (var action in this._widget.options.actions) { + action = this._widget.options.actions[action]; + if (action.children && action.children[current]) + return true; + } + return false; + }; + links = links.filter(action_search, this); + } + catch (e) { + } + return links; + }; + /** + * Overridden from the parent to also process any additional data that + * the data source adds, such as readonlys and additonal content. + * For example, non-numeric IDs in rows are added to the content manager + */ + et2_nextmatch_controller.prototype._fetchCallback = function (_response) { + var nm = this.self._widget; + if (!nm) { + // Nextmatch either not connected, or it tried to destroy this + // but the server returned something + return; + } + // Readonlys + // Other stuff + for (var i in _response.rows) { + if (jQuery.isNumeric(i)) + continue; + if (i == 'sel_options') { + var mgr = nm.getArrayMgr(i); + for (var id in _response.rows.sel_options) { + mgr.data[id] = _response.rows.sel_options[id]; + var select = nm.getWidgetById(id); + if (select && select.set_select_options) { + select.set_select_options(_response.rows.sel_options[id]); + } + // Clear rowProvider internal cache so it uses new values + if (id == 'cat_id') { + this.self._rowProvider.categories = null; + } + // update array mgr so select widgets in row also get refreshed options + nm.getParent().getArrayMgr('sel_options').data[id] = _response.rows.sel_options[id]; + } + } + else { + var mgr = nm.getArrayMgr('content'); + mgr.data[i] = _response.rows[i]; + // It's not enough to just update the data, the widgets need to + // be updated too, if there are matching widgets. + var widget = nm.getWidgetById(i); + if (widget && widget.set_value) { + widget.set_value(mgr.getEntry(i)); + } + } + } + // Might be trying to disable a column + var col_refresh = false; + for (var column_index = 0; column_index < nm.columns.length; column_index++) { + if (typeof nm.columns[column_index].disabled === 'string' && + nm.columns[column_index].disabled[0] === '@') { + col_refresh = true; + nm.dataview.columnMgr.getColumnById('col_' + column_index) + .set_visibility(nm.getArrayMgr('content').parseBoolExpression(nm.columns[column_index].disabled) ? + et2_dataview_column.ET2_COL_VISIBILITY_DISABLED : + nm.columns[column_index].visible); + } + } + if (col_refresh) { + nm.dataview.columnMgr.updated(); + nm.dataview._updateColumns(); + } + // If we're doing an autorefresh and the count decreases, preserve the + // selection or it will be lost when the grid rows are shuffled. Increases + // are fine though. + if (this.self && this.self.kept_selection == null && + !this.refresh && this.self._grid.getTotalCount() > _response.total) { + this.self.keepSelection(); + } + // Call the inherited function + _super.prototype._fetchCallback.apply(this, arguments); + // Restore selection, if passed + if (this.self && this.self.kept_selection && this.self._selectionMgr) { + if (this.self.kept_selection.all) { + this.self._selectionMgr.selectAll(); + } + for (var i = (this.self.kept_selection.ids.length || 1) - 1; i >= 0; i--) { + // Only keep the selected if they came back in the fetch + if (_response.order.indexOf(this.self.kept_selection.ids[i]) >= 0) { + this.self._selectionMgr.setSelected(this.self.kept_selection.ids[i], true); + this.self.kept_selection.ids.splice(i, 1); + } + else { + this.self.kept_selection.ids.splice(i, 1); + } + } + if (this.self.kept_focus && _response.order.indexOf(this.self.kept_focus) >= 0) { + this.self._selectionMgr.setFocused(this.self.kept_focus, true); + } + // Re-expanding rows handled in et2_extension_nextmatch_rowProvider + // Expansions might still be valid, so we don't clear them + if (this.self.kept_selection != null && typeof this.self.kept_selection.ids != 'undefined' && this.self.kept_selection.ids.length == 0) { + this.self.kept_selection = null; + } + this.self.kept_focus = null; + } + }; + /** + * Execute the select callback when the row selection changes + */ + et2_nextmatch_controller.prototype._selectCallback = function (action, senders) { + if (typeof senders == "undefined") { + senders = []; + } + if (!this._widget) + return; + // inform mobile framework about nm selections, need to update status of header objects on selection + if (egwIsMobile()) + framework.nm_onselect_ctrl(this._widget, action, senders); + this._widget.onselect.call(this._widget, action, senders); + }; + /** -- Implementation of et2_IDataProvider -- **/ + et2_nextmatch_controller.prototype.dataFetch = function (_queriedRange, _callback, _context) { + // Merge the parent id into the _queriedRange if it is set + if (this._parentId !== null) { + _queriedRange["parent_id"] = this._parentId; + } + // sub-levels dont have there own _filters object, need to use the one from parent (or it's parents parent) + var obj = this; + while ((typeof obj._filters == 'undefined' || jQuery.isEmptyObject(obj._filters)) && obj._parentController) { + obj = obj._parentController; + } + // Pass the fetch call to the API, multiplex the data about the + // nextmatch instance into the call. + this.egw.dataFetch(this._widget.getInstanceManager().etemplate_exec_id || this._execId, _queriedRange, obj._filters, this._widgetId, _callback, _context); + }; + et2_nextmatch_controller.prototype.dataRegisterUID = function (_uid, _callback, _context) { + this.egw.dataRegisterUID(_uid, _callback, _context, this._widget.getInstanceManager().etemplate_exec_id || this._execId, this._widgetId); + }; + et2_nextmatch_controller.prototype.dataUnregisterUID = function () { + // Overwritten in the constructor + }; + // Display constants + et2_nextmatch_controller.VIEW_ROW = 'row'; + et2_nextmatch_controller.VIEW_TILE = 'tile'; + return et2_nextmatch_controller; +}(et2_dataview_controller)); +exports.et2_nextmatch_controller = et2_nextmatch_controller; +//# sourceMappingURL=et2_extension_nextmatch_controller.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_extension_nextmatch_controller.ts b/api/js/etemplate/et2_extension_nextmatch_controller.ts new file mode 100644 index 0000000000..0d86d99da5 --- /dev/null +++ b/api/js/etemplate/et2_extension_nextmatch_controller.ts @@ -0,0 +1,769 @@ +/** + * EGroupware eTemplate2 - Class which contains a the data model for nextmatch widgets + * + * @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 2012 + * @version $Id$ + * + +/*egw:uses + et2_core_common; + et2_core_inheritance; + + et2_dataview_view_row; + et2_dataview_controller; + et2_dataview_interfaces; + et2_dataview_view_tile; + + et2_extension_nextmatch_actions; // Contains nm_action + + egw_data; +*/ + +import {et2_IDataProvider} from "./et2_dataview_interfaces"; +import {et2_dataview_row} from "./et2_dataview_view_row"; +import {et2_dataview_tile} from "./et2_dataview_view_tile"; + +/** + * @augments et2_dataview_controller + */ +export class et2_nextmatch_controller extends et2_dataview_controller implements et2_IDataProvider +{ + // Display constants + public static readonly VIEW_ROW : string = 'row'; + public static readonly VIEW_TILE: string = 'tile'; + + /** + * Initializes the nextmatch controller. + * + * @param _parentController is the parent nextmatch controller instance + * @param _egw is the api instance + * @param _execId is the execId of the etemplate + * @param _widget is the nextmatch-widget we are fetching data for. + * @param _grid is the grid the grid controller will be controlling + * @param _rowProvider is the nextmatch row provider instance. + * @param _objectManager is the parent object manager (if null, the object + * manager) will be created using + * @param _actionLinks contains the action links + * @param _actions contains the actions, may be null if an object manager + * is given. + * @memberOf et2_nextmatch_controller + */ + constructor( _parentController, _egw, _execId, _widget, _parentId, + _grid, _rowProvider, _actionLinks, _objectManager, _actions?) + { + + // Call the parent et2_dataview_controller constructor + super(_parentController, _grid); + + this.setDataProvider(this); + this.setRowCallback(this._rowCallback); + this.setLinkCallback(this._linkCallback); + this.setContext(this); + + // Copy the egw reference + this.egw = _egw; + + // Keep a reference to the widget + this._widget = _widget; + + // Copy the given parameters + this._actionLinks = _actionLinks; + this._execId = _execId; + + // Get full widget ID, including path + var id = _widget.getArrayMgr('content').getPath(); + + if(typeof id == 'string') + { + this._widgetId = id; + } + else if (id.length === 1) + { + this._widgetId = id[0]; + } + else + { + this._widgetId = id.shift() + '[' + id.join('][') + ']'; + } + this._parentId = _parentId; + this._rowProvider = _rowProvider; + + // Initialize the action and the object manager + // _initActions calls _init_link_dnd, which uses this._actionLinks, + // so this must happen after the above parameter copying + if (!_objectManager) + { + this._initActions(_actions); + } + else + { + this._actionManager = null; + this._objectManager = _objectManager; + } + + this.setActionObjectManager(this._objectManager); + + // Add our selection callback to selection manager + var self = this; + this._objectManager.setSelectedCallback = function() {self._selectCallback.apply(self,[this,arguments]);}; + + // We start with no filters + this._filters = {}; + + // Keep selection across filter changes + this.kept_selection = null; + this.kept_focus = null; + this.kept_expansion = []; + + // Directly use the API-Implementation of dataRegisterUID and + // dataUnregisterUID + this.dataUnregisterUID = _egw.dataUnregisterUID; + + // Default to rows + this._view = et2_nextmatch_controller.VIEW_ROW; + + } + + destroy( ) + { + // If the actionManager variable is set, the object- and actionManager + // were created by this instance -- clear them + if (this._actionManager) + { + this._objectManager.remove(); + this._actionManager.remove(); + } + + super.destroy(); + } + + /** + * Updates the filter instance. + */ + setFilters( _filters) + { + // Update the filters + this._filters = _filters; + } + + /** + * Keep the selection, if possible, across a data fetch and restore it + * after + */ + keepSelection( ) + { + this.kept_selection = this._selectionMgr ? this._selectionMgr.getSelected() : null; + this.kept_focus = this._selectionMgr && this._selectionMgr._focusedEntry ? + this._selectionMgr._focusedEntry.uid || null : null; + + // Find expanded rows + var nm = this._widget; + var controller = this; + jQuery('.arrow.opened',this._widget.getDOMNode(this._widget)).each(function() { + var entry = controller.getRowByNode(this); + if(entry && entry.uid) + { + controller.kept_expansion.push(entry.uid); + } + }); + } + + getObjectManager( ) + { + return this._objectManager; + } + + /** + * Deletes a row from the grid + * + * @param {string} uid + */ + deleteRow( uid) + { + var entry = this._selectionMgr._getRegisteredRowsEntry(uid); + + // Unselect + this._selectionMgr.setSelected(uid,false); + + if(entry && entry.idx !== null) + { + // This will remove the row, but add an empty to the end. + // That's OK, because it will be removed when we update the row count + this._grid.deleteRow(entry.idx); + + // Trigger controller to remove from internals + this.egw.dataStoreUID(uid,null); + // Stop caring about this ID + this.egw.dataDeleteUID(uid); + // Remove from internal map + delete this._indexMap[entry.idx]; + + // Update the indices of all elements after the current one + for(var mapIndex = entry.idx + 1; typeof this._indexMap[mapIndex] !== 'undefined'; mapIndex++) + { + var entry = this._indexMap[mapIndex]; + entry.idx = mapIndex-1; + this._indexMap[mapIndex-1] = entry; + + // Update selection mgr too + if(entry.uid && typeof this._selectionMgr._registeredRows[entry.uid] !== 'undefined') + { + var reg = this._selectionMgr._getRegisteredRowsEntry(entry.uid); + reg.idx = entry.idx; + if(reg.ao && reg.ao._index) reg.ao._index = entry.idx; + } + } + // Remove last one, it was moved to mapIndex-1 before increment + delete this._indexMap[mapIndex-1]; + + // Not needed, they share by reference + // this._selectionMgr.setIndexMap(this._indexMap); + } + } + + /** -- PRIVATE FUNCTIONS -- **/ + + /** + * Create a new row, either normal or tiled + * + * @param {type} ctx + * @returns {et2_dataview_container} + */ + _createRow( ctx) + { + switch(this._view) + { + case et2_nextmatch_controller.VIEW_TILE: + var row = new et2_dataview_tile(this._grid); + // Try to overcome chrome rendering issue where float is not + // applied properly, leading to incomplete rows + window.setTimeout(function() { + if(!row.tr) return; + row.tr.css('float','none'); + window.setTimeout(function() { + if(!row.tr) return; + row.tr.css('float','left'); + },50); + },100); + return row; + case et2_nextmatch_controller.VIEW_ROW: + default: + return new et2_dataview_row(this._grid); + } + } + + /** + * Initializes the action and the object manager. + */ + _initActions( _actions) + { + // Generate a uid for the action and object manager + var uid = this._widget.id||this.egw.uid(); + + if(_actions == null) _actions = []; + + // 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(this._actionManager == null) + { + this._actionManager = gam.addAction("actionManager", uid); + } + this._actionManager.updateActions(_actions, this.egw.appName); + var data = this._actionManager.data; + if (data == 'undefined' || !data) + { + data = {}; + } + data.nextmatch = this._widget; + this._actionManager.set_data(data); + + // Set the default execute handler + var self = this; + this._actionManager.setDefaultExecute(function (_action, _senders, _target) { + // Get the selected ids descriptor object + var ids = self._selectionMgr.getSelected(); + + // Pass a reference to the actual widget + if (typeof _action.data == 'undefined' || !_action.data) _action.data = {}; + _action.data.nextmatch = self._widget; + + // Call the nm_action function with the ids + nm_action(_action, _senders, _target, ids); + }); + + // Set the 'Select All' handler + var select_all = this._actionManager.getActionById('select_all'); + if(select_all) + { + select_all.set_onExecute(jQuery.proxy(function(action, selected) { + this._selectionMgr.selectAll(); + }, this)); + } + + // Initialize the object manager - look for application + // object manager 1 level deep + var gom = egw_getObjectManager(this.egw.appName,true,1); + if(this._objectManager == null) + { + this._objectManager = gom.addObject( + new egwActionObjectManager(uid, this._actionManager)); + + this._objectManager.handleKeyPress = function(_keyCode, _shift, _ctrl, _alt) { + for(var i = 0; i < self._actionManager.children.length; i++) + { + if(typeof self._actionManager.children[i].shortcut === 'object' && + self._actionManager.children[i].shortcut && + _keyCode == self._actionManager.children[i].shortcut.keyCode) + { + return this.executeActionImplementation( + { + "keyEvent": { + "keyCode": _keyCode, + "shift": _shift, + "ctrl": _ctrl, + "alt": _alt + } + }, "popup", EGW_AO_EXEC_SELECTED); + } + } + return egwActionObject.prototype.handleKeyPress.call(this, _keyCode,_shift,_ctrl,_alt); + } + + } + this._objectManager.flags = this._objectManager.flags + | EGW_AO_FLAG_DEFAULT_FOCUS | EGW_AO_FLAG_IS_CONTAINER; + + this._init_links_dnd(); + + if(this._selectionMgr) + { + // Need to update the action links for every registered row too + for (var uid in this._selectionMgr._registeredRows) + { + // Get the corresponding entry from the registered rows array + var entry = this._selectionMgr._getRegisteredRowsEntry(uid); + if(entry.ao) + { + entry.ao.updateActionLinks(this._actionLinks); + } + } + } + } + + /** + * Automatically add dnd support for linking + */ + _init_links_dnd( ) + { + var mgr = this._actionManager; + var self = this; + + var drop_action = mgr.getActionById('egw_link_drop'); + var drag_action = mgr.getActionById('egw_link_drag'); + var drop_cancel = mgr.getActionById('egw_cancel_drop'); + + if(!this._actionLinks) + { + this._actionLinks = []; + } + + if (!drop_cancel) + { + // Create a generic cancel action in order to cancel drop action + // applied for all apps plus file and link action. + drop_cancel = mgr.addAction('drop', 'egw_cancel_drop', this.egw.lang('Cancel'), egw.image('cancel'), function(){},true); + drop_cancel.set_group('99'); + drop_cancel.acceptedTypes = drop_cancel.acceptedTypes.concat(Object.keys(egw.user('apps')).concat(['link', 'file'])); + this._actionLinks.push (drop_cancel.id); + } + + // Check if this app supports linking + if(!egw.link_get_registry(this.dataStorePrefix || this.egw.appName, 'query') || + egw.link_get_registry(this.dataStorePrefix || this.egw.appName, 'title')) + { + if(drop_action) + { + drop_action.remove(); + if(this._actionLinks.indexOf(drop_action.id) >= 0) + { + this._actionLinks.splice(this._actionLinks.indexOf(drop_action.id),1); + } + } + if(drag_action) + { + drag_action.remove(); + if(this._actionLinks.indexOf(drag_action.id) >= 0) + { + this._actionLinks.splice(this._actionLinks.indexOf(drag_action.id),1); + } + } + return; + } + + // Don't re-add + if(drop_action == null) + { + // Create the drop action that links entries + drop_action = mgr.addAction('drop', 'egw_link_drop', this.egw.lang('Create link'), egw.image('link'), function(action, source, dropped) { + // Extract link IDs + var links = []; + var id = ''; + for(var i = 0; i < source.length; i++) + { + if(!source[i].id) continue; + id = source[i].id.split('::'); + links.push({app: id[0] == 'filemanager' ? 'link' : id[0], id: id[1]}); + } + if(!links.length) + { + return; + } + + // Link the entries + self.egw.json("EGroupware\\Api\\Etemplate\\Widget\\Link::ajax_link", + dropped.id.split('::').concat([links]), + function(result) { + if(result) + { + for (var i=0; i < this._objectManager.selectedChildren.length; i++) + { + this._widget.refresh(this._objectManager.selectedChildren[i].id,'update'); + } + this._widget.egw().message('Linked'); + // Update the target to show the liks + this._widget.refresh(dropped.id,'update'); + } + }, + self, + true, + self + ).sendRequest(); + + },true); + } + if(this._actionLinks.indexOf(drop_action.id) < 0) + { + this._actionLinks.push(drop_action.id); + } + // Accept other links, and files dragged from the filemanager + // This does not handle files dragged from the desktop. They are + // handled by et2_nextmatch, since it needs DOM stuff + if(drop_action.acceptedTypes.indexOf('link') == -1) + { + drop_action.acceptedTypes.push('link'); + } + + // Don't re-add + if(drag_action == null) + { + // Create drag action that allows linking + drag_action = mgr.addAction('drag', 'egw_link_drag', this.egw.lang('link'), 'link', function(action, selected) { + // Drag helper - list titles. Arbitrarily limited to 10. + var helper = jQuery(document.createElement("div")); + for(var i = 0; i < selected.length && i < 10; i++) + { + var id = selected[i].id.split('::'); + var span = jQuery(document.createElement('span')).appendTo(helper); + self.egw.link_title(id[0],id[1], function(title) { + this.append(title); + this.append('
'); + }, span); + } + // As we wanted to have a general defaul helper interface, we return null here and not using customize helper for links + // TODO: Need to decide if we need to create a customized helper interface for links anyway + //return helper; + return null; + },true); + } + if(this._actionLinks.indexOf(drag_action.id) < 0) + { + this._actionLinks.push(drag_action.id); + } + drag_action.set_dragType('link'); + } + + /** + * Set the data cache prefix + * Overridden from the parent to re-check automatically the added link dnd + * since the prefix is used in support detection. + */ + setPrefix( prefix) + { + super.setPrefix(prefix); + + this._init_links_dnd(); + } + + /** + * Overwrites the inherited _destroyCallback function in order to be able + * to free the "rowWidget". + */ + _destroyCallback( _row) + { + // Destroy any widget associated to the row + if (this.entry.widget) + { + this.entry.widget.destroy(); + this.entry.widget = null; + } + + // Call the inherited function + super._destroyCallback(_row); + } + + /** + * Creates the actual data row. + * + * @param _data is an array containing the row data + * @param _tr is the tr into which the data will be inserted + * @param _idx is the index of the row + * @param _entry is the internal row datastructure of the controller, in + * this special case used to store the rowWidget reference, so that it can + * be properly freed. + */ + _rowCallback( _data, _tr, _idx, _entry) + { + // Let the row provider fill in the data row -- store the returned + // rowWidget inside the _entry + _entry.widget = this._rowProvider.getDataRow( + { "content": _data }, _tr, _idx, this); + } + + /** + * Returns the names of action links for a given data row -- currently these are + * always the same links, as we controll enabled/disabled over the row + * classes, unless the row UID is "", then it's an 'empty' row. + * + * The empty row placeholder can still have actions, but nothing that requires + * an actual UID. + * + * @TODO: Currently empty row is just add, need to actually filter somehow. Here + * might not be the right place. + * + * @param _data Object The data for the row + * @param _idx int The row index + * @param _uid String The row's ID + * + * @return Array List of action names that valid for the row + */ + _linkCallback( _data, _idx, _uid) + { + if(_uid.trim() != "") + { + return this._actionLinks; + } + + // No UID, so return a filtered list of actions that doesn't need a UID + var links = []; + try { + links = typeof this._widget.options.settings.placeholder_actions != 'undefined' ? + this._widget.options.settings.placeholder_actions : (this._widget.options.add ? ["add"] : []); + // Make sure that placeholder actions are defined and existed in client-side, + // otherwise do not set them as placeholder. for instance actions with + // attribute hideOnMobile do not get sent to client-side. + var action_search = function(current) { + if (typeof this._widget.options.actions[current] !== 'undefined') return true; + // Check children + for(var action in this._widget.options.actions) + { + action = this._widget.options.actions[action]; + if( action.children && action.children[current]) return true; + } + return false; + }; + links = links.filter(action_search, this); + } catch (e) { + } + + return links; + } + + + /** + * Overridden from the parent to also process any additional data that + * the data source adds, such as readonlys and additonal content. + * For example, non-numeric IDs in rows are added to the content manager + */ + _fetchCallback( _response) + { + var nm = this.self._widget; + if(!nm) + { + // Nextmatch either not connected, or it tried to destroy this + // but the server returned something + return; + } + // Readonlys + // Other stuff + for(var i in _response.rows) + { + if(jQuery.isNumeric(i)) continue; + if(i == 'sel_options') + { + var mgr = nm.getArrayMgr(i); + for(var id in _response.rows.sel_options) + { + mgr.data[id] = _response.rows.sel_options[id]; + var select = nm.getWidgetById(id); + if(select && select.set_select_options) + { + select.set_select_options(_response.rows.sel_options[id]); + } + // Clear rowProvider internal cache so it uses new values + if(id == 'cat_id') + { + this.self._rowProvider.categories = null; + } + // update array mgr so select widgets in row also get refreshed options + nm.getParent().getArrayMgr('sel_options').data[id] = _response.rows.sel_options[id]; + } + } + else + { + var mgr = nm.getArrayMgr('content'); + mgr.data[i] = _response.rows[i]; + + // It's not enough to just update the data, the widgets need to + // be updated too, if there are matching widgets. + var widget = nm.getWidgetById(i); + if(widget && widget.set_value) + { + widget.set_value(mgr.getEntry(i)); + } + } + } + // Might be trying to disable a column + var col_refresh = false; + for(var column_index = 0; column_index < nm.columns.length; column_index++) + { + if(typeof nm.columns[column_index].disabled === 'string' && + nm.columns[column_index].disabled[0] === '@') + { + col_refresh = true; + nm.dataview.columnMgr.getColumnById('col_'+column_index) + .set_visibility( + nm.getArrayMgr('content').parseBoolExpression(nm.columns[column_index].disabled) ? + et2_dataview_column.ET2_COL_VISIBILITY_DISABLED : + nm.columns[column_index].visible + ); + } + } + if(col_refresh) + { + nm.dataview.columnMgr.updated(); + nm.dataview._updateColumns(); + } + + // If we're doing an autorefresh and the count decreases, preserve the + // selection or it will be lost when the grid rows are shuffled. Increases + // are fine though. + if(this.self && this.self.kept_selection == null && + !this.refresh && this.self._grid.getTotalCount() > _response.total) + { + this.self.keepSelection(); + } + + // Call the inherited function + super._fetchCallback.apply(this, arguments); + + // Restore selection, if passed + if(this.self && this.self.kept_selection && this.self._selectionMgr) + { + if(this.self.kept_selection.all) + { + this.self._selectionMgr.selectAll(); + } + for(var i = (this.self.kept_selection.ids.length || 1)-1; i >= 0; i--) + { + // Only keep the selected if they came back in the fetch + if(_response.order.indexOf(this.self.kept_selection.ids[i]) >= 0) + { + this.self._selectionMgr.setSelected(this.self.kept_selection.ids[i],true); + this.self.kept_selection.ids.splice(i,1); + } + else + { + this.self.kept_selection.ids.splice(i,1); + } + } + if(this.self.kept_focus && _response.order.indexOf(this.self.kept_focus) >= 0) + { + this.self._selectionMgr.setFocused(this.self.kept_focus,true); + } + // Re-expanding rows handled in et2_extension_nextmatch_rowProvider + // Expansions might still be valid, so we don't clear them + if(this.self.kept_selection != null && typeof this.self.kept_selection.ids != 'undefined' && this.self.kept_selection.ids.length == 0) + { + this.self.kept_selection = null; + } + this.self.kept_focus = null; + } + } + + /** + * Execute the select callback when the row selection changes + */ + _selectCallback(action,senders) + { + if(typeof senders == "undefined") + { + senders = []; + } + if(!this._widget) return; + + // inform mobile framework about nm selections, need to update status of header objects on selection + if (egwIsMobile()) framework.nm_onselect_ctrl(this._widget, action, senders); + + this._widget.onselect.call(this._widget, action,senders); + } + + /** -- Implementation of et2_IDataProvider -- **/ + + + dataFetch( _queriedRange, _callback, _context) + { + + // Merge the parent id into the _queriedRange if it is set + if (this._parentId !== null) + { + _queriedRange["parent_id"] = this._parentId; + } + + // sub-levels dont have there own _filters object, need to use the one from parent (or it's parents parent) + var obj = this; + while((typeof obj._filters == 'undefined' || jQuery.isEmptyObject(obj._filters)) && obj._parentController) + { + obj = obj._parentController; + } + + // Pass the fetch call to the API, multiplex the data about the + // nextmatch instance into the call. + this.egw.dataFetch( + this._widget.getInstanceManager().etemplate_exec_id || this._execId, + _queriedRange, + obj._filters, + this._widgetId, + _callback, + _context); + } + + dataRegisterUID( _uid, _callback, _context) + { + this.egw.dataRegisterUID(_uid, _callback, _context, + this._widget.getInstanceManager().etemplate_exec_id || this._execId, + this._widgetId + ); + } + + dataUnregisterUID( ) + { + // Overwritten in the constructor + } + +} \ No newline at end of file diff --git a/api/js/etemplate/et2_extension_nextmatch_rowProvider.js b/api/js/etemplate/et2_extension_nextmatch_rowProvider.js index b09c354966..a333c9d148 100644 --- a/api/js/etemplate/et2_extension_nextmatch_rowProvider.js +++ b/api/js/etemplate/et2_extension_nextmatch_rowProvider.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - Class which contains a factory method for rows * @@ -9,666 +10,546 @@ * @copyright Stylite 2012 * @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 - /vendor/bower-asset/jquery/dist/jquery.js; - et2_core_inheritance; - et2_core_interfaces; - et2_core_arrayMgr; - et2_core_widget; + /vendor/bower-asset/jquery/dist/jquery.js; + et2_core_inheritance; + et2_core_interfaces; + et2_core_arrayMgr; + et2_core_widget; + et2_dataview_view_rowProvider; */ - +var et2_core_widget_1 = require("./et2_core_widget"); +var et2_core_arrayMgr_1 = require("./et2_core_arrayMgr"); /** * The row provider contains prototypes (full clonable dom-trees) * for all registered row types. * - * @augments Class */ -var et2_nextmatch_rowProvider = (function(){ "use strict"; return ClassWithAttributes.extend( -{ - /** - * Creates the nextmatch row provider. - * - * @param {et2_nextmatch_rowProvider} _rowProvider - * @param {function} _subgridCallback - * @param {object} _context - * @memberOf et2_nextmatch_rowProvider - */ - init: function (_rowProvider, _subgridCallback, _context) { - // Copy the arguments - this._rowProvider = _rowProvider; - this._subgridCallback = _subgridCallback; - this._context = _context; - - this._createEmptyPrototype(); - }, - - /** - * Creates the data row prototype. - * - * @param _widgets is an array containing the root widget for each column. - * @param _rowData contains the properties of the root "tr" (like its class) - * @param _rootWidget is the parent widget of the data rows (i.e. - * the nextmatch) - */ - setDataRowTemplate: function(_widgets, _rowData, _rootWidget) { - // Copy the root widget - this._rootWidget = _rootWidget; - - // Create the base row - var row = this._rowProvider.getPrototype("default"); - - // Copy the row template - var rowTemplate = { - "row": row[0], - "rowData": _rowData, - "widgets": _widgets, - "root": _rootWidget, - "seperated": null, - "mgrs": _rootWidget.getArrayMgrs() - }; - - // Create the row widget and insert the given widgets into the row - var rowWidget = new et2_nextmatch_rowWidget(rowTemplate.mgrs, row[0]); - rowWidget._parent = _rootWidget; - rowWidget.createWidgets(_widgets); - - // Get the set containing all variable attributes - var variableAttributes = this._getVariableAttributeSet(rowWidget); - - // Filter out all widgets which do not implement the et2_IDetachedDOM - // interface or do not support all attributes listed in the et2_IDetachedDOM - // interface. A warning is issued for all those widgets as they heavily - // degrade the performance of the dataview - var seperated = rowTemplate.seperated = - this._seperateWidgets(variableAttributes); - - // Remove all DOM-Nodes of all widgets inside the "remaining" slot from - // the row-template, then build the access functions for the detachable - // widgets - this._stripTemplateRow(rowTemplate); - this._buildNodeAccessFuncs(rowTemplate); - - // Create the DOM row template - var tmpl = document.createDocumentFragment(); - row.children().each(function() { tmpl.appendChild(this); }); - - this._dataRow = tmpl; - this._template = rowTemplate; - }, - - getDataRow: function(_data, _row, _idx, _controller) { - - // Clone the row template - var row = this._dataRow.cloneNode(true); - - // Create array managers with the given data merged in - var mgrs = et2_arrayMgrs_expand(rowWidget, this._template.mgrs, - _data, _idx); - - // Insert the widgets into the row which do not provide the functions - // to set the _data directly - var rowWidget = null; - if (this._template.seperated.remaining.length > 0) - { - // Transform the variable attributes - for (var i = 0; i < this._template.seperated.remaining.length; i++) - { - var entry = this._template.seperated.remaining[i]; - - for (var j = 0; j < entry.data.length; j++) - { - var set = entry.data[j]; - entry.widget.options[set.attribute] = mgrs["content"].expandName(set.expression); - } - } - - // Create the row widget - var rowWidget = new et2_nextmatch_rowTemplateWidget(this._rootWidget, - row); - - // Let the row widget create the widgets - rowWidget.createWidgets(mgrs, this._template.placeholders); - } - - // Update the content of all other widgets - for (var i = 0; i < this._template.seperated.detachable.length; i++) - { - var entry = this._template.seperated.detachable[i]; - - // Parse the attribute expressions - var data = {}; - for (var j = 0; j < entry.data.length; j++) - { - var set = entry.data[j]; - data[set.attribute] = mgrs["content"].expandName(set.expression); - } - - // Retrieve all DOM-Nodes - var nodes = new Array(entry.nodeFuncs.length); - for (var j = 0; j < nodes.length; j++) - { - // Use the previously compiled node function to get the node - // from the entry - nodes[j] = entry.nodeFuncs[j](row); - } - - // Set the array managers first - entry.widget._mgrs = mgrs; - if (typeof data.id != "undefined") - { - entry.widget.id = data.id; - } - - // Adjust data for that row - entry.widget.transformAttributes.call(entry.widget,data); - - // Call the setDetachedAttributes function - entry.widget.setDetachedAttributes(nodes, data, _data); - } - - // Insert the row into the tr - var tr = _row.getDOMNode(); - tr.appendChild(row); - - // Make the row expandable - if (typeof _data.content["is_parent"] !== "undefined" - && _data.content["is_parent"]) - { - _row.makeExpandable(true, function () { - return this._subgridCallback.call(this._context, - _row, _data, _controller); - }, this); - - // Check for kept expansion, and set the row up to be re-expanded - // Only the top controller tracks expanded, including sub-grids - var top_controller = _controller; - while(top_controller._parentController != null) - { - top_controller = top_controller._parentController; - } - var expansion_index = top_controller.kept_expansion.indexOf( - top_controller.dataStorePrefix + '::' + _data.content[this._context.settings.row_id] - ); - if(top_controller.kept_expansion && expansion_index >=0) - { - top_controller.kept_expansion.splice(expansion_index,1); - // Use a timeout since the DOM nodes might not be finished yet - window.setTimeout(function() { - _row.expansionButton.trigger('click'); - },ET2_GRID_INVALIDATE_TIMEOUT); - } - } - - // Set the row data - this._setRowData(this._template.rowData, tr, mgrs); - - return rowWidget; - }, - - - /** - * Placeholder for empty row - * - * The empty row placeholder is used when there are no results to display. - * This allows the user to still have a drop target, or use actions that - * do not require a row ID, such as 'Add new'. - */ - _createEmptyPrototype: function() { - var label = this._context && this._context.options && this._context.options.settings.placeholder; - - var placeholder = jQuery(document.createElement("td")) - .attr("colspan",this._rowProvider.getColumnCount()) - .css("height","19px") - .text(typeof label != "undefined" && label ? label : egw().lang("No matches found")); - this._rowProvider._prototypes["empty"] = jQuery(document.createElement("tr")) - .addClass("egwGridView_empty") - .append(placeholder); - }, - - /** -- PRIVATE FUNCTIONS -- **/ - - /** - * Returns an array containing objects which have variable attributes - * - * @param {et2_widget} _widget - */ - _getVariableAttributeSet: function(_widget) { - var variableAttributes = []; - - _widget.iterateOver(function(_widget) { - // Create the attribtues - var hasAttr = false; - var widgetData = { - "widget": _widget, - "data": [] - }; - - // Get all attribute values - for (var key in _widget.attributes) - { - if (!_widget.attributes[key].ignore && - typeof _widget.options[key] != "undefined") - { - var val = _widget.options[key]; - - // TODO: Improve detection - if (typeof val == "string" && val.indexOf("$") >= 0) - { - hasAttr = true; - widgetData.data.push({ - "attribute": key, - "expression": val - }); - } - } - } - - // Add the entry if there is any data in it - if (hasAttr) - { - variableAttributes.push(widgetData); - } - - }, this); - - return variableAttributes; - }, - - _seperateWidgets: function(_varAttrs) { - // The detachable array contains all widgets which implement the - // et2_IDetachedDOM interface for all needed attributes - var detachable = []; - - // The remaining array creates all widgets which have to be completely - // cloned when the widget tree is created - var remaining = []; - - // Iterate over the widgets - for (var i = 0; i < _varAttrs.length; i++) - { - var widget = _varAttrs[i].widget; - - // Check whether the widget parents are not allready in the "remaining" - // slot - if this is the case do not include the widget at all. - var insertWidget = true; - var checkWidget = function (_widget) { - if (_widget.parent != null) - { - for (var i = 0; i < remaining.length; i++) - { - if (remaining[i].widget == _widget.parent) - { - insertWidget = false; - return; - } - } - - checkWidget(_widget.parent); - } - }; - checkWidget(widget); - - // Handle the next widget if this one should not be included. - if (!insertWidget) - { - continue; - } - - // Check whether the widget implements the et2_IDetachedDOM interface - var isDetachable = false; - if (widget.implements(et2_IDetachedDOM)) - { - // Get all attributes the widgets supports to be set in the - // "detached" mode - var supportedAttrs = []; - widget.getDetachedAttributes(supportedAttrs); - supportedAttrs.push("id"); - isDetachable = true; - - for (var j = 0; j < _varAttrs[i].data.length/* && isDetachable*/; j++) - { - var data = _varAttrs[i].data[j]; - - var supportsAttr = supportedAttrs.indexOf(data.attribute) != -1; - - if (!supportsAttr) - { - egw.debug("warn", "et2_IDetachedDOM widget " + - widget._type + " does not support " + data.attribute); - } - - isDetachable &= supportsAttr; - } - } - - // Insert the widget into the correct slot - if (isDetachable) - { - detachable.push(_varAttrs[i]); - } - else - { - remaining.push(_varAttrs[i]); - } - } - - return { - "detachable": detachable, - "remaining": remaining - }; - }, - - /** - * Removes to DOM code for all widgets in the "remaining" slot - * - * @param {object} _rowTemplate - */ - _stripTemplateRow: function(_rowTemplate) { - _rowTemplate.placeholders = []; - - for (var i = 0; i < _rowTemplate.seperated.remaining.length; i++) - { - var entry = _rowTemplate.seperated.remaining[i]; - - // Issue a warning - widgets which do not implement et2_IDOMNode - // are very slow - egw.debug("warn", "Non-clonable widget '"+ entry.widget._type + "' in dataview row - this " + - "might be slow", entry); - - // Set the placeholder for the entry to null - entry.placeholder = null; - - // Get the outer DOM-Node of the widget - if (entry.widget.implements(et2_IDOMNode)) - { - var node = entry.widget.getDOMNode(entry.widget); - - if (node && node.parentNode) - { - // Get the parent node and replace the node with a placeholder - entry.placeholder = document.createElement("span"); - node.parentNode.replaceChild(entry.placeholder, node); - _rowTemplate.placeholders.push({ - "widget": entry.widget, - "func": this._compileDOMAccessFunc(_rowTemplate.row, - entry.placeholder) - }); - } - } - } - }, - - _nodeIndex: function(_node) { - if(_node.parentNode == null) - { - return 0; - } - for (var i = 0; i < _node.parentNode.childNodes.length; i++) - { - if (_node.parentNode.childNodes[i] == _node) - { - return i; - } - } - - return -1; - }, - - /** - * Returns a function which does a relative access on the given DOM-Node - * - * @param {DOMElement} _root - * @param {DOMElement} _target - */ - _compileDOMAccessFunc: function(_root, _target) { - function recordPath(_root, _target, _path) - { - if (typeof _path == "undefined") - { - _path = []; - } - - if (_root != _target && _target) - { - // Get the index of _target in its parent node - var idx = this._nodeIndex(_target); - if (idx >= 0) - { - // Add the access selector - _path.unshift("childNodes[" + idx + "]"); - - // Record the remaining path - return recordPath.call(this, _root, _target.parentNode, _path); - } - - throw("Internal error while compiling DOM access function."); - } - else - { - _path.unshift("_node"); - return "return " + _path.join(".") + ";"; - } - } - - return new Function("_node", recordPath.call(this, _root, _target)); - }, - - /** - * Builds relative paths to the DOM-Nodes and compiles fast-access functions - * - * @param {object} _rowTemplate - */ - _buildNodeAccessFuncs: function(_rowTemplate) { - for (var i = 0; i < _rowTemplate.seperated.detachable.length; i++) - { - var entry = _rowTemplate.seperated.detachable[i]; - - // Get all needed nodes from the widget - var nodes = entry.widget.getDetachedNodes(); - var nodeFuncs = entry.nodeFuncs = new Array(nodes.length); - - // Record the path to each DOM-Node - for (var j = 0; j < nodes.length; j++) - { - nodeFuncs[j] = this._compileDOMAccessFunc(_rowTemplate.row, - nodes[j]); - } - } - }, - - /** - * Match category-ids from class attribute eg. "cat_15" or "123,456,789 " - * - * Make sure to not match numbers inside other class-names. - * - * We can NOT use something like /(^| |,|cat_)([0-9]+)( |,|$)/g as it wont find all cats in "123,456,789 "! - */ - cat_regexp: /(^| |,|cat_)([0-9]+)/g, - /** - * Regular expression used to filter out non-nummerical chars from above matches - */ - cat_cleanup: /[^0-9]/g, - - /** - * Applies additional row data (like the class) to the tr - * - * @param {object} _data - * @param {DOMElement} _tr - * @param {object} _mgrs - */ - _setRowData: function (_data, _tr, _mgrs) { - // TODO: Implement other fields than "class" - if (_data["class"]) - { - var classes = _mgrs["content"].expandName(_data["class"]); - - // Get fancy with categories - var cats = []; - // Assume any numeric class is a category - if(_data["class"].indexOf("cat") !== -1 || classes.match(/[0-9]+/)) - { - // Accept either cat, cat_id or category as ID, and look there for category settings - var category_location = _data["class"].match(/(cat(_id|egory)?)/); - if(category_location) category_location = category_location[0]; - - cats = classes.match(this.cat_regexp) || []; - classes = classes.replace(this.cat_regexp, ''); - - // Set category class - for(var i = 0; i < cats.length; i++) - { - // Need cat_, classes can't start with a number - var cat_id = cats[i].replace(this.cat_cleanup, ''); - var cat_class = 'cat_'+cat_id; - - classes += ' '+cat_class; - } - classes += " row_category"; - } - classes += " row"; - _tr.setAttribute("class", classes); - } - if(_data['valign']) - { - var align = _mgrs["content"].expandName(_data["valign"]); - _tr.setAttribute("valign", align); - } - } - -});}).call(this); - +var et2_nextmatch_rowProvider = /** @class */ (function () { + /** + * Creates the nextmatch row provider. + * + * @param {et2_nextmatch_rowProvider} _rowProvider + * @param {function} _subgridCallback + * @param {object} _context + * @memberOf et2_nextmatch_rowProvider + */ + function et2_nextmatch_rowProvider(_rowProvider, _subgridCallback, _context) { + /** + * Match category-ids from class attribute eg. "cat_15" or "123,456,789 " + * + * Make sure to not match numbers inside other class-names. + * + * We can NOT use something like /(^| |,|cat_)([0-9]+)( |,|$)/g as it wont find all cats in "123,456,789 "! + */ + this.cat_regexp = /(^| |,|cat_)([0-9]+)/g; + /** + * Regular expression used to filter out non-nummerical chars from above matches + */ + this.cat_cleanup = /[^0-9]/g; + // Copy the arguments + this._rowProvider = _rowProvider; + this._subgridCallback = _subgridCallback; + this._context = _context; + this._createEmptyPrototype(); + } + et2_nextmatch_rowProvider.prototype.destroy = function () { + this._rowProvider.destroy(); + this._subgridCallback = null; + this._context = null; + this._dataRow = null; + }; + /** + * Creates the data row prototype. + * + * @param _widgets is an array containing the root widget for each column. + * @param _rowData contains the properties of the root "tr" (like its class) + * @param _rootWidget is the parent widget of the data rows (i.e. + * the nextmatch) + */ + et2_nextmatch_rowProvider.prototype.setDataRowTemplate = function (_widgets, _rowData, _rootWidget) { + // Copy the root widget + this._rootWidget = _rootWidget; + // Create the base row + var row = this._rowProvider.getPrototype("default"); + // Copy the row template + var rowTemplate = { + "row": row[0], + "rowData": _rowData, + "widgets": _widgets, + "root": _rootWidget, + "seperated": null, + "mgrs": _rootWidget.getArrayMgrs() + }; + // Create the row widget and insert the given widgets into the row + var rowWidget = new et2_nextmatch_rowWidget(rowTemplate.mgrs, row[0]); + rowWidget._parent = _rootWidget; + rowWidget.createWidgets(_widgets); + // Get the set containing all variable attributes + var variableAttributes = this._getVariableAttributeSet(rowWidget); + // Filter out all widgets which do not implement the et2_IDetachedDOM + // interface or do not support all attributes listed in the et2_IDetachedDOM + // interface. A warning is issued for all those widgets as they heavily + // degrade the performance of the dataview + var seperated = rowTemplate.seperated = + this._seperateWidgets(variableAttributes); + // Remove all DOM-Nodes of all widgets inside the "remaining" slot from + // the row-template, then build the access functions for the detachable + // widgets + this._stripTemplateRow(rowTemplate); + this._buildNodeAccessFuncs(rowTemplate); + // Create the DOM row template + var tmpl = document.createDocumentFragment(); + row.children().each(function () { tmpl.appendChild(this); }); + this._dataRow = tmpl; + this._template = rowTemplate; + }; + et2_nextmatch_rowProvider.prototype.getDataRow = function (_data, _row, _idx, _controller) { + // Clone the row template + var row = this._dataRow.cloneNode(true); + // Create array managers with the given data merged in + var mgrs = et2_core_arrayMgr_1.et2_arrayMgrs_expand(rowWidget, this._template.mgrs, _data, _idx); + // Insert the widgets into the row which do not provide the functions + // to set the _data directly + var rowWidget = null; + if (this._template.seperated.remaining.length > 0) { + // Transform the variable attributes + for (var i = 0; i < this._template.seperated.remaining.length; i++) { + var entry = this._template.seperated.remaining[i]; + for (var j = 0; j < entry.data.length; j++) { + var set = entry.data[j]; + entry.widget.options[set.attribute] = mgrs["content"].expandName(set.expression); + } + } + // Create the row widget + var rowWidget = new et2_nextmatch_rowTemplateWidget(this._rootWidget, row); + // Let the row widget create the widgets + rowWidget.createWidgets(mgrs, this._template.placeholders); + } + // Update the content of all other widgets + for (var i = 0; i < this._template.seperated.detachable.length; i++) { + var entry = this._template.seperated.detachable[i]; + // Parse the attribute expressions + var data = {}; + for (var j = 0; j < entry.data.length; j++) { + var set = entry.data[j]; + data[set.attribute] = mgrs["content"].expandName(set.expression); + } + // Retrieve all DOM-Nodes + var nodes = new Array(entry.nodeFuncs.length); + for (var j = 0; j < nodes.length; j++) { + // Use the previously compiled node function to get the node + // from the entry + nodes[j] = entry.nodeFuncs[j](row); + } + // Set the array managers first + entry.widget._mgrs = mgrs; + if (typeof data.id != "undefined") { + entry.widget.id = data.id; + } + // Adjust data for that row + entry.widget.transformAttributes.call(entry.widget, data); + // Call the setDetachedAttributes function + entry.widget.setDetachedAttributes(nodes, data, _data); + } + // Insert the row into the tr + var tr = _row.getDOMNode(); + tr.appendChild(row); + // Make the row expandable + if (typeof _data.content["is_parent"] !== "undefined" + && _data.content["is_parent"]) { + _row.makeExpandable(true, function () { + return this._subgridCallback.call(this._context, _row, _data, _controller); + }, this); + // Check for kept expansion, and set the row up to be re-expanded + // Only the top controller tracks expanded, including sub-grids + var top_controller = _controller; + while (top_controller._parentController != null) { + top_controller = top_controller._parentController; + } + var expansion_index = top_controller.kept_expansion.indexOf(top_controller.dataStorePrefix + '::' + _data.content[this._context.settings.row_id]); + if (top_controller.kept_expansion && expansion_index >= 0) { + top_controller.kept_expansion.splice(expansion_index, 1); + // Use a timeout since the DOM nodes might not be finished yet + window.setTimeout(function () { + _row.expansionButton.trigger('click'); + }, et2_dataview_grid.ET2_GRID_INVALIDATE_TIMEOUT); + } + } + // Set the row data + this._setRowData(this._template.rowData, tr, mgrs); + return rowWidget; + }; + /** + * Placeholder for empty row + * + * The empty row placeholder is used when there are no results to display. + * This allows the user to still have a drop target, or use actions that + * do not require a row ID, such as 'Add new'. + */ + et2_nextmatch_rowProvider.prototype._createEmptyPrototype = function () { + var label = this._context && this._context.options && this._context.options.settings.placeholder; + var placeholder = jQuery(document.createElement("td")) + .attr("colspan", this._rowProvider.getColumnCount()) + .css("height", "19px") + .text(typeof label != "undefined" && label ? label : egw().lang("No matches found")); + this._rowProvider._prototypes["empty"] = jQuery(document.createElement("tr")) + .addClass("egwGridView_empty") + .append(placeholder); + }; + /** -- PRIVATE FUNCTIONS -- **/ + /** + * Returns an array containing objects which have variable attributes + * + * @param {et2_widget} _widget + */ + et2_nextmatch_rowProvider.prototype._getVariableAttributeSet = function (_widget) { + var variableAttributes = []; + _widget.iterateOver(function (_widget) { + // Create the attribtues + var hasAttr = false; + var widgetData = { + "widget": _widget, + "data": [] + }; + // Get all attribute values + for (var key in _widget.attributes) { + if (!_widget.attributes[key].ignore && + typeof _widget.options[key] != "undefined") { + var val = _widget.options[key]; + // TODO: Improve detection + if (typeof val == "string" && val.indexOf("$") >= 0) { + hasAttr = true; + widgetData.data.push({ + "attribute": key, + "expression": val + }); + } + } + } + // Add the entry if there is any data in it + if (hasAttr) { + variableAttributes.push(widgetData); + } + }, this); + return variableAttributes; + }; + et2_nextmatch_rowProvider.prototype._seperateWidgets = function (_varAttrs) { + // The detachable array contains all widgets which implement the + // et2_IDetachedDOM interface for all needed attributes + var detachable = []; + // The remaining array creates all widgets which have to be completely + // cloned when the widget tree is created + var remaining = []; + // Iterate over the widgets + for (var i = 0; i < _varAttrs.length; i++) { + var widget = _varAttrs[i].widget; + // Check whether the widget parents are not allready in the "remaining" + // slot - if this is the case do not include the widget at all. + var insertWidget = true; + var checkWidget = function (_widget) { + if (_widget.parent != null) { + for (var i = 0; i < remaining.length; i++) { + if (remaining[i].widget == _widget.parent) { + insertWidget = false; + return; + } + } + checkWidget(_widget.parent); + } + }; + checkWidget(widget); + // Handle the next widget if this one should not be included. + if (!insertWidget) { + continue; + } + // Check whether the widget implements the et2_IDetachedDOM interface + var isDetachable = false; + if (widget.implements(et2_IDetachedDOM)) { + // Get all attributes the widgets supports to be set in the + // "detached" mode + var supportedAttrs = []; + widget.getDetachedAttributes(supportedAttrs); + supportedAttrs.push("id"); + isDetachable = true; + for (var j = 0; j < _varAttrs[i].data.length /* && isDetachable*/; j++) { + var data = _varAttrs[i].data[j]; + var supportsAttr = supportedAttrs.indexOf(data.attribute) != -1; + if (!supportsAttr) { + egw.debug("warn", "et2_IDetachedDOM widget " + + widget._type + " does not support " + data.attribute); + } + isDetachable = isDetachable && supportsAttr; + } + } + // Insert the widget into the correct slot + if (isDetachable) { + detachable.push(_varAttrs[i]); + } + else { + remaining.push(_varAttrs[i]); + } + } + return { + "detachable": detachable, + "remaining": remaining + }; + }; + /** + * Removes to DOM code for all widgets in the "remaining" slot + * + * @param {object} _rowTemplate + */ + et2_nextmatch_rowProvider.prototype._stripTemplateRow = function (_rowTemplate) { + _rowTemplate.placeholders = []; + for (var i = 0; i < _rowTemplate.seperated.remaining.length; i++) { + var entry = _rowTemplate.seperated.remaining[i]; + // Issue a warning - widgets which do not implement et2_IDOMNode + // are very slow + egw.debug("warn", "Non-clonable widget '" + entry.widget._type + "' in dataview row - this " + + "might be slow", entry); + // Set the placeholder for the entry to null + entry.placeholder = null; + // Get the outer DOM-Node of the widget + if (entry.widget.implements(et2_IDOMNode)) { + var node = entry.widget.getDOMNode(entry.widget); + if (node && node.parentNode) { + // Get the parent node and replace the node with a placeholder + entry.placeholder = document.createElement("span"); + node.parentNode.replaceChild(entry.placeholder, node); + _rowTemplate.placeholders.push({ + "widget": entry.widget, + "func": this._compileDOMAccessFunc(_rowTemplate.row, entry.placeholder) + }); + } + } + } + }; + et2_nextmatch_rowProvider.prototype._nodeIndex = function (_node) { + if (_node.parentNode == null) { + return 0; + } + for (var i = 0; i < _node.parentNode.childNodes.length; i++) { + if (_node.parentNode.childNodes[i] == _node) { + return i; + } + } + return -1; + }; + /** + * Returns a function which does a relative access on the given DOM-Node + * + * @param {DOMElement} _root + * @param {DOMElement} _target + */ + et2_nextmatch_rowProvider.prototype._compileDOMAccessFunc = function (_root, _target) { + function recordPath(_root, _target, _path) { + if (typeof _path == "undefined") { + _path = []; + } + if (_root != _target && _target) { + // Get the index of _target in its parent node + var idx = this._nodeIndex(_target); + if (idx >= 0) { + // Add the access selector + _path.unshift("childNodes[" + idx + "]"); + // Record the remaining path + return recordPath.call(this, _root, _target.parentNode, _path); + } + throw ("Internal error while compiling DOM access function."); + } + else { + _path.unshift("_node"); + return "return " + _path.join(".") + ";"; + } + } + return new Function("_node", recordPath.call(this, _root, _target)); + }; + /** + * Builds relative paths to the DOM-Nodes and compiles fast-access functions + * + * @param {object} _rowTemplate + */ + et2_nextmatch_rowProvider.prototype._buildNodeAccessFuncs = function (_rowTemplate) { + for (var i = 0; i < _rowTemplate.seperated.detachable.length; i++) { + var entry = _rowTemplate.seperated.detachable[i]; + // Get all needed nodes from the widget + var nodes = entry.widget.getDetachedNodes(); + var nodeFuncs = entry.nodeFuncs = new Array(nodes.length); + // Record the path to each DOM-Node + for (var j = 0; j < nodes.length; j++) { + nodeFuncs[j] = this._compileDOMAccessFunc(_rowTemplate.row, nodes[j]); + } + } + }; + /** + * Applies additional row data (like the class) to the tr + * + * @param {object} _data + * @param {DOMElement} _tr + * @param {object} _mgrs + */ + et2_nextmatch_rowProvider.prototype._setRowData = function (_data, _tr, _mgrs) { + // TODO: Implement other fields than "class" + if (_data["class"]) { + var classes = _mgrs["content"].expandName(_data["class"]); + // Get fancy with categories + var cats = []; + // Assume any numeric class is a category + if (_data["class"].indexOf("cat") !== -1 || classes.match(/[0-9]+/)) { + // Accept either cat, cat_id or category as ID, and look there for category settings + var category_location = _data["class"].match(/(cat(_id|egory)?)/); + if (category_location) + category_location = category_location[0]; + cats = classes.match(this.cat_regexp) || []; + classes = classes.replace(this.cat_regexp, ''); + // Set category class + for (var i = 0; i < cats.length; i++) { + // Need cat_, classes can't start with a number + var cat_id = cats[i].replace(this.cat_cleanup, ''); + var cat_class = 'cat_' + cat_id; + classes += ' ' + cat_class; + } + classes += " row_category"; + } + classes += " row"; + _tr.setAttribute("class", classes); + } + if (_data['valign']) { + var align = _mgrs["content"].expandName(_data["valign"]); + _tr.setAttribute("valign", align); + } + }; + return et2_nextmatch_rowProvider; +}()); +exports.et2_nextmatch_rowProvider = et2_nextmatch_rowProvider; /** * @augments et2_widget */ -var et2_nextmatch_rowWidget = (function(){ "use strict"; return et2_widget.extend(et2_IDOMNode, -{ - /** - * Constructor - * - * @param _mgrs - * @param _row - * @memberOf et2_nextmatch_rowWidget - */ - init: function(_mgrs, _row) { - // Call the parent constructor with some dummy attributes - this._super(null, {"id": "", "type": "rowWidget"}); - - // Initialize some variables - this._widgets = []; - - // Copy the given DOM node and the content arrays - this._mgrs = _mgrs; - this._row = _row; - }, - - /** - * Copies the given array manager and clones the given widgets and inserts - * them into the row which has been passed in the constructor. - * - * @param {array} _widgets - */ - createWidgets: function(_widgets) { - // Clone the given the widgets with this element as parent - this._widgets = new Array(_widgets.length); - for (var i = 0; i < _widgets.length; i++) - { - // Disabled columns might be missing widget - skip it - if(!_widgets[i]) continue; - - this._widgets[i] = _widgets[i].clone(this); - this._widgets[i].loadingFinished(); - // Set column alignment from widget - if(this._widgets[i].align) - { - this._row.childNodes[i].align = this._widgets[i].align; - } - } - }, - - /** - * Returns the column node for the given sender - * - * @param {et2_widget} _sender - * @return {DOMElement} - */ - getDOMNode: function(_sender) { - for (var i = 0; i < this._widgets.length; i++) - { - if (this._widgets[i] == _sender) - { - return this._row.childNodes[i].childNodes[0]; // Return the i-th td tag - } - } - - return null; - } - -});}).call(this); - +var et2_nextmatch_rowWidget = /** @class */ (function (_super) { + __extends(et2_nextmatch_rowWidget, _super); + /** + * Constructor + * + * @param _mgrs + * @param _row + * @memberOf et2_nextmatch_rowWidget + */ + function et2_nextmatch_rowWidget(_mgrs, _row) { + var _this = + // Call the parent constructor with some dummy attributes + _super.call(this, null, { "id": "", "type": "rowWidget" }) || this; + // Initialize some variables + _this._widgets = []; + // Copy the given DOM node and the content arrays + _this._mgrs = _mgrs; + _this._row = _row; + return _this; + } + /** + * Copies the given array manager and clones the given widgets and inserts + * them into the row which has been passed in the constructor. + * + * @param {array} _widgets + */ + et2_nextmatch_rowWidget.prototype.createWidgets = function (_widgets) { + // Clone the given the widgets with this element as parent + this._widgets = new Array(_widgets.length); + for (var i = 0; i < _widgets.length; i++) { + // Disabled columns might be missing widget - skip it + if (!_widgets[i]) + continue; + this._widgets[i] = _widgets[i].clone(this); + this._widgets[i].loadingFinished(); + // Set column alignment from widget + if (this._widgets[i].align) { + this._row.childNodes[i].align = this._widgets[i].align; + } + } + }; + /** + * Returns the column node for the given sender + * + * @param {et2_widget} _sender + * @return {DOMElement} + */ + et2_nextmatch_rowWidget.prototype.getDOMNode = function (_sender) { + for (var i = 0; i < this._widgets.length; i++) { + if (this._widgets[i] == _sender) { + return this._row.childNodes[i].childNodes[0]; // Return the i-th td tag + } + } + return null; + }; + return et2_nextmatch_rowWidget; +}(et2_core_widget_1.et2_widget)); /** * @augments et2_widget */ -var et2_nextmatch_rowTemplateWidget = (function(){ "use strict"; return et2_widget.extend(et2_IDOMNode, -{ - /** - * Constructor - * - * @param _root - * @param _row - * @memberOf et2_nextmatch_rowTemplateWidget - */ - init: function(_root, _row) { - // Call the parent constructor with some dummy attributes - this._super(null, {"id": "", "type": "rowTemplateWidget"}); - - this._root = _root; - this._mgrs = {}; - this._row = _row; - - // Set parent to root widget, so sub-widget calls still work - this._parent = _root; - - // Clone the widgets inside the placeholders array - this._widgets = []; - }, - - createWidgets: function(_mgrs, _widgets) { - // Set the array managers - don't use setArrayMgrs here as this creates - // an unnecessary copy of the object - this._mgrs = _mgrs; - - this._widgets = new Array(_widgets.length); - for (var i = 0; i < _widgets.length; i++) - { - this._row.childNodes[0].childNodes[0]; - - this._widgets[i] = { - "widget": _widgets[i].widget.clone(this), - "node": _widgets[i].func(this._row) - }; - this._widgets[i].widget.loadingFinished(); - } - }, - - /** - * Returns the column node for the given sender - * - * @param {et2_widget} _sender - * @return {DOMElement} - */ - getDOMNode: function(_sender) { - - for (var i = 0; i < this._widgets.length; i++) - { - if (this._widgets[i].widget == _sender) - { - return this._widgets[i].node; - } - } - - return null; - } - -});}).call(this); - +var et2_nextmatch_rowTemplateWidget = /** @class */ (function (_super) { + __extends(et2_nextmatch_rowTemplateWidget, _super); + /** + * Constructor + * + * @param _root + * @param _row + * @memberOf et2_nextmatch_rowTemplateWidget + */ + function et2_nextmatch_rowTemplateWidget(_root, _row) { + var _this = + // Call the parent constructor with some dummy attributes + _super.call(this, null, { "id": "", "type": "rowTemplateWidget" }) || this; + _this._root = _root; + _this._mgrs = {}; + _this._row = _row; + // Set parent to root widget, so sub-widget calls still work + _this._parent = _root; + // Clone the widgets inside the placeholders array + _this._widgets = []; + return _this; + } + et2_nextmatch_rowTemplateWidget.prototype.createWidgets = function (_mgrs, _widgets) { + // Set the array managers - don't use setArrayMgrs here as this creates + // an unnecessary copy of the object + this._mgrs = _mgrs; + this._widgets = new Array(_widgets.length); + for (var i = 0; i < _widgets.length; i++) { + this._row.childNodes[0].childNodes[0]; + this._widgets[i] = { + "widget": _widgets[i].widget.clone(this), + "node": _widgets[i].func(this._row) + }; + this._widgets[i].widget.loadingFinished(); + } + }; + /** + * Returns the column node for the given sender + * + * @param {et2_widget} _sender + * @return {DOMElement} + */ + et2_nextmatch_rowTemplateWidget.prototype.getDOMNode = function (_sender) { + for (var i = 0; i < this._widgets.length; i++) { + if (this._widgets[i].widget == _sender) { + return this._widgets[i].node; + } + } + return null; + }; + return et2_nextmatch_rowTemplateWidget; +}(et2_core_widget_1.et2_widget)); +//# sourceMappingURL=et2_extension_nextmatch_rowProvider.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_extension_nextmatch_rowProvider.ts b/api/js/etemplate/et2_extension_nextmatch_rowProvider.ts new file mode 100644 index 0000000000..0401d7a4f9 --- /dev/null +++ b/api/js/etemplate/et2_extension_nextmatch_rowProvider.ts @@ -0,0 +1,714 @@ +/** + * EGroupware eTemplate2 - Class which contains a factory method for rows + * + * @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 2012 + * @version $Id$ + */ + +/*egw:uses + /vendor/bower-asset/jquery/dist/jquery.js; + et2_core_inheritance; + et2_core_interfaces; + et2_core_arrayMgr; + et2_core_widget; + et2_dataview_view_rowProvider; +*/ + +import {et2_widget} from "./et2_core_widget"; +import {et2_arrayMgrs_expand} from "./et2_core_arrayMgr"; + +/** + * The row provider contains prototypes (full clonable dom-trees) + * for all registered row types. + * + */ +export class et2_nextmatch_rowProvider +{ + private _rowProvider: any; + private _subgridCallback: any; + private _context: any; + private _rootWidget: any; + private _template: any; + private _dataRow: any; + /** + * Creates the nextmatch row provider. + * + * @param {et2_nextmatch_rowProvider} _rowProvider + * @param {function} _subgridCallback + * @param {object} _context + * @memberOf et2_nextmatch_rowProvider + */ + constructor( _rowProvider, _subgridCallback, _context) + { + + + // Copy the arguments + this._rowProvider = _rowProvider; + this._subgridCallback = _subgridCallback; + this._context = _context; + + this._createEmptyPrototype(); + } + + destroy() + { + this._rowProvider.destroy(); + this._subgridCallback = null; + this._context = null; + this._dataRow = null; + } + + /** + * Creates the data row prototype. + * + * @param _widgets is an array containing the root widget for each column. + * @param _rowData contains the properties of the root "tr" (like its class) + * @param _rootWidget is the parent widget of the data rows (i.e. + * the nextmatch) + */ + setDataRowTemplate( _widgets, _rowData, _rootWidget) + { + // Copy the root widget + this._rootWidget = _rootWidget; + + // Create the base row + var row = this._rowProvider.getPrototype("default"); + + // Copy the row template + var rowTemplate = { + "row": row[0], + "rowData": _rowData, + "widgets": _widgets, + "root": _rootWidget, + "seperated": null, + "mgrs": _rootWidget.getArrayMgrs() + }; + + // Create the row widget and insert the given widgets into the row + var rowWidget = new et2_nextmatch_rowWidget(rowTemplate.mgrs, row[0]); + rowWidget._parent = _rootWidget; + rowWidget.createWidgets(_widgets); + + // Get the set containing all variable attributes + var variableAttributes = this._getVariableAttributeSet(rowWidget); + + // Filter out all widgets which do not implement the et2_IDetachedDOM + // interface or do not support all attributes listed in the et2_IDetachedDOM + // interface. A warning is issued for all those widgets as they heavily + // degrade the performance of the dataview + var seperated = rowTemplate.seperated = + this._seperateWidgets(variableAttributes); + + // Remove all DOM-Nodes of all widgets inside the "remaining" slot from + // the row-template, then build the access functions for the detachable + // widgets + this._stripTemplateRow(rowTemplate); + this._buildNodeAccessFuncs(rowTemplate); + + // Create the DOM row template + var tmpl = document.createDocumentFragment(); + row.children().each(function() { tmpl.appendChild(this); }); + + this._dataRow = tmpl; + this._template = rowTemplate; + } + + getDataRow( _data : any, _row, _idx, _controller) + { + + // Clone the row template + var row = this._dataRow.cloneNode(true); + + // Create array managers with the given data merged in + var mgrs = et2_arrayMgrs_expand(rowWidget, this._template.mgrs, + _data, _idx); + + // Insert the widgets into the row which do not provide the functions + // to set the _data directly + var rowWidget : et2_nextmatch_rowTemplateWidget = null; + if (this._template.seperated.remaining.length > 0) + { + // Transform the variable attributes + for (var i = 0; i < this._template.seperated.remaining.length; i++) + { + var entry = this._template.seperated.remaining[i]; + + for (var j = 0; j < entry.data.length; j++) + { + var set = entry.data[j]; + entry.widget.options[set.attribute] = mgrs["content"].expandName(set.expression); + } + } + + // Create the row widget + var rowWidget = new et2_nextmatch_rowTemplateWidget(this._rootWidget, + row); + + // Let the row widget create the widgets + rowWidget.createWidgets(mgrs, this._template.placeholders); + } + + // Update the content of all other widgets + for (var i = 0; i < this._template.seperated.detachable.length; i++) + { + var entry = this._template.seperated.detachable[i]; + + // Parse the attribute expressions + var data : any = {}; + for (var j = 0; j < entry.data.length; j++) + { + var set = entry.data[j]; + data[set.attribute] = mgrs["content"].expandName(set.expression); + } + + // Retrieve all DOM-Nodes + var nodes = new Array(entry.nodeFuncs.length); + for (var j = 0; j < nodes.length; j++) + { + // Use the previously compiled node function to get the node + // from the entry + nodes[j] = entry.nodeFuncs[j](row); + } + + // Set the array managers first + entry.widget._mgrs = mgrs; + if (typeof data.id != "undefined") + { + entry.widget.id = data.id; + } + + // Adjust data for that row + entry.widget.transformAttributes.call(entry.widget,data); + + // Call the setDetachedAttributes function + entry.widget.setDetachedAttributes(nodes, data, _data); + } + + // Insert the row into the tr + var tr = _row.getDOMNode(); + tr.appendChild(row); + + // Make the row expandable + if (typeof _data.content["is_parent"] !== "undefined" + && _data.content["is_parent"]) + { + _row.makeExpandable(true, function () { + return this._subgridCallback.call(this._context, + _row, _data, _controller); + }, this); + + // Check for kept expansion, and set the row up to be re-expanded + // Only the top controller tracks expanded, including sub-grids + var top_controller = _controller; + while(top_controller._parentController != null) + { + top_controller = top_controller._parentController; + } + var expansion_index = top_controller.kept_expansion.indexOf( + top_controller.dataStorePrefix + '::' + _data.content[this._context.settings.row_id] + ); + if(top_controller.kept_expansion && expansion_index >=0) + { + top_controller.kept_expansion.splice(expansion_index,1); + // Use a timeout since the DOM nodes might not be finished yet + window.setTimeout(function() { + _row.expansionButton.trigger('click'); + },et2_dataview_grid.ET2_GRID_INVALIDATE_TIMEOUT); + } + } + + // Set the row data + this._setRowData(this._template.rowData, tr, mgrs); + + return rowWidget; + } + + + /** + * Placeholder for empty row + * + * The empty row placeholder is used when there are no results to display. + * This allows the user to still have a drop target, or use actions that + * do not require a row ID, such as 'Add new'. + */ + _createEmptyPrototype( ) + { + var label = this._context && this._context.options && this._context.options.settings.placeholder; + + var placeholder = jQuery(document.createElement("td")) + .attr("colspan",this._rowProvider.getColumnCount()) + .css("height","19px") + .text(typeof label != "undefined" && label ? label : egw().lang("No matches found")); + this._rowProvider._prototypes["empty"] = jQuery(document.createElement("tr")) + .addClass("egwGridView_empty") + .append(placeholder); + } + + /** -- PRIVATE FUNCTIONS -- **/ + + /** + * Returns an array containing objects which have variable attributes + * + * @param {et2_widget} _widget + */ + _getVariableAttributeSet( _widget) + { + var variableAttributes = []; + + _widget.iterateOver(function(_widget) { + // Create the attribtues + var hasAttr = false; + var widgetData = { + "widget": _widget, + "data": [] + }; + + // Get all attribute values + for (var key in _widget.attributes) + { + if (!_widget.attributes[key].ignore && + typeof _widget.options[key] != "undefined") + { + var val = _widget.options[key]; + + // TODO: Improve detection + if (typeof val == "string" && val.indexOf("$") >= 0) + { + hasAttr = true; + widgetData.data.push({ + "attribute": key, + "expression": val + }); + } + } + } + + // Add the entry if there is any data in it + if (hasAttr) + { + variableAttributes.push(widgetData); + } + + }, this); + + return variableAttributes; + } + + _seperateWidgets( _varAttrs) + { + // The detachable array contains all widgets which implement the + // et2_IDetachedDOM interface for all needed attributes + var detachable = []; + + // The remaining array creates all widgets which have to be completely + // cloned when the widget tree is created + var remaining = []; + + // Iterate over the widgets + for (var i = 0; i < _varAttrs.length; i++) + { + var widget = _varAttrs[i].widget; + + // Check whether the widget parents are not allready in the "remaining" + // slot - if this is the case do not include the widget at all. + var insertWidget = true; + var checkWidget = function (_widget) { + if (_widget.parent != null) + { + for (var i = 0; i < remaining.length; i++) + { + if (remaining[i].widget == _widget.parent) + { + insertWidget = false; + return; + } + } + + checkWidget(_widget.parent); + } + }; + checkWidget(widget); + + // Handle the next widget if this one should not be included. + if (!insertWidget) + { + continue; + } + + // Check whether the widget implements the et2_IDetachedDOM interface + var isDetachable = false; + if (widget.implements(et2_IDetachedDOM)) + { + // Get all attributes the widgets supports to be set in the + // "detached" mode + var supportedAttrs = []; + widget.getDetachedAttributes(supportedAttrs); + supportedAttrs.push("id"); + isDetachable = true; + + for (var j = 0; j < _varAttrs[i].data.length/* && isDetachable*/; j++) + { + var data = _varAttrs[i].data[j]; + + var supportsAttr = supportedAttrs.indexOf(data.attribute) != -1; + + if (!supportsAttr) + { + egw.debug("warn", "et2_IDetachedDOM widget " + + widget._type + " does not support " + data.attribute); + } + + isDetachable = isDetachable && supportsAttr; + } + } + + // Insert the widget into the correct slot + if (isDetachable) + { + detachable.push(_varAttrs[i]); + } + else + { + remaining.push(_varAttrs[i]); + } + } + + return { + "detachable": detachable, + "remaining": remaining + }; + } + + /** + * Removes to DOM code for all widgets in the "remaining" slot + * + * @param {object} _rowTemplate + */ + _stripTemplateRow( _rowTemplate) + { + _rowTemplate.placeholders = []; + + for (var i = 0; i < _rowTemplate.seperated.remaining.length; i++) + { + var entry = _rowTemplate.seperated.remaining[i]; + + // Issue a warning - widgets which do not implement et2_IDOMNode + // are very slow + egw.debug("warn", "Non-clonable widget '"+ entry.widget._type + "' in dataview row - this " + + "might be slow", entry); + + // Set the placeholder for the entry to null + entry.placeholder = null; + + // Get the outer DOM-Node of the widget + if (entry.widget.implements(et2_IDOMNode)) + { + var node = entry.widget.getDOMNode(entry.widget); + + if (node && node.parentNode) + { + // Get the parent node and replace the node with a placeholder + entry.placeholder = document.createElement("span"); + node.parentNode.replaceChild(entry.placeholder, node); + _rowTemplate.placeholders.push({ + "widget": entry.widget, + "func": this._compileDOMAccessFunc(_rowTemplate.row, + entry.placeholder) + }); + } + } + } + } + + _nodeIndex( _node) + { + if(_node.parentNode == null) + { + return 0; + } + for (var i = 0; i < _node.parentNode.childNodes.length; i++) + { + if (_node.parentNode.childNodes[i] == _node) + { + return i; + } + } + + return -1; + } + + /** + * Returns a function which does a relative access on the given DOM-Node + * + * @param {DOMElement} _root + * @param {DOMElement} _target + */ + _compileDOMAccessFunc( _root, _target) + { + function recordPath(_root, _target, _path) + { + if (typeof _path == "undefined") + { + _path = []; + } + + if (_root != _target && _target) + { + // Get the index of _target in its parent node + var idx = this._nodeIndex(_target); + if (idx >= 0) + { + // Add the access selector + _path.unshift("childNodes[" + idx + "]"); + + // Record the remaining path + return recordPath.call(this, _root, _target.parentNode, _path); + } + + throw("Internal error while compiling DOM access function."); + } + else + { + _path.unshift("_node"); + return "return " + _path.join(".") + ";"; + } + } + + return new Function("_node", recordPath.call(this, _root, _target)); + } + + /** + * Builds relative paths to the DOM-Nodes and compiles fast-access functions + * + * @param {object} _rowTemplate + */ + _buildNodeAccessFuncs( _rowTemplate) + { + for (var i = 0; i < _rowTemplate.seperated.detachable.length; i++) + { + var entry = _rowTemplate.seperated.detachable[i]; + + // Get all needed nodes from the widget + var nodes = entry.widget.getDetachedNodes(); + var nodeFuncs = entry.nodeFuncs = new Array(nodes.length); + + // Record the path to each DOM-Node + for (var j = 0; j < nodes.length; j++) + { + nodeFuncs[j] = this._compileDOMAccessFunc(_rowTemplate.row, + nodes[j]); + } + } + } + + /** + * Match category-ids from class attribute eg. "cat_15" or "123,456,789 " + * + * Make sure to not match numbers inside other class-names. + * + * We can NOT use something like /(^| |,|cat_)([0-9]+)( |,|$)/g as it wont find all cats in "123,456,789 "! + */ + cat_regexp: RegExp = /(^| |,|cat_)([0-9]+)/g; + /** + * Regular expression used to filter out non-nummerical chars from above matches + */ + cat_cleanup: RegExp = /[^0-9]/g; + + /** + * Applies additional row data (like the class) to the tr + * + * @param {object} _data + * @param {DOMElement} _tr + * @param {object} _mgrs + */ + _setRowData( _data, _tr, _mgrs) + { + // TODO: Implement other fields than "class" + if (_data["class"]) + { + var classes = _mgrs["content"].expandName(_data["class"]); + + // Get fancy with categories + var cats = []; + // Assume any numeric class is a category + if(_data["class"].indexOf("cat") !== -1 || classes.match(/[0-9]+/)) + { + // Accept either cat, cat_id or category as ID, and look there for category settings + var category_location = _data["class"].match(/(cat(_id|egory)?)/); + if(category_location) category_location = category_location[0]; + + cats = classes.match(this.cat_regexp) || []; + classes = classes.replace(this.cat_regexp, ''); + + // Set category class + for(var i = 0; i < cats.length; i++) + { + // Need cat_, classes can't start with a number + var cat_id = cats[i].replace(this.cat_cleanup, ''); + var cat_class = 'cat_'+cat_id; + + classes += ' '+cat_class; + } + classes += " row_category"; + } + classes += " row"; + _tr.setAttribute("class", classes); + } + if(_data['valign']) + { + var align = _mgrs["content"].expandName(_data["valign"]); + _tr.setAttribute("valign", align); + } + } +} + +/** + * @augments et2_widget + */ +class et2_nextmatch_rowWidget extends et2_widget implements et2_IDOMNode +{ + private _widgets: any[]; + private _row: any; + /** + * Constructor + * + * @param _mgrs + * @param _row + * @memberOf et2_nextmatch_rowWidget + */ + constructor( _mgrs, _row) + { + // Call the parent constructor with some dummy attributes + super(null, {"id": "", "type": "rowWidget"}); + + // Initialize some variables + this._widgets = []; + + // Copy the given DOM node and the content arrays + this._mgrs = _mgrs; + this._row = _row; + } + + /** + * Copies the given array manager and clones the given widgets and inserts + * them into the row which has been passed in the constructor. + * + * @param {array} _widgets + */ + createWidgets( _widgets) + { + // Clone the given the widgets with this element as parent + this._widgets = new Array(_widgets.length); + for (var i = 0; i < _widgets.length; i++) + { + // Disabled columns might be missing widget - skip it + if(!_widgets[i]) continue; + + this._widgets[i] = _widgets[i].clone(this); + this._widgets[i].loadingFinished(); + // Set column alignment from widget + if(this._widgets[i].align) + { + this._row.childNodes[i].align = this._widgets[i].align; + } + } + } + + /** + * Returns the column node for the given sender + * + * @param {et2_widget} _sender + * @return {DOMElement} + */ + getDOMNode( _sender) + { + for (var i = 0; i < this._widgets.length; i++) + { + if (this._widgets[i] == _sender) + { + return this._row.childNodes[i].childNodes[0]; // Return the i-th td tag + } + } + + return null; + } + +} + +/** + * @augments et2_widget + */ +class et2_nextmatch_rowTemplateWidget extends et2_widget implements et2_IDOMNode +{ + private _root: any; + private _row: any; + private _widgets: any[]; + /** + * Constructor + * + * @param _root + * @param _row + * @memberOf et2_nextmatch_rowTemplateWidget + */ + constructor( _root, _row) + { + // Call the parent constructor with some dummy attributes + super(null, {"id": "", "type": "rowTemplateWidget"}); + + this._root = _root; + this._mgrs = {}; + this._row = _row; + + // Set parent to root widget, so sub-widget calls still work + this._parent = _root; + + // Clone the widgets inside the placeholders array + this._widgets = []; + } + + createWidgets( _mgrs, _widgets : {widget : et2_widget,func(_row: any): any; }[]) + { + // Set the array managers - don't use setArrayMgrs here as this creates + // an unnecessary copy of the object + this._mgrs = _mgrs; + + this._widgets = new Array(_widgets.length); + for (var i = 0; i < _widgets.length; i++) + { + this._row.childNodes[0].childNodes[0]; + + this._widgets[i] = { + "widget": _widgets[i].widget.clone(this), + "node": _widgets[i].func(this._row) + }; + this._widgets[i].widget.loadingFinished(); + } + } + + /** + * Returns the column node for the given sender + * + * @param {et2_widget} _sender + * @return {DOMElement} + */ + getDOMNode( _sender: et2_widget): HTMLElement + { + + for (var i = 0; i < this._widgets.length; i++) + { + if (this._widgets[i].widget == _sender) + { + return this._widgets[i].node; + } + } + + return null; + } + +} + diff --git a/api/js/etemplate/et2_types.d.ts b/api/js/etemplate/et2_types.d.ts index 4dc22cc7cd..dea3639aca 100644 --- a/api/js/etemplate/et2_types.d.ts +++ b/api/js/etemplate/et2_types.d.ts @@ -3,28 +3,46 @@ declare module eT2 } declare var etemplate2 : any; -declare var et2_DOMWidget : any; +declare class et2_widget{ + destroy() + getWidgetById(string) : et2_widget; +} +declare class et2_DOMWidget extends et2_widget{} +declare class et2_baseWidget extends et2_DOMWidget{} +declare class et2_valueWidget extends et2_baseWidget{} +declare class et2_inputWidget extends et2_valueWidget{ + getInputNode() : HTMLElement; + public set_value(value: string | object | number); + public getValue() : any; +} +declare class et2_tabbox extends et2_valueWidget { + tabData : any; + activateTab(et2_widget); +} +declare class et2_button extends et2_DOMWidget { + click() : boolean; + onclick: Function; + set_disabled(b: boolean) : void; +} 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_editableWidget : any; -declare var et2_inputWidget : any; -declare var et2_IDOMNode : any; +declare var et2_validTypes : string[]; +declare var et2_typeDefaults : object; +//declare const et2_no_init : object; +declare class et2_editableWidget extends et2_inputWidget { + public set_readonly(value : boolean); +} +/*declare var et2_IDOMNode : any; declare var et2_IInput : any; declare var et2_IResizeable : any; declare var et2_IAligned : any; 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_IPrint : any;*/ +declare var et2_registry : {}; declare var et2_dataview : any; declare var et2_dataview_controller : any; declare var et2_dataview_selectionManager : any; @@ -39,22 +57,27 @@ declare var et2_dataview_row : any; declare var et2_dataview_rowProvider : any; declare var et2_dataview_spacer : any; declare var et2_dataview_tile : any; -declare var et2_customfields_list : any; -declare var et2_INextmatchHeader : any; -declare var et2_INextmatchSortable : any; -declare var et2_nextmatch : any; +declare class et2_customfields_list extends et2_valueWidget { + constructor(_parent: any, _attrs: WidgetConfig, object: object); + + public static readonly prefix : string; + public customfields : any; + set_visible(visible : boolean); +} +declare class et2_nextmatch extends et2_DOMWidget { + +} declare var et2_nextmatch_header_bar : any; declare var et2_nextmatch_header : any; declare var et2_nextmatch_customfields : any; -declare var et2_nextmatch_sortheader : any; -declare var et2_nextmatch_filterheader : any; -declare var et2_nextmatch_accountfilterheader : any; -declare var et2_nextmatch_taglistheader : any; -declare var et2_nextmatch_entryheader : any; -declare var et2_nextmatch_customfilter : any; declare var et2_nextmatch_controller : any; -declare var et2_dynheight : any; -declare var et2_nextmatch_rowProvider : any; +declare class et2_dynheight { + constructor(_outerNode, _innerNode, _minHeight); + outerNode : any; + update : any; + free : any; +} +declare class et2_nextmatch_rowProvider {} declare var et2_nextmatch_rowWidget : any; declare var et2_nextmatch_rowTemplateWidget : any; declare var et2_ajaxSelect : any; @@ -62,7 +85,6 @@ declare var et2_ajaxSelect_ro : any; declare var et2_barcode : any; declare var et2_box : any; declare var et2_details : any; -declare var et2_button : any; declare var et2_checkbox : any; declare var et2_checkbox_ro : any; declare var et2_color : any; @@ -78,7 +100,7 @@ declare var et2_diff : any; declare var et2_dropdown_button : any; declare var et2_entry : any; declare var et2_favorites : any; -declare var et2_file : any; +declare class et2_file extends et2_widget {} declare var et2_grid : any; declare var et2_groupbox : any; declare var et2_groupbox_legend : any; @@ -87,7 +109,9 @@ declare var et2_historylog : any; declare var et2_hrule : any; declare var et2_html : any; declare var et2_htmlarea : any; -declare var et2_iframe : any; +declare class et2_iframe extends et2_valueWidget { + public set_src(string); +} declare var et2_image : any; declare var et2_appicon : any; declare var et2_avatar : any; @@ -110,13 +134,19 @@ declare var et2_radioGroup : any; declare var et2_script : any; declare var et2_selectAccount : any; declare var et2_selectAccount_ro : any; -declare var et2_selectbox : any; +declare class et2_selectbox extends et2_inputWidget { + protected options : any; + public createInputWidget(); + public set_multiple(boolean); + public set_select_options(options: any); +} declare var et2_selectbox_ro : any; declare var et2_menulist : any; declare var et2_split : any; declare var et2_styles : any; -declare var et2_tabbox : any; -declare var et2_taglist : any; +declare class et2_taglist extends et2_selectbox { + protected div : JQuery; +} declare var et2_taglist_account : any; declare var et2_taglist_email : any; declare var et2_taglist_category : any; @@ -128,7 +158,7 @@ declare var et2_textbox : any; declare var et2_textbox_ro : any; declare var et2_searchbox : any; declare var et2_timestamper : any; -declare var et2_toolbar : any; +declare class et2_toolbar extends et2_DOMWidget {} declare var et2_tree : any; declare var et2_url : any; declare var et2_url_ro : any; @@ -143,13 +173,36 @@ declare var et2_vfsUid : any; declare var et2_vfsUpload : any; 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; +declare var tinymce : any; +declare var date : any; +declare var tinyMCE : any; +declare class et2_nextmatch_sortheader extends et2_nextmatch_header {} +declare class et2_nextmatch_filterheader extends et2_nextmatch_header {} +declare class et2_nextmatch_accountfilterheader extends et2_nextmatch_header {} +declare class et2_nextmatch_taglistheader extends et2_nextmatch_header {} +declare class et2_nextmatch_entryheader extends et2_nextmatch_header {} +declare class et2_nextmatch_customfilter extends et2_nextmatch_filterheader {} +declare function et2_createWidget(type : string, params? : {}, parent? : any) : any; +declare function nm_action(_action : {}, _senders : [], _target? : any, _ids? : any) : void; +declare function et2_compileLegacyJS(_code : string, _widget : et2_widget, _context? : HTMLElement) : Function; +// et2_core_xml.js +declare function et2_loadXMLFromURL(_url : string, _callback : Function, _context? : object, _fail_callback? : Function) : void; +declare function et2_directChildrenByTagName(_node, _tagName); +declare function et2_filteredNodeIterator(_node, _callback, _context); +declare function et2_readAttrWithDefault(_node, _name, _default?); +declare function sprintf(format : string, ...args : any) : string; declare function fetchAll(ids, nextmatch, callback : Function) : boolean; declare function doLongTask(idsArr : string[], all : boolean, _action : any, nextmatch : any) : boolean; declare function nm_compare_field(_action, _senders, _target) : boolean; declare function nm_open_popup(_action, _selected) : void; declare function nm_submit_popup(button) : void; declare function nm_hide_popup(element, div_id) : false; -declare function nm_activate_link(_action, _senders) : void; \ No newline at end of file +declare function nm_activate_link(_action, _senders) : void; +declare function egw_seperateJavaScript(_html) : void; +declare class Resumable { + constructor(asyncOptions: any); +} +declare class dhtmlXTreeObject { + constructor(options : any); +} +declare function expose(widget:any) : any; \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_ajaxSelect.js b/api/js/etemplate/et2_widget_ajaxSelect.js index 53678c6e0a..e9c4268250 100644 --- a/api/js/etemplate/et2_widget_ajaxSelect.js +++ b/api/js/etemplate/et2_widget_ajaxSelect.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - JS Ajax select / auto complete object @@ -8,16 +9,32 @@ * @link http://www.egroupware.org * @author Nathan Gray * @copyright Nathan Gray 2012 - * @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 - /vendor/bower-asset/jquery/dist/jquery.js; - /vendor/bower-asset/jquery-ui/jquery-ui.js; - et2_core_inputWidget; - et2_core_valueWidget; + /vendor/bower-asset/jquery/dist/jquery.js; + /vendor/bower-asset/jquery-ui/jquery-ui.js; + et2_core_inputWidget; + et2_core_valueWidget; */ - +var et2_core_widget_1 = require("./et2_core_widget"); +var et2_core_inputWidget_1 = require("./et2_core_inputWidget"); +var et2_core_inheritance_1 = require("./et2_core_inheritance"); +var et2_core_valueWidget_1 = require("./et2_core_valueWidget"); +var et2_widget_selectbox_1 = require("./et2_widget_selectbox"); /** * Using AJAX, this widget allows a type-ahead find similar to a ComboBox, where as the user enters information, * a drop-down box is populated with the n closest matches. If the user clicks on an item in the drop-down, that @@ -27,227 +44,205 @@ * This widget can get data from any function that can provide data to a nextmatch widget. * @augments et2_inputWidget */ -var et2_ajaxSelect = (function(){ "use strict"; return et2_inputWidget.extend( -{ - attributes: { - 'get_rows': { - "name": "Data source", - "type": "any", - "default": "", - "description": "Function to get search results, either a javascript function or server-side." - }, - 'get_title': { - "name": "Title function", - "type": "any", - "default": "", - "description": "Function to get title for selected entry. Used when closed, and if no template is given." - }, - 'id_field': { - "name": "Result ID field", - "type": "string", - "default": "value", - "description": "Which key in result sub-array to look for row ID. If omitted, the key for the row will be used." - }, - 'template': { - "name": "Row template", - "type": "string", - "default": "", - "description": "ID of the template to use to display rows. If omitted, title will be shown for each result." - }, - 'filter': { - "name": "Filter", - "type": "string", - "default": "", - "description": "Apply filter to search results. Same as nextmatch." - }, - 'filter2': { - "name": "Filter 2", - "type": "string", - "default": "", - "description": "Apply filter to search results. Same as nextmatch." - }, - 'link': { - "name": "Read only link", - "type": "boolean", - "default": "true", - "description": "If readonly, widget will be text. If link is set, widget will be a link." - }, - - // Pass by code only - 'values': { - "name": "Values", - "type": "any", - "default": {}, - "description": "Specify the available options. Use this, or Data source." - } - }, - - /** - * Constructor - * - * @memberOf et2_ajaxSelect - */ - init: function(parent, attrs) { - this._super.apply(this, arguments); - - if(typeof attrs.get_rows == 'string') - { - attrs.get_rows = this.egw().link('/index.php', { - menuaction: this.options.get_rows - }) - } - this.createInputWidget(); - - this.input = null; - - this.createInputWidget(); - }, - - createInputWidget: function() { - this.input = jQuery(document.createElement("input")); - - this.input.addClass("et2_textbox"); - - this.setDOMNode(this.input[0]); - - var widget = this; - this.input.autocomplete({ - delay: 100, - source: this.options.get_rows ? - this.options.get_rows : - et2_selectbox.find_select_options(this,this.options.values), - select: function(event, ui) { - widget.value = ui.item[widget.options.id_field]; - if(widget.options.get_title) - { - if(typeof widget.options.get_title == 'function') - { - widget.input.val(widget.options.get_title.call(widget.value)); - } - else if (typeof widget.options.get_title == 'string') - { - // TODO: Server side callback - } - } - else - { - widget.input.val(ui.item.label); - } - // Prevent default action of setting field to the value - return false; - } - }); - }, - - getValue: function() - { - if(this.options.blur && this.input.val() == this.options.blur) return ""; - return this.value; - }, - - set_value: function(_value) - { - this.value = _value; - if(this.input.autocomplete('instance')) - { - var source = this.input.autocomplete('option','source'); - if(typeof source == 'object') - { - for(var i in source) - { - if(typeof source[i].value != 'undefined' && typeof source[i].label != 'undefined' && source[i].value === _value) - { - this.input.val(source[i].label) - } - else if (typeof source[i] == 'string') - { - this.input.val(source[_value]); - break; - } - } - } - else if(typeof source == 'function') - { - // TODO - } - } - }, - - set_blur: function(_value) { - if(_value) { - this.input.attr("placeholder", _value + ""); // HTML5 - if(!this.input[0].placeholder) { - // Not HTML5 - if(this.input.val() == "") this.input.val(this.options.blur); - this.input.focus(this,function(e) { - if(e.data.input.val() == e.data.options.blur) e.data.input.val(""); - }).blur(this, function(e) { - if(e.data.input.val() == "") e.data.input.val(e.data.options.blur); - }); - } - } else { - this.input.removeAttr("placeholder"); - } - } -});}).call(this); -et2_register_widget(et2_ajaxSelect, ["ajax_select"]); - +var et2_ajaxSelect = /** @class */ (function (_super) { + __extends(et2_ajaxSelect, _super); + /** + * Constructor + * + * @memberOf et2_ajaxSelect + */ + function et2_ajaxSelect(_parent, _attrs, _child) { + var _this = + // Call the inherited constructor + _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_ajaxSelect._attributes, _child || {})) || this; + _this.input = null; + if (typeof _attrs.get_rows == 'string') { + _attrs.get_rows = _this.egw().link('/index.php', { + menuaction: _this.options.get_rows + }); + } + _this.createInputWidget(); + _this.input = null; + _this.createInputWidget(); + return _this; + } + et2_ajaxSelect.prototype.createInputWidget = function () { + this.input = jQuery(document.createElement("input")); + this.input.addClass("et2_textbox"); + this.setDOMNode(this.input[0]); + var widget = this; + this.input.autocomplete({ + delay: 100, + source: this.options.get_rows ? + this.options.get_rows : + et2_widget_selectbox_1.et2_selectbox.find_select_options(this, this.options.values), + select: function (event, ui) { + widget.value = ui.item[widget.options.id_field]; + if (widget.options.get_title) { + if (typeof widget.options.get_title == 'function') { + widget.input.val(widget.options.get_title.call(widget.value)); + } + else if (typeof widget.options.get_title == 'string') { + // TODO: Server side callback + } + } + else { + widget.input.val(ui.item.label); + } + // Prevent default action of setting field to the value + return false; + } + }); + }; + et2_ajaxSelect.prototype.getValue = function () { + if (this.options.blur && this.input.val() == this.options.blur) + return ""; + return this.value; + }; + et2_ajaxSelect.prototype.set_value = function (_value) { + this.value = _value; + if (this.input.autocomplete('instance')) { + var source = this.input.autocomplete('option', 'source'); + if (typeof source == 'object') { + for (var i in source) { + if (typeof source[i].value != 'undefined' && typeof source[i].label != 'undefined' && source[i].value === _value) { + this.input.val(source[i].label); + } + else if (typeof source[i] == 'string') { + this.input.val(source[_value]); + break; + } + } + } + else if (typeof source == 'function') { + // TODO + } + } + }; + et2_ajaxSelect.prototype.set_blur = function (_value) { + if (_value) { + this.input.attr("placeholder", _value + ""); // HTML5 + if (!this.input[0]["placeholder"]) { + // Not HTML5 + if (this.input.val() == "") + this.input.val(this.options.blur); + this.input.focus(this, function (e) { + if (e.data.input.val() == e.data.options.blur) + e.data.input.val(""); + }).blur(this, function (e) { + if (e.data.input.val() == "") + e.data.input.val(e.data.options.blur); + }); + } + } + else { + this.input.removeAttr("placeholder"); + } + }; + et2_ajaxSelect._attributes = { + 'get_rows': { + "name": "Data source", + "type": "any", + "default": "", + "description": "Function to get search results, either a javascript function or server-side." + }, + 'get_title': { + "name": "Title function", + "type": "any", + "default": "", + "description": "Function to get title for selected entry. Used when closed, and if no template is given." + }, + 'id_field': { + "name": "Result ID field", + "type": "string", + "default": "value", + "description": "Which key in result sub-array to look for row ID. If omitted, the key for the row will be used." + }, + 'template': { + "name": "Row template", + "type": "string", + "default": "", + "description": "ID of the template to use to display rows. If omitted, title will be shown for each result." + }, + 'filter': { + "name": "Filter", + "type": "string", + "default": "", + "description": "Apply filter to search results. Same as nextmatch." + }, + 'filter2': { + "name": "Filter 2", + "type": "string", + "default": "", + "description": "Apply filter to search results. Same as nextmatch." + }, + 'link': { + "name": "Read only link", + "type": "boolean", + "default": "true", + "description": "If readonly, widget will be text. If link is set, widget will be a link." + }, + // Pass by code only + 'values': { + "name": "Values", + "type": "any", + "default": {}, + "description": "Specify the available options. Use this, or Data source." + } + }; + return et2_ajaxSelect; +}(et2_core_inputWidget_1.et2_inputWidget)); +et2_core_widget_1.et2_register_widget(et2_ajaxSelect, ["ajax_select"]); /** - * et2_textbox_ro is the dummy readonly implementation of the textbox. - * @augments et2_valueWidget - */ -var et2_ajaxSelect_ro = (function(){ "use strict"; return et2_valueWidget.extend([et2_IDetachedDOM], -{ - /** - * Ignore all more advanced attributes. - */ - attributes: { - "multiline": { - "ignore": true - } - }, - - /** - * Constructor - * - * @memberOf et2_ajaxSelect_ro - */ - init: function() { - this._super.apply(this, arguments); - - this.value = ""; - this.span = jQuery(document.createElement("span")); - - this.setDOMNode(this.span[0]); - }, - - set_value: function(_value) { - this.value = _value; - - if(!_value) _value = ""; - this.span.text(_value); - }, - /** - * Code for implementing et2_IDetachedDOM - */ - getDetachedAttributes: function(_attrs) - { - _attrs.push("value"); - }, - - getDetachedNodes: function() - { - return [this.span[0]]; - }, - - setDetachedAttributes: function(_nodes, _values) - { - this.span = jQuery(_nodes[0]); - if(typeof _values["value"] != 'undefined') - { - this.set_value(_values["value"]); - } - } -});}).call(this); -et2_register_widget(et2_ajaxSelect_ro, ["ajax_select_ro"]); - +* et2_textbox_ro is the dummy readonly implementation of the textbox. +* @augments et2_valueWidget +*/ +var et2_ajaxSelect_ro = /** @class */ (function (_super) { + __extends(et2_ajaxSelect_ro, _super); + /** + * Constructor + * + * @memberOf et2_ajaxSelect_ro + */ + function et2_ajaxSelect_ro(_parent, _attrs, _child) { + var _this = + // Call the inherited constructor + _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_ajaxSelect_ro._attributes, _child || {})) || this; + _this.value = ""; + _this.span = jQuery(document.createElement("span")); + _this.setDOMNode(_this.span[0]); + return _this; + } + et2_ajaxSelect_ro.prototype.set_value = function (_value) { + this.value = _value; + if (!_value) + _value = ""; + this.span.text(_value); + }; + /** + * Code for implementing et2_IDetachedDOM + */ + et2_ajaxSelect_ro.prototype.getDetachedAttributes = function (_attrs) { + _attrs.push("value"); + }; + et2_ajaxSelect_ro.prototype.getDetachedNodes = function () { + return [this.span[0]]; + }; + et2_ajaxSelect_ro.prototype.setDetachedAttributes = function (_nodes, _values) { + this.span = jQuery(_nodes[0]); + if (typeof _values["value"] != 'undefined') { + this.set_value(_values["value"]); + } + }; + /** + * Ignore all more advanced attributes. + */ + et2_ajaxSelect_ro._attributes = { + "multiline": { + "ignore": true + } + }; + return et2_ajaxSelect_ro; +}(et2_core_valueWidget_1.et2_valueWidget)); +et2_core_widget_1.et2_register_widget(et2_ajaxSelect_ro, ["ajax_select_ro"]); +//# sourceMappingURL=et2_widget_ajaxSelect.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_ajaxSelect.ts b/api/js/etemplate/et2_widget_ajaxSelect.ts new file mode 100644 index 0000000000..617b21fc79 --- /dev/null +++ b/api/js/etemplate/et2_widget_ajaxSelect.ts @@ -0,0 +1,268 @@ +/** + + * EGroupware eTemplate2 - JS Ajax select / auto complete object + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage api + * @link http://www.egroupware.org + * @author Nathan Gray + * @copyright Nathan Gray 2012 + */ + +/*egw:uses + /vendor/bower-asset/jquery/dist/jquery.js; + /vendor/bower-asset/jquery-ui/jquery-ui.js; + et2_core_inputWidget; + et2_core_valueWidget; +*/ + +import {et2_register_widget, WidgetConfig} from "./et2_core_widget"; +import {et2_inputWidget} from "./et2_core_inputWidget"; +import {ClassWithAttributes} from "./et2_core_inheritance"; +import {et2_valueWidget} from "./et2_core_valueWidget"; +import {et2_selectbox} from "./et2_widget_selectbox"; + +/** + * Using AJAX, this widget allows a type-ahead find similar to a ComboBox, where as the user enters information, + * a drop-down box is populated with the n closest matches. If the user clicks on an item in the drop-down, that + * value is selected. + * n is the maximum number of results set in the user's preferences. + * The user is restricted to selecting values in the list. + * This widget can get data from any function that can provide data to a nextmatch widget. + * @augments et2_inputWidget + */ +class et2_ajaxSelect extends et2_inputWidget +{ + static readonly _attributes : any = { + 'get_rows': { + "name": "Data source", + "type": "any", + "default": "", + "description": "Function to get search results, either a javascript function or server-side." + }, + 'get_title': { + "name": "Title function", + "type": "any", + "default": "", + "description": "Function to get title for selected entry. Used when closed, and if no template is given." + }, + 'id_field': { + "name": "Result ID field", + "type": "string", + "default": "value", + "description": "Which key in result sub-array to look for row ID. If omitted, the key for the row will be used." + }, + 'template': { + "name": "Row template", + "type": "string", + "default": "", + "description": "ID of the template to use to display rows. If omitted, title will be shown for each result." + }, + 'filter': { + "name": "Filter", + "type": "string", + "default": "", + "description": "Apply filter to search results. Same as nextmatch." + }, + 'filter2': { + "name": "Filter 2", + "type": "string", + "default": "", + "description": "Apply filter to search results. Same as nextmatch." + }, + 'link': { + "name": "Read only link", + "type": "boolean", + "default": "true", + "description": "If readonly, widget will be text. If link is set, widget will be a link." + }, + + // Pass by code only + 'values': { + "name": "Values", + "type": "any", + "default": {}, + "description": "Specify the available options. Use this, or Data source." + } + }; + private input: JQuery = null; + private value : any; + /** + * Constructor + * + * @memberOf et2_ajaxSelect + */ + constructor(_parent, _attrs? : WidgetConfig, _child? : object) + { + // Call the inherited constructor + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_ajaxSelect._attributes, _child || {})); + + if(typeof _attrs.get_rows == 'string') + { + _attrs.get_rows = this.egw().link('/index.php', { + menuaction: this.options.get_rows + }) + } + this.createInputWidget(); + + this.input = null; + + this.createInputWidget(); + } + + createInputWidget() { + this.input = jQuery(document.createElement("input")); + + this.input.addClass("et2_textbox"); + + this.setDOMNode(this.input[0]); + + let widget = this; + this.input.autocomplete({ + delay: 100, + source: this.options.get_rows ? + this.options.get_rows : + et2_selectbox.find_select_options(this, this.options.values), + select: function(event, ui) { + widget.value = ui.item[widget.options.id_field]; + if(widget.options.get_title) + { + if(typeof widget.options.get_title == 'function') + { + widget.input.val(widget.options.get_title.call(widget.value)); + } + else if (typeof widget.options.get_title == 'string') + { + // TODO: Server side callback + } + } + else + { + widget.input.val(ui.item.label); + } + // Prevent default action of setting field to the value + return false; + } + }); + } + + getValue() + { + if(this.options.blur && this.input.val() == this.options.blur) return ""; + return this.value; + } + + set_value(_value) + { + this.value = _value; + if(this.input.autocomplete('instance')) + { + let source = this.input.autocomplete('option','source'); + if(typeof source == 'object') + { + for(let i in source) + { + if(typeof source[i].value != 'undefined' && typeof source[i].label != 'undefined' && source[i].value === _value) + { + this.input.val(source[i].label) + } + else if (typeof source[i] == 'string') + { + this.input.val(source[_value]); + break; + } + } + } + else if(typeof source == 'function') + { + // TODO + } + } + } + + set_blur(_value) + { + if(_value) { + this.input.attr("placeholder", _value + ""); // HTML5 + if(!this.input[0]["placeholder"]) { + // Not HTML5 + if(this.input.val() == "") this.input.val(this.options.blur); + this.input.focus(this,function(e) { + if(e.data.input.val() == e.data.options.blur) e.data.input.val(""); + }).blur(this, function(e) { + if(e.data.input.val() == "") e.data.input.val(e.data.options.blur); + }); + } + } else { + this.input.removeAttr("placeholder"); + } + } +} +et2_register_widget(et2_ajaxSelect, ["ajax_select"]); + +/** +* et2_textbox_ro is the dummy readonly implementation of the textbox. +* @augments et2_valueWidget +*/ +class et2_ajaxSelect_ro extends et2_valueWidget implements et2_IDetachedDOM +{ + /** + * Ignore all more advanced attributes. + */ + static readonly _attributes : any = { + "multiline": { + "ignore": true + } + }; + private value: string; + private span: JQuery; + + /** + * Constructor + * + * @memberOf et2_ajaxSelect_ro + */ + constructor(_parent, _attrs? : WidgetConfig, _child? : object) + { + // Call the inherited constructor + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_ajaxSelect_ro._attributes, _child || {})); + + this.value = ""; + this.span = jQuery(document.createElement("span")); + + this.setDOMNode(this.span[0]); + } + + set_value(_value) + { + this.value = _value; + + if(!_value) _value = ""; + this.span.text(_value); + } + /** + * Code for implementing et2_IDetachedDOM + */ + getDetachedAttributes(_attrs) + { + _attrs.push("value"); + } + + getDetachedNodes() + { + return [this.span[0]]; + } + + setDetachedAttributes(_nodes, _values) + { + this.span = jQuery(_nodes[0]); + if(typeof _values["value"] != 'undefined') + { + this.set_value(_values["value"]); + } + } +} +et2_register_widget(et2_ajaxSelect_ro, ["ajax_select_ro"]); + + diff --git a/api/js/etemplate/et2_widget_barcode.js b/api/js/etemplate/et2_widget_barcode.js index 13f4b78e91..922d9a8d32 100644 --- a/api/js/etemplate/et2_widget_barcode.js +++ b/api/js/etemplate/et2_widget_barcode.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - JS barcode widget * @@ -9,14 +10,26 @@ * @copyright Stylite AG * @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 - /vendor/bower-asset/jquery/dist/jquery.js; - /api/js/jquery/barcode/jquery-barcode.min.js; - et2_core_interfaces; - et2_core_baseWidget; + /vendor/bower-asset/jquery/dist/jquery.js; + /api/js/jquery/barcode/jquery-barcode.min.js; + et2_core_interfaces; + et2_core_baseWidget; */ - /** * This widget creates barcode out of a given text * @@ -38,123 +51,112 @@ * * Further information about types and formats are defined in static part of the class at the end */ - +var et2_core_widget_1 = require("./et2_core_widget"); +var et2_core_inheritance_1 = require("./et2_core_inheritance"); +var et2_core_valueWidget_1 = require("./et2_core_valueWidget"); /** * Class which implements the "barcode" XET-Tag * - * @augments et2_baseWidget */ -var et2_barcode = (function(){ "use strict"; return et2_valueWidget.extend( -{ - attributes : { - "code_type": { - "name": "code type", - "type": "string", - "default": "datamatrix", //et2_barcode.TYPE_DATAMATRIX - "description": "Barcode type to be generated, default is QR barcode" - }, - bgColor: { - "name":"bgColor", - "type": "string", - "default":'#FFFFFF', - "description": "Defines backgorund color of barcode container" - }, - barColor: { - "name":"barColor", - "type": "string", - "default":'#000000', - "description": "Defines color of the bars in barcode." - }, - format: { - "name":"format", - "type": "string", - "default":'css', //et2_barcode.FORMAT_CSS - "description": "Defines in which format the barcode should be rendered. Default is SVG." - }, - barWidth: { - "name":"bar width", - "type": "string", - "default":'1', - "description": "Defines width of each bar in the barcode." - }, - barHeight: { - "name":"bar height", - "type": "string", - "default":'50', - "description": "Defines heigh of each bar in the barcode." - }, - }, - - /** - * Constructor - * - * @memberOf et2_video - */ - init: function() - { - this._super.apply(this, arguments); - - this.div = jQuery(document.createElement('div')).attr({ class:'et2_barcode' }); - - // Set domid - this.set_id(this.id); - - this.setDOMNode(this.div[0]); - this.createWidget(); - }, - - createWidget: function () - { - this.settings = { - output:this.options.format, - bgColor: this.options.bgColor, - color: this.options.barColor, - barWidth: this.options.barWidth, - barHeight: this.options.barHeight, - }; - if (this.get_value()) this.div.barcode(this.get_value(), this.options.code_type, this.settings); - }, - - set_value: function (_val) - { - if (typeof _val !== 'undefined') - { - this.value = _val; - this.createWidget(); - } - }, - - get_value: function() - { - return this.value; - } -});}).call(this); -et2_register_widget(et2_barcode, ["barcode"]); - -// Static part of the class -jQuery.extend(et2_barcode, -{ - // Class Constants - - /* - * type const - */ - TYPE_CODEBAR: "codebar", - TYPE_CODE11: "code11", //(code 11) - TYPE_CODE39: "code39", //(code 39) - TYPE_CODE128: "code128", //(code 128) - TYPE_EAN8: "ean8", //(ean 8) - http://barcode-coder.com/en/ean-8-specification-101.html - TYPE_EAN13: "ean13", //(ean 13) - http://barcode-coder.com/en/ean-13-specification-102.html - TYPE_STD25: "std25", //(standard 2 of 5 - industrial 2 of 5) - http://barcode-coder.com/en/standard-2-of-5-specification-103.html - TYPE_INT25: "int25", //(interleaved 2 of 5) - TYPE_MSI: "msi", - TYPE_DATAMATRIX: "datamatrix", //(ASCII + extended) - http://barcode-coder.com/en/datamatrix-specification-104.html - - /** - * Formats consts - */ - FORMAT_CSS: "css", - FORMAT_SVG: "svg", - FORMAT_bmp: "bmp", - FORMAT_CANVAS: "canvas", -}); \ No newline at end of file +var et2_barcode = /** @class */ (function (_super) { + __extends(et2_barcode, _super); + /** + * Constructor + */ + function et2_barcode(_parent, _attrs, _child) { + var _this = + // Call the inherited constructor + _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_barcode._attributes, _child || {})) || this; + _this.div = jQuery(document.createElement('div')).attr({ class: 'et2_barcode' }); + // Set domid + _this.set_id(_this.id); + _this.setDOMNode(_this.div[0]); + _this.createWidget(); + return _this; + } + et2_barcode.prototype.createWidget = function () { + this.settings = { + output: this.options.format, + bgColor: this.options.bgColor, + color: this.options.barColor, + barWidth: this.options.barWidth, + barHeight: this.options.barHeight, + }; + if (this.get_value()) { + // @ts-ignore + this.div.barcode(this.get_value(), this.options.code_type, this.settings); + } + }; + et2_barcode.prototype.set_value = function (_val) { + if (typeof _val !== 'undefined') { + this.value = _val; + this.createWidget(); + } + }; + et2_barcode.prototype.get_value = function () { + return this.value; + }; + // Class Constants + /* + * type const + */ + et2_barcode.TYPE_CODEBAR = "codebar"; + et2_barcode.TYPE_CODE11 = "code11"; //(code 11) + et2_barcode.TYPE_CODE39 = "code39"; //(code 39) + et2_barcode.TYPE_CODE128 = "code128"; //(code 128) + et2_barcode.TYPE_EAN8 = "ean8"; //(ean 8) - http://barcode-coder.com/en/ean-8-specification-101.html + et2_barcode.TYPE_EAN13 = "ean13"; //(ean 13) - http://barcode-coder.com/en/ean-13-specification-102.html + et2_barcode.TYPE_STD25 = "std25"; //(standard 2 of 5 - industrial 2 of 5) - http://barcode-coder.com/en/standard-2-of-5-specification-103.html + et2_barcode.TYPE_INT25 = "int25"; //(interleaved 2 of 5) + et2_barcode.TYPE_MSI = "msi"; + et2_barcode.TYPE_DATAMATRIX = "datamatrix"; //(ASCII + extended) - http://barcode-coder.com/en/datamatrix-specification-104.html + /** + * Formats consts + */ + et2_barcode.FORMAT_CSS = "css"; + et2_barcode.FORMAT_SVG = "svg"; + et2_barcode.FORMAT_bmp = "bmp"; + et2_barcode.FORMAT_CANVAS = "canvas"; + et2_barcode._attributes = { + "code_type": { + "name": "code type", + "type": "string", + "default": et2_barcode.TYPE_DATAMATRIX, + "description": "Barcode type to be generated, default is QR barcode" + }, + bgColor: { + "name": "bgColor", + "type": "string", + "default": '#FFFFFF', + "description": "Defines backgorund color of barcode container" + }, + barColor: { + "name": "barColor", + "type": "string", + "default": '#000000', + "description": "Defines color of the bars in barcode." + }, + format: { + "name": "format", + "type": "string", + "default": 'css', + "description": "Defines in which format the barcode should be rendered. Default is SVG." + }, + barWidth: { + "name": "bar width", + "type": "string", + "default": '1', + "description": "Defines width of each bar in the barcode." + }, + barHeight: { + "name": "bar height", + "type": "string", + "default": '50', + "description": "Defines heigh of each bar in the barcode." + }, + }; + return et2_barcode; +}(et2_core_valueWidget_1.et2_valueWidget)); +exports.et2_barcode = et2_barcode; +et2_core_widget_1.et2_register_widget(et2_barcode, ["barcode"]); +//# sourceMappingURL=et2_widget_barcode.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_barcode.ts b/api/js/etemplate/et2_widget_barcode.ts new file mode 100644 index 0000000000..d601d719b1 --- /dev/null +++ b/api/js/etemplate/et2_widget_barcode.ts @@ -0,0 +1,166 @@ +/** + * EGroupware eTemplate2 - JS barcode widget + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage api + * @link http://www.egroupware.org + * @author Hadi Nategh + * @copyright Stylite AG + * @version $Id:$ + */ + +/*egw:uses + /vendor/bower-asset/jquery/dist/jquery.js; + /api/js/jquery/barcode/jquery-barcode.min.js; + et2_core_interfaces; + et2_core_baseWidget; +*/ + +/** + * This widget creates barcode out of a given text + * + * The widget can be created in the following ways: + * + * var barcodeTag = et2_createWidget("barcode", { + * code_type:et2_barcode.TYPE_CSS, + * bgColor:"#FFFFFF", + * barColor:"#000000", + * format:et2_barcode.FORMAT_SVG, + * barWidth:"1", + * barHeight:"50" + * }); + * + * Or by adding XET-tag in your template (.xet) file: + * + * + * + * + * Further information about types and formats are defined in static part of the class at the end + */ + +import {et2_register_widget, WidgetConfig} from "./et2_core_widget"; +import {ClassWithAttributes} from "./et2_core_inheritance"; +import {et2_valueWidget} from "./et2_core_valueWidget"; + +/** + * Class which implements the "barcode" XET-Tag + * + */ +export class et2_barcode extends et2_valueWidget +{ + // Class Constants + + /* + * type const + */ + public static readonly TYPE_CODEBAR = "codebar"; + public static readonly TYPE_CODE11 = "code11"; //(code 11) + public static readonly TYPE_CODE39 = "code39"; //(code 39) + public static readonly TYPE_CODE128 = "code128"; //(code 128) + public static readonly TYPE_EAN8 = "ean8"; //(ean 8) - http://barcode-coder.com/en/ean-8-specification-101.html + public static readonly TYPE_EAN13 = "ean13"; //(ean 13) - http://barcode-coder.com/en/ean-13-specification-102.html + public static readonly TYPE_STD25 = "std25"; //(standard 2 of 5 - industrial 2 of 5) - http://barcode-coder.com/en/standard-2-of-5-specification-103.html + public static readonly TYPE_INT25 = "int25"; //(interleaved 2 of 5) + public static readonly TYPE_MSI = "msi"; + public static readonly TYPE_DATAMATRIX = "datamatrix"; //(ASCII + extended) - http://barcode-coder.com/en/datamatrix-specification-104.html + + /** + * Formats consts + */ + public static readonly FORMAT_CSS = "css"; + public static readonly FORMAT_SVG = "svg"; + public static readonly FORMAT_bmp = "bmp"; + public static readonly FORMAT_CANVAS = "canvas"; + + static readonly _attributes = { + "code_type": { + "name": "code type", + "type": "string", + "default": et2_barcode.TYPE_DATAMATRIX, + "description": "Barcode type to be generated, default is QR barcode" + }, + bgColor: { + "name":"bgColor", + "type": "string", + "default":'#FFFFFF', + "description": "Defines backgorund color of barcode container" + }, + barColor: { + "name":"barColor", + "type": "string", + "default":'#000000', + "description": "Defines color of the bars in barcode." + }, + format: { + "name":"format", + "type": "string", + "default":'css', //et2_barcode.FORMAT_CSS + "description": "Defines in which format the barcode should be rendered. Default is SVG." + }, + barWidth: { + "name":"bar width", + "type": "string", + "default":'1', + "description": "Defines width of each bar in the barcode." + }, + barHeight: { + "name":"bar height", + "type": "string", + "default":'50', + "description": "Defines heigh of each bar in the barcode." + }, + }; + private div: JQuery; + private value: string; + private settings: { output: string; barWidth: string; barHeight: string; bgColor: string; color: string }; + + /** + * Constructor + */ + constructor(_parent, _attrs? : WidgetConfig, _child? : object) + { + // Call the inherited constructor + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_barcode._attributes, _child || {})); + + + this.div = jQuery(document.createElement('div')).attr({ class:'et2_barcode' }); + + // Set domid + this.set_id(this.id); + + this.setDOMNode(this.div[0]); + this.createWidget(); + } + + createWidget() + { + this.settings = { + output:this.options.format, + bgColor: this.options.bgColor, + color: this.options.barColor, + barWidth: this.options.barWidth, + barHeight: this.options.barHeight, + }; + if (this.get_value()) + { + // @ts-ignore + this.div.barcode(this.get_value(), this.options.code_type, this.settings); + } + } + + set_value(_val) + { + if (typeof _val !== 'undefined') + { + this.value = _val; + this.createWidget(); + } + } + + get_value() + { + return this.value; + } +} +et2_register_widget(et2_barcode, ["barcode"]); \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_box.js b/api/js/etemplate/et2_widget_box.js index 8fd5f98a13..4252f610f1 100644 --- a/api/js/etemplate/et2_widget_box.js +++ b/api/js/etemplate/et2_widget_box.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - JS Box object * @@ -9,12 +10,26 @@ * @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 - /vendor/bower-asset/jquery/dist/jquery.js; - et2_core_baseWidget; + /vendor/bower-asset/jquery/dist/jquery.js; + et2_core_baseWidget; */ - +var et2_core_widget_1 = require("./et2_core_widget"); +var et2_core_baseWidget_1 = require("./et2_core_baseWidget"); /** * Class which implements box and vbox tag * @@ -23,137 +38,110 @@ * * @augments et2_baseWidget */ -var et2_box = (function(){ "use strict"; return et2_baseWidget.extend([et2_IDetachedDOM], -{ - attributes: { - // Not needed - "rows": {"ignore": true}, - "cols": {"ignore": true} - }, - - createNamespace: true, - - /** - * Constructor - * - * @memberOf et2_box - */ - init: function() { - this._super.apply(this, arguments); - - this.div = jQuery(document.createElement("div")) - .addClass("et2_" + this._type) - .addClass("et2_box_widget"); - - this.setDOMNode(this.div[0]); - }, - - /** - * Overriden so we can check for autorepeating children. We only check for - * $ in the immediate children & grandchildren of this node. - * - * @param {object} _node - */ - loadFromXML: function(_node) { - if(this._type != "box") - { - return this._super.apply(this, arguments); - } - // Load the child nodes. - var childIndex = 0; - var repeatNode = null; - 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, if no expansion needed - var id = et2_readAttrWithDefault(node, "id", ""); - if(id.indexOf('$') < 0 || widgetType != 'box') - { - this.createElementFromNode(node); - childIndex++; - } - else - { - repeatNode = node; - } - } - - // Only the last child repeats(?) - if(repeatNode != null) - { - var currentPerspective = this.getArrayMgr("content").perspectiveData; - // Extra content - for(childIndex; typeof this.getArrayMgr("content").data[childIndex] != "undefined" && this.getArrayMgr("content").data[childIndex]; childIndex++) { - // Adjust for the row - var mgrs = this.getArrayMgrs(); - for(var name in mgrs) - { - if(this.getArrayMgr(name).getEntry(childIndex)) - { - this.getArrayMgr(name).perspectiveData.row = childIndex; - } - } - - this.createElementFromNode(repeatNode); - } - - // Reset - for(var name in this.getArrayMgrs()) - { - this.getArrayMgr(name).perspectiveData = currentPerspective; - } - } - }, - - /** - * Code for implementing et2_IDetachedDOM - * This doesn't need to be implemented. - * Individual widgets are detected and handled by the grid, but the interface is needed for this to happen - * - * @param {array} _attrs array to add further attributes to - */ - getDetachedAttributes: function(_attrs) - { - _attrs.push('data'); - }, - - getDetachedNodes: function() - { - return [this.getDOMNode()]; - }, - - setDetachedAttributes: function(_nodes, _values) - { - if (_values.data) - { - var pairs = _values.data.split(/,/g); - for(var i=0; i < pairs.length; ++i) - { - var name_value = pairs[i].split(':'); - jQuery(_nodes[0]).attr('data-'+name_value[0], name_value[1]); - } - } - } - -});}).call(this); -et2_register_widget(et2_box, ["vbox", "box"]); - +var et2_box = /** @class */ (function (_super) { + __extends(et2_box, _super); + /** + * Constructor + * + * @memberOf et2_box + */ + function et2_box(_parent, _attrs, _child) { + var _this = _super.call(this, _parent, _attrs, _child) || this; + _this.div = jQuery(document.createElement("div")) + .addClass("et2_" + _this.getType()) + .addClass("et2_box_widget"); + _this.setDOMNode(_this.div[0]); + return _this; + } + et2_box.prototype._createNamespace = function () { + return true; + }; + /** + * Overriden so we can check for autorepeating children. We only check for + * $ in the immediate children & grandchildren of this node. + * + * @param {object} _node + */ + et2_box.prototype.loadFromXML = function (_node) { + if (this.getType() != "box") { + return _super.prototype.loadFromXML.call(this, _node); + } + // Load the child nodes. + var childIndex = 0; + var repeatNode = null; + 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, if no expansion needed + var id = et2_readAttrWithDefault(node, "id", ""); + if (id.indexOf('$') < 0 || widgetType != 'box') { + this.createElementFromNode(node); + childIndex++; + } + else { + repeatNode = node; + } + } + // Only the last child repeats(?) + if (repeatNode != null) { + var currentPerspective = this.getArrayMgr("content").perspectiveData; + // Extra content + for (childIndex; typeof this.getArrayMgr("content").data[childIndex] != "undefined" && this.getArrayMgr("content").data[childIndex]; childIndex++) { + // Adjust for the row + var mgrs = this.getArrayMgrs(); + for (var name in mgrs) { + if (this.getArrayMgr(name).getEntry(childIndex)) { + this.getArrayMgr(name).setRow(childIndex); + } + } + this.createElementFromNode(repeatNode); + } + // Reset + for (var name in this.getArrayMgrs()) { + this.getArrayMgr(name).setPerspectiveData(currentPerspective); + } + } + }; + /** + * Code for implementing et2_IDetachedDOM + * This doesn't need to be implemented. + * Individual widgets are detected and handled by the grid, but the interface is needed for this to happen + * + * @param {array} _attrs array to add further attributes to + */ + et2_box.prototype.getDetachedAttributes = function (_attrs) { + _attrs.push('data'); + }; + et2_box.prototype.getDetachedNodes = function () { + return [this.getDOMNode()]; + }; + et2_box.prototype.setDetachedAttributes = function (_nodes, _values) { + if (_values.data) { + var pairs = _values.data.split(/,/g); + for (var i = 0; i < pairs.length; ++i) { + var name_value = pairs[i].split(':'); + jQuery(_nodes[0]).attr('data-' + name_value[0], name_value[1]); + } + } + }; + et2_box._attributes = { + // Not needed + "rows": { "ignore": true }, + "cols": { "ignore": true } + }; + return et2_box; +}(et2_core_baseWidget_1.et2_baseWidget)); +exports.et2_box = et2_box; +et2_core_widget_1.et2_register_widget(et2_box, ["vbox", "box"]); /** * Details widget implementation * widget name is "details" and can be use as a wrapping container @@ -167,80 +155,74 @@ et2_register_widget(et2_box, ["vbox", "box"]); *
* */ -var et2_details = (function(){ "use strict"; return et2_box.extend( -{ - attributes:{ - "toggle_align": { - name: "Toggle button alignment", - description:" Defines where to align the toggle button, default is right alignment", - type:"string", - default: "right" - }, - title: { - name: "title", - description:"Set a header title for box and shows it next to toggle button, default is no title", - type:"string", - default: "", - translate: true - } - }, - - init: function() { - this._super.apply(this, arguments); - - this.div = jQuery(document.createElement('div')).addClass('et2_details'); - this.title = jQuery(document.createElement('span')) - .addClass('et2_label et2_details_title') - .appendTo(this.div); - this.span = jQuery(document.createElement('span')) - .addClass('et2_details_toggle') - .appendTo(this.div); - this.wrapper = jQuery(document.createElement('div')) - .addClass('et2_details_wrapper') - .appendTo(this.div); - - - this._createWidget(); - }, - - /** - * Function happens on toggle action - */ - _toggle: function (){ - this.div.toggleClass('et2_details_expanded'); - }, - - /** - * Create widget, set contents, and binds handlers - */ - _createWidget: function () { - var self = this; - - this.span.on('click', function (e){ - self._toggle(); - }); - - //Set header title - if (this.options.title) - { - this.title - .click (function(){self._toggle();}) - .text(this.options.title); - } - - // Align toggle button left/right - if (this.options.toggle_align === "left") this.span.css({float:'left'}); - }, - - getDOMNode: function(_sender) { - if (!_sender || _sender === this) - { - return this.div[0]; - } - else - { - return this.wrapper[0]; - } - } -});}).call(this); -et2_register_widget(et2_details, ["details"]); \ No newline at end of file +var et2_details = /** @class */ (function (_super) { + __extends(et2_details, _super); + function et2_details(_parent, _attrs, _child) { + var _this = _super.call(this, _parent, _attrs, _child) || this; + _this.div = jQuery(document.createElement('div')).addClass('et2_details'); + _this.title = jQuery(document.createElement('span')) + .addClass('et2_label et2_details_title') + .appendTo(_this.div); + _this.span = jQuery(document.createElement('span')) + .addClass('et2_details_toggle') + .appendTo(_this.div); + _this.wrapper = jQuery(document.createElement('div')) + .addClass('et2_details_wrapper') + .appendTo(_this.div); + _this._createWidget(); + return _this; + } + /** + * Function happens on toggle action + */ + et2_details.prototype._toggle = function () { + this.div.toggleClass('et2_details_expanded'); + }; + /** + * Create widget, set contents, and binds handlers + */ + et2_details.prototype._createWidget = function () { + var self = this; + this.span.on('click', function (e) { + self._toggle(); + }); + //Set header title + if (this.options.title) { + this.title + .click(function () { + self._toggle(); + }) + .text(this.options.title); + } + // Align toggle button left/right + if (this.options.toggle_align === "left") + this.span.css({ float: 'left' }); + }; + et2_details.prototype.getDOMNode = function (_sender) { + if (!_sender || _sender === this) { + return this.div[0]; + } + else { + return this.wrapper[0]; + } + }; + et2_details._attributes = { + "toggle_align": { + name: "Toggle button alignment", + description: " Defines where to align the toggle button, default is right alignment", + type: "string", + default: "right" + }, + title: { + name: "title", + description: "Set a header title for box and shows it next to toggle button, default is no title", + type: "string", + default: "", + translate: true + } + }; + return et2_details; +}(et2_box)); +exports.et2_details = et2_details; +et2_core_widget_1.et2_register_widget(et2_details, ["details"]); +//# sourceMappingURL=et2_widget_box.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_box.ts b/api/js/etemplate/et2_widget_box.ts new file mode 100644 index 0000000000..8845a8dd00 --- /dev/null +++ b/api/js/etemplate/et2_widget_box.ts @@ -0,0 +1,262 @@ +/** + * EGroupware eTemplate2 - JS Box object + * + * @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 + * @version $Id$ + */ + +/*egw:uses + /vendor/bower-asset/jquery/dist/jquery.js; + et2_core_baseWidget; +*/ + + +import {et2_register_widget, WidgetConfig} from "./et2_core_widget"; +import {et2_baseWidget} from "./et2_core_baseWidget"; + +/** + * Class which implements box and vbox tag + * + * Auto-repeat: In order to get box auto repeat to work we need to have another + * box as a wrapper with an id set. + * + * @augments et2_baseWidget + */ +export class et2_box extends et2_baseWidget implements et2_IDetachedDOM +{ + static readonly _attributes: any = { + // Not needed + "rows": {"ignore": true}, + "cols": {"ignore": true} + }; + + div: JQuery; + + /** + * Constructor + * + * @memberOf et2_box + */ + constructor(_parent, _attrs? : WidgetConfig, _child? : object) + { + super(_parent, _attrs, _child); + + this.div = jQuery(document.createElement("div")) + .addClass("et2_" + this.getType()) + .addClass("et2_box_widget"); + + this.setDOMNode(this.div[0]); + } + + _createNamespace() : boolean + { + return true; + } + + /** + * Overriden so we can check for autorepeating children. We only check for + * $ in the immediate children & grandchildren of this node. + * + * @param {object} _node + */ + loadFromXML(_node) { + if(this.getType() != "box") + { + return super.loadFromXML(_node); + } + // Load the child nodes. + var childIndex = 0; + var repeatNode = null; + 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, if no expansion needed + var id = et2_readAttrWithDefault(node, "id", ""); + if(id.indexOf('$') < 0 || widgetType != 'box') + { + this.createElementFromNode(node); + childIndex++; + } + else + { + repeatNode = node; + } + } + + // Only the last child repeats(?) + if(repeatNode != null) + { + var currentPerspective = this.getArrayMgr("content").perspectiveData; + // Extra content + for(childIndex; typeof this.getArrayMgr("content").data[childIndex] != "undefined" && this.getArrayMgr("content").data[childIndex]; childIndex++) { + // Adjust for the row + var mgrs = this.getArrayMgrs(); + for(var name in mgrs) + { + if(this.getArrayMgr(name).getEntry(childIndex)) + { + this.getArrayMgr(name).setRow(childIndex); + } + } + + this.createElementFromNode(repeatNode); + } + + // Reset + for(var name in this.getArrayMgrs()) { + this.getArrayMgr(name).setPerspectiveData(currentPerspective); + } + } + } + + /** + * Code for implementing et2_IDetachedDOM + * This doesn't need to be implemented. + * Individual widgets are detected and handled by the grid, but the interface is needed for this to happen + * + * @param {array} _attrs array to add further attributes to + */ + getDetachedAttributes(_attrs) + { + _attrs.push('data'); + } + + getDetachedNodes() + { + return [this.getDOMNode()]; + } + + setDetachedAttributes(_nodes, _values) + { + if (_values.data) + { + var pairs = _values.data.split(/,/g); + for(var i=0; i < pairs.length; ++i) + { + var name_value = pairs[i].split(':'); + jQuery(_nodes[0]).attr('data-'+name_value[0], name_value[1]); + } + } + } + +} +et2_register_widget(et2_box, ["vbox", "box"]); + +/** + * Details widget implementation + * widget name is "details" and can be use as a wrapping container + * in order to make its children collapsible. + * + * Note: details widget does not represent html5 "details" tag in DOM + * + *
+ * + * .... + *
+ * + */ +export class et2_details extends et2_box +{ + static readonly _attributes: any = { + "toggle_align": { + name: "Toggle button alignment", + description:" Defines where to align the toggle button, default is right alignment", + type:"string", + default: "right" + }, + title: { + name: "title", + description: "Set a header title for box and shows it next to toggle button, default is no title", + type: "string", + default: "", + translate: true + } + }; + + private title: JQuery; + private span: JQuery; + private readonly wrapper: JQuery; + + constructor(_parent, _attrs?: WidgetConfig, _child?: object) { + super(_parent, _attrs, _child); + + this.div = jQuery(document.createElement('div')).addClass('et2_details'); + this.title = jQuery(document.createElement('span')) + .addClass('et2_label et2_details_title') + .appendTo(this.div); + this.span = jQuery(document.createElement('span')) + .addClass('et2_details_toggle') + .appendTo(this.div); + this.wrapper = jQuery(document.createElement('div')) + .addClass('et2_details_wrapper') + .appendTo(this.div); + + + this._createWidget(); + } + + /** + * Function happens on toggle action + */ + _toggle () + { + this.div.toggleClass('et2_details_expanded'); + } + + /** + * Create widget, set contents, and binds handlers + */ + _createWidget() { + const self = this; + + this.span.on('click', function (e) { + self._toggle(); + }); + + //Set header title + if (this.options.title) { + this.title + .click(function () { + self._toggle(); + }) + .text(this.options.title); + } + + // Align toggle button left/right + if (this.options.toggle_align === "left") this.span.css({float:'left'}); + } + + getDOMNode(_sender) + { + if (!_sender || _sender === this) + { + return this.div[0]; + } + else + { + return this.wrapper[0]; + } + } +} +et2_register_widget(et2_details, ["details"]); \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_button.js b/api/js/etemplate/et2_widget_button.js index 73d6590523..13fe2a5808 100644 --- a/api/js/etemplate/et2_widget_button.js +++ b/api/js/etemplate/et2_widget_button.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - JS Button object * @@ -6,440 +7,387 @@ * @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 - /vendor/bower-asset/jquery/dist/jquery.js; - et2_core_interfaces; - et2_core_baseWidget; + /vendor/bower-asset/jquery/dist/jquery.js; + et2_core_interfaces; + et2_core_baseWidget; */ - +require("./et2_core_common"); +var et2_core_inheritance_1 = require("./et2_core_inheritance"); +var et2_core_widget_1 = require("./et2_core_widget"); +var et2_core_baseWidget_1 = require("./et2_core_baseWidget"); +require("./et2_types"); /** * Class which implements the "button" XET-Tag - * @augments et2_baseWidget */ -var et2_button = (function(){ "use strict"; return et2_baseWidget.extend([et2_IInput, et2_IDetachedDOM], -{ - attributes: { - "label": { - "name": "caption", - "type": "string", - "description": "Label of the button", - "translate": true - }, - "image": { - "name": "Icon", - "type": "string", - "description": "Use an icon instead of label (when available)" - }, - "ro_image": { - "name": "Read-only Icon", - "type": "string", - "description": "Use this icon instead of hiding for read-only" - }, - "onclick": { - "description": "JS code which gets executed when the button is clicked" - }, - "accesskey": { - "name": "Access Key", - "type": "string", - "default": et2_no_init, - "description": "Alt + activates widget" - }, - "tabindex": { - "name": "Tab index", - "type": "integer", - "default": et2_no_init, - "description": "Specifies the tab order of a widget when the 'tab' button is used for navigating." - }, - background_image: { - name: "Add image in front of text", - type: "boolean", - description: "Adds image in front of text instead of just using an image with text as tooltip", - default: et2_no_init // to leave it undefined, if not defined, so background-image is assigned by default - }, - novalidate: { - name: "Do NOT validate form", - type: "boolean", - description: "Do NOT validate form before submitting it", - default: false - }, - // No such thing as a required button - "needed": { - "ignore": true - } - }, - - legacyOptions: ["image", "ro_image"], - - /** - * Constructor - * - * @memberOf et2_button - */ - init: function() { - this._super.apply(this, arguments); - - this.label = ""; - this.clicked = false; - this.btn = null; - this.image = null; - - if (!this.options.background_image && (this.options.image || this.options.ro_image)) - { - this.image = jQuery(document.createElement("img")) - .addClass("et2_button et2_button_icon"); - if (!this.options.readonly) this.image.addClass("et2_clickable"); - this.setDOMNode(this.image[0]); - return; - } - if (!this.options.readonly || this.options.ro_image) - { - this.btn = jQuery(document.createElement("button")) - .addClass("et2_button") - .attr({type:"button"}); - this.setDOMNode(this.btn[0]); - } - if (this.options.image) this.set_image(this.options.image); - }, - - /** - * Apply the "modifications" to the element and translate attributes marked - * with "translate: true" - * - * Reimplemented here to assign default background-images to buttons - * - * @param {object} _attrs - */ - transformAttributes: function(_attrs) - { - if (this.id && typeof _attrs.background_image == 'undefined' && !_attrs.image) - { - for(var image in et2_button.default_background_images) - { - if (this.id.match(et2_button.default_background_images[image])) - { - _attrs.image = image; - _attrs.background_image = true; - break; - } - } - } - for(var name in et2_button.default_classes) - { - if (this.id.match(et2_button.default_classes[name])) - { - _attrs.class = (typeof _attrs.class == 'undefined' ? '' : _attrs.class+' ')+name; - break; - } - } - this._super.apply(this, arguments); - }, - - set_accesskey: function(key) { - jQuery(this.node).attr("accesskey", key); - }, - /** - * Set image and update current image - * - * @param _image - */ - set_image: function(_image) { - this.options.image = _image; - this.update_image(); - }, - /** - * Set readonly image and update current image - * - * @param _image - */ - set_ro_image: function(_image) { - this.options.ro_image = _image; - this.update_image(); - }, - /** - * Set current image (dont update options.image) - * - * @param _image - */ - update_image: function(_image) { - if(!this.isInTree() || !this.options.background_image && this.image == null) return; - - if (typeof _image == 'undefined') - _image = this.options.readonly ? (this.options.ro_image || this.options.image) : this.options.image; - - // Silently blank for percentages instead of warning about missing image - use a progress widget - if(_image.match(/^[0-9]+\%$/)) - { - _image = ""; - //this.egw().debug("warn", "Use a progress widget instead of percentage images", this); - } - - var found_image = false; - if(_image != "") - { - var src = this.egw().image(_image); - if(src) - { - found_image = true; - } - else if (_image[0] == '/' || _image.substr(0,4) == 'http') - { - src= image; - found_image = true; - } - if(found_image) - { - if(this.image != null) - { - this.image.attr("src", src); - } - else if (this.options.background_image && this.btn) - { - this.btn.css("background-image","url("+src+")"); - this.btn.addClass('et2_button_with_image'); - } - } - } - if(!found_image) - { - this.set_label(this.label); - if(this.btn) - { - this.btn.css("background-image",""); - this.btn.removeClass('et2_button_with_image'); - } - } - }, - - /** - * Set options.readonly and update image - * - * @param {boolean} _ro - */ - set_readonly: function(_ro) - { - if (_ro != this.options.readonly) - { - this.options.readonly = _ro; - - if (this.options.image || this.options.ro_image) - { - this.update_image(); - } - // dont show readonly buttons as clickable - if (this.btn || this.image) - { - (this.btn || this.image) - .toggleClass('et2_clickable', !_ro) - .toggleClass('et2_button_ro', _ro) - .css('cursor', _ro ? 'default' : 'pointer'); // temp. 'til it is removed from et2_button - } - } - }, - - attachToDOM: function() { - this._super.apply(this, arguments); - - if (this.options.readonly && (this.btn || this.image)) - { - (this.btn || this.image) - .removeClass('et2_clickable') - .addClass('et2_button_ro') - .css('cursor', 'default'); // temp. 'til it is removed from et2_button - } - }, - - getDOMNode: function() { - return this.btn ? this.btn[0] : (this.image ? this.image[0] : null); - }, - - /** - * Overwritten to maintain an internal clicked attribute - * - * @param _ev - * @returns {Boolean} - */ - click: function(_ev) { - // ignore click on readonly button - if (this.options.readonly) return false; - - this.clicked = true; - - if (!this._super.apply(this, arguments)) - { - this.clicked = false; - return false; - } - - // Submit the form - if (this._type != "buttononly") - { - this.getInstanceManager().submit(this, false, this.options.novalidate); //TODO: this only needs to be passed if it's in a datagrid - } - this.clicked = false; - return true; - }, - - set_label: function(_value) { - if (this.btn) - { - this.label = _value; - - this.btn.text(_value); - - if (_value && !this.image) - this.btn.addClass('et2_button_text'); - else - this.btn.removeClass('et2_button_text'); - } - if(this.image) - { - this.image.attr("alt", _value); - // Don't set title if there's a tooltip, browser may show both - if(!this.options.statustext) - { - this.image.attr("title",_value); - } - } - }, - - /** - * Set tab index - * - * @param {number} index - */ - set_tabindex: function(index) { - jQuery(this.btn).attr("tabindex", index); - }, - - /** - * Implementation of the et2_IInput interface - */ - - /** - * Always return false as a button is never dirty - */ - isDirty: function() { - return false; - }, - - resetDirty: function() { - }, - - getValue: function() { - if (this.clicked) - { - return true; - } - - // If "null" is returned, the result is not added to the submitted - // array. - return null; - }, - isValid: function() { - return true; - }, - - /** - * et2_IDetachedDOM - * - * @param {array} _attrs - */ - getDetachedAttributes: function(_attrs) - { - _attrs.push("label", "value", "class", "image", "ro_image", "onclick", "background_image" ); - }, - - getDetachedNodes: function() - { - return [ - this.btn != null ? this.btn[0] : null, - this.image != null ? this.image[0] : null - ]; - }, - - setDetachedAttributes: function(_nodes, _values) - { - // Datagrid puts in the row for null - this.btn = _nodes[0].nodeName[0] != '#' ? jQuery(_nodes[0]) : null; - this.image = jQuery(_nodes[1]); - - if (typeof _values["id"] != "undefined") - { - this.set_id(_values["id"]); - } - if (typeof _values["label"] != "undefined") - { - this.set_label(_values["label"]); - } - if (typeof _values["value"] != "undefined") - { - } - if (typeof _values["image"] != "undefined") - { - this.set_image(_values["image"]); - } - if (typeof _values["ro_image"] != "undefined") - { - this.set_ro_image(_values["ro_image"]); - } - if (typeof _values["class"] != "undefined") - { - this.set_class(_values["class"]); - } - - if (typeof _values["onclick"] != "undefined") - { - this.options.onclick = _values["onclick"]; - } - var type = this._type; - var attrs = jQuery.extend(_values, this.options); - var parent = this._parent; - jQuery(this.getDOMNode()).bind("click.et2_baseWidget", this, function(e) { - var widget = et2_createWidget(type,attrs,parent); - e.data = widget; - e.data.set_id(_values["id"]); - return e.data.click.call(e.data,e); - }); - } -});}).call(this); -et2_register_widget(et2_button, ["button", "buttononly"]); - -// Static class stuff -jQuery.extend(et2_button, -/** @lends et2_button */ -{ - /** - * images to be used as background-image, if none is explicitly applied and id matches given regular expression - */ - default_background_images: { - save: /save(&|\]|$)/, - apply: /apply(&|\]|$)/, - cancel: /cancel(&|\]|$)/, - delete: /delete(&|\]|$)/, - discard: /discard(&|\]|$)/, - edit: /edit(&|\[\]|$)/, - next: /(next|continue)(&|\]|$)/, - finish: /finish(&|\]|$)/, - back: /(back|previous)(&|\]|$)/, - copy: /copy(&|\]|$)/, - more: /more(&|\]|$)/, - check: /(yes|check)(&|\]|$)/, - cancelled: /no(&|\]|$)/, - ok: /ok(&|\]|$)/, - close: /close(&|\]|$)/, - add: /(add(&|\]|$)|create)/ // customfields use create* - }, - - /** - * Classnames added automatic to buttons to set certain hover background colors - */ - default_classes: { - et2_button_cancel: /cancel(&|\]|$)/, // yellow - et2_button_question: /(yes|no)(&|\]|$)/, // yellow - et2_button_delete: /delete(&|\]|$)/ // red - } -}); +var et2_button = /** @class */ (function (_super) { + __extends(et2_button, _super); + /** + * Constructor + */ + function et2_button(_parent, _attrs, _child) { + var _this = + // Call the inherited constructor + _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_button._attributes, _child || {})) || this; + _this.legacyOptions = ["image", "ro_image"]; + _this.label = ""; + _this.clicked = false; + _this.btn = null; + _this.image = null; + if (!_this.options.background_image && (_this.options.image || _this.options.ro_image)) { + _this.image = jQuery(document.createElement("img")) + .addClass("et2_button et2_button_icon"); + if (!_this.options.readonly) + _this.image.addClass("et2_clickable"); + _this.setDOMNode(_this.image[0]); + return _this; + } + if (!_this.options.readonly || _this.options.ro_image) { + _this.btn = jQuery(document.createElement("button")) + .addClass("et2_button") + .attr({ type: "button" }); + _this.setDOMNode(_this.btn[0]); + } + if (_this.options.image) + _this.set_image(_this.options.image); + return _this; + } + /** + * Apply the "modifications" to the element and translate attributes marked + * with "translate: true" + * + * Reimplemented here to assign default background-images to buttons + * + * @param {object} _attrs + */ + et2_button.prototype.transformAttributes = function (_attrs) { + if (this.id && typeof _attrs.background_image == 'undefined' && !_attrs.image) { + for (var image in et2_button.default_background_images) { + if (this.id.match(et2_button.default_background_images[image])) { + _attrs.image = image; + _attrs.background_image = true; + break; + } + } + } + for (var name in et2_button.default_classes) { + if (this.id.match(et2_button.default_classes[name])) { + _attrs.class = (typeof _attrs.class == 'undefined' ? '' : _attrs.class + ' ') + name; + break; + } + } + _super.prototype.transformAttributes.call(this, _attrs); + }; + et2_button.prototype.set_accesskey = function (key) { + jQuery(this.node).attr("accesskey", key); + }; + /** + * Set image and update current image + * + * @param _image + */ + et2_button.prototype.set_image = function (_image) { + this.options.image = _image; + this.update_image(); + }; + /** + * Set readonly image and update current image + * + * @param _image + */ + et2_button.prototype.set_ro_image = function (_image) { + this.options.ro_image = _image; + this.update_image(); + }; + /** + * Set current image (dont update options.image) + * + * @param _image + */ + et2_button.prototype.update_image = function (_image) { + if (!this.isInTree() || !this.options.background_image && this.image == null) + return; + if (typeof _image == 'undefined') + _image = this.options.readonly ? (this.options.ro_image || this.options.image) : this.options.image; + // Silently blank for percentages instead of warning about missing image - use a progress widget + if (_image.match(/^[0-9]+\%$/)) { + _image = ""; + //this.egw().debug("warn", "Use a progress widget instead of percentage images", this); + } + var found_image = false; + if (_image != "") { + var src = this.egw().image(_image); + if (src) { + found_image = true; + } + else if (_image[0] == '/' || _image.substr(0, 4) == 'http') { + src = _image; + found_image = true; + } + if (found_image) { + if (this.image != null) { + this.image.attr("src", src); + } + else if (this.options.background_image && this.btn) { + this.btn.css("background-image", "url(" + src + ")"); + this.btn.addClass('et2_button_with_image'); + } + } + } + if (!found_image) { + this.set_label(this.label); + if (this.btn) { + this.btn.css("background-image", ""); + this.btn.removeClass('et2_button_with_image'); + } + } + }; + /** + * Set options.readonly and update image + * + * @param {boolean} _ro + */ + et2_button.prototype.set_readonly = function (_ro) { + if (_ro != this.options.readonly) { + this.options.readonly = _ro; + if (this.options.image || this.options.ro_image) { + this.update_image(); + } + // dont show readonly buttons as clickable + if (this.btn || this.image) { + (this.btn || this.image) + .toggleClass('et2_clickable', !_ro) + .toggleClass('et2_button_ro', _ro) + .css('cursor', _ro ? 'default' : 'pointer'); // temp. 'til it is removed from et2_button + } + } + }; + et2_button.prototype.attachToDOM = function () { + var ret = _super.prototype.attachToDOM.call(this); + if (this.options.readonly && (this.btn || this.image)) { + (this.btn || this.image) + .removeClass('et2_clickable') + .addClass('et2_button_ro') + .css('cursor', 'default'); // temp. 'til it is removed from et2_button + } + return ret; + }; + et2_button.prototype.getDOMNode = function () { + return this.btn ? this.btn[0] : (this.image ? this.image[0] : null); + }; + /** + * Overwritten to maintain an internal clicked attribute + * + * @param _ev + * @returns {Boolean} + */ + et2_button.prototype.click = function (_ev) { + // ignore click on readonly button + if (this.options.readonly) + return false; + this.clicked = true; + if (!_super.prototype.click.apply(this, arguments)) { + this.clicked = false; + return false; + } + // Submit the form + if (this.getType() != "buttononly") { + this.getInstanceManager().submit(this, false, this.options.novalidate); //TODO: this only needs to be passed if it's in a datagrid + } + this.clicked = false; + return true; + }; + et2_button.prototype.set_label = function (_value) { + if (this.btn) { + this.label = _value; + this.btn.text(_value); + if (_value && !this.image) + this.btn.addClass('et2_button_text'); + else + this.btn.removeClass('et2_button_text'); + } + if (this.image) { + this.image.attr("alt", _value); + // Don't set title if there's a tooltip, browser may show both + if (!this.options.statustext) { + this.image.attr("title", _value); + } + } + }; + /** + * Set tab index + * + * @param {number} index + */ + et2_button.prototype.set_tabindex = function (index) { + jQuery(this.btn).attr("tabindex", index); + }; + /** + * Implementation of the et2_IInput interface + */ + /** + * Always return false as a button is never dirty + */ + et2_button.prototype.isDirty = function () { + return false; + }; + et2_button.prototype.resetDirty = function () { + }; + et2_button.prototype.getValue = function () { + if (this.clicked) { + return true; + } + // If "null" is returned, the result is not added to the submitted + // array. + return null; + }; + et2_button.prototype.isValid = function () { + return true; + }; + /** + * et2_IDetachedDOM + * + * @param {array} _attrs + */ + et2_button.prototype.getDetachedAttributes = function (_attrs) { + _attrs.push("label", "value", "class", "image", "ro_image", "onclick", "background_image"); + }; + et2_button.prototype.getDetachedNodes = function () { + return [ + this.btn != null ? this.btn[0] : null, + this.image != null ? this.image[0] : null + ]; + }; + et2_button.prototype.setDetachedAttributes = function (_nodes, _values) { + // Datagrid puts in the row for null + this.btn = _nodes[0].nodeName[0] != '#' ? jQuery(_nodes[0]) : null; + this.image = jQuery(_nodes[1]); + if (typeof _values["id"] != "undefined") { + this.set_id(_values["id"]); + } + if (typeof _values["label"] != "undefined") { + this.set_label(_values["label"]); + } + if (typeof _values["value"] != "undefined") { + } + if (typeof _values["image"] != "undefined") { + this.set_image(_values["image"]); + } + if (typeof _values["ro_image"] != "undefined") { + this.set_ro_image(_values["ro_image"]); + } + if (typeof _values["class"] != "undefined") { + this.set_class(_values["class"]); + } + if (typeof _values["onclick"] != "undefined") { + this.options.onclick = _values["onclick"]; + } + var type = this.getType(); + var attrs = jQuery.extend(_values, this.options); + var parent = this.getParent(); + jQuery(this.getDOMNode()).bind("click.et2_baseWidget", this, function (e) { + var widget = et2_core_widget_1.et2_createWidget(type, attrs, parent); + e.data = widget; + e.data.set_id(_values["id"]); + return e.data.click.call(e.data, e); + }); + }; + et2_button._attributes = { + "label": { + "name": "caption", + "type": "string", + "description": "Label of the button", + "translate": true + }, + "image": { + "name": "Icon", + "type": "string", + "description": "Use an icon instead of label (when available)" + }, + "ro_image": { + "name": "Read-only Icon", + "type": "string", + "description": "Use this icon instead of hiding for read-only" + }, + "onclick": { + "description": "JS code which gets executed when the button is clicked", + "type": "js" + }, + "accesskey": { + "name": "Access Key", + "type": "string", + "default": et2_no_init, + "description": "Alt + activates widget" + }, + "tabindex": { + "name": "Tab index", + "type": "integer", + "default": et2_no_init, + "description": "Specifies the tab order of a widget when the 'tab' button is used for navigating." + }, + background_image: { + name: "Add image in front of text", + type: "boolean", + description: "Adds image in front of text instead of just using an image with text as tooltip", + default: et2_no_init // to leave it undefined, if not defined, so background-image is assigned by default + }, + novalidate: { + name: "Do NOT validate form", + type: "boolean", + description: "Do NOT validate form before submitting it", + default: false + }, + // No such thing as a required button + "needed": { + "ignore": true + } + }; + /** + * images to be used as background-image, if none is explicitly applied and id matches given regular expression + */ + et2_button.default_background_images = { + save: /save(&|\]|$)/, + apply: /apply(&|\]|$)/, + cancel: /cancel(&|\]|$)/, + delete: /delete(&|\]|$)/, + discard: /discard(&|\]|$)/, + edit: /edit(&|\[\]|$)/, + next: /(next|continue)(&|\]|$)/, + finish: /finish(&|\]|$)/, + back: /(back|previous)(&|\]|$)/, + copy: /copy(&|\]|$)/, + more: /more(&|\]|$)/, + check: /(yes|check)(&|\]|$)/, + cancelled: /no(&|\]|$)/, + ok: /ok(&|\]|$)/, + close: /close(&|\]|$)/, + add: /(add(&|\]|$)|create)/ // customfields use create* + }; + /** + * Classnames added automatic to buttons to set certain hover background colors + */ + et2_button.default_classes = { + et2_button_cancel: /cancel(&|\]|$)/, + et2_button_question: /(yes|no)(&|\]|$)/, + et2_button_delete: /delete(&|\]|$)/ // red + }; + return et2_button; +}(et2_core_baseWidget_1.et2_baseWidget)); +exports.et2_button = et2_button; +et2_core_widget_1.et2_register_widget(et2_button, ["button", "buttononly"]); +//# sourceMappingURL=et2_widget_button.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_button.ts b/api/js/etemplate/et2_widget_button.ts new file mode 100644 index 0000000000..ca87766c94 --- /dev/null +++ b/api/js/etemplate/et2_widget_button.ts @@ -0,0 +1,459 @@ +/** + * EGroupware eTemplate2 - JS Button object + * + * @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 + /vendor/bower-asset/jquery/dist/jquery.js; + et2_core_interfaces; + et2_core_baseWidget; +*/ + +import './et2_core_common'; +import {ClassWithAttributes} from "./et2_core_inheritance"; +import {et2_createWidget, et2_register_widget, WidgetConfig} from "./et2_core_widget"; +import {et2_baseWidget} from './et2_core_baseWidget' +import './et2_types'; + +/** + * Class which implements the "button" XET-Tag + */ +export class et2_button extends et2_baseWidget implements et2_IInput, et2_IDetachedDOM +{ + static readonly _attributes : any = { + "label": { + "name": "caption", + "type": "string", + "description": "Label of the button", + "translate": true + }, + "image": { + "name": "Icon", + "type": "string", + "description": "Use an icon instead of label (when available)" + }, + "ro_image": { + "name": "Read-only Icon", + "type": "string", + "description": "Use this icon instead of hiding for read-only" + }, + "onclick": { + "description": "JS code which gets executed when the button is clicked", + "type": "js" + }, + "accesskey": { + "name": "Access Key", + "type": "string", + "default": et2_no_init, + "description": "Alt + activates widget" + }, + "tabindex": { + "name": "Tab index", + "type": "integer", + "default": et2_no_init, + "description": "Specifies the tab order of a widget when the 'tab' button is used for navigating." + }, + background_image: { + name: "Add image in front of text", + type: "boolean", + description: "Adds image in front of text instead of just using an image with text as tooltip", + default: et2_no_init // to leave it undefined, if not defined, so background-image is assigned by default + }, + novalidate: { + name: "Do NOT validate form", + type: "boolean", + description: "Do NOT validate form before submitting it", + default: false + }, + // No such thing as a required button + "needed": { + "ignore": true + } + }; + + legacyOptions: string[] = ["image", "ro_image"]; + + /** + * images to be used as background-image, if none is explicitly applied and id matches given regular expression + */ + static readonly default_background_images: object = { + save: /save(&|\]|$)/, + apply: /apply(&|\]|$)/, + cancel: /cancel(&|\]|$)/, + delete: /delete(&|\]|$)/, + discard: /discard(&|\]|$)/, + edit: /edit(&|\[\]|$)/, + next: /(next|continue)(&|\]|$)/, + finish: /finish(&|\]|$)/, + back: /(back|previous)(&|\]|$)/, + copy: /copy(&|\]|$)/, + more: /more(&|\]|$)/, + check: /(yes|check)(&|\]|$)/, + cancelled: /no(&|\]|$)/, + ok: /ok(&|\]|$)/, + close: /close(&|\]|$)/, + add: /(add(&|\]|$)|create)/ // customfields use create* + }; + + /** + * Classnames added automatic to buttons to set certain hover background colors + */ + static readonly default_classes: object = { + et2_button_cancel: /cancel(&|\]|$)/, // yellow + et2_button_question: /(yes|no)(&|\]|$)/, // yellow + et2_button_delete: /delete(&|\]|$)/ // red + }; + + label: string = ""; + clicked: boolean = false; + btn: JQuery = null; + image: JQuery = null; + + /** + * Constructor + */ + constructor(_parent, _attrs? : WidgetConfig, _child? : object) + { + // Call the inherited constructor + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_button._attributes, _child || {})); + + if (!this.options.background_image && (this.options.image || this.options.ro_image)) + { + this.image = jQuery(document.createElement("img")) + .addClass("et2_button et2_button_icon"); + if (!this.options.readonly) this.image.addClass("et2_clickable"); + this.setDOMNode(this.image[0]); + return; + } + if (!this.options.readonly || this.options.ro_image) + { + this.btn = jQuery(document.createElement("button")) + .addClass("et2_button") + .attr({type:"button"}); + this.setDOMNode(this.btn[0]); + } + if (this.options.image) this.set_image(this.options.image); + } + + /** + * Apply the "modifications" to the element and translate attributes marked + * with "translate: true" + * + * Reimplemented here to assign default background-images to buttons + * + * @param {object} _attrs + */ + transformAttributes(_attrs) + { + if (this.id && typeof _attrs.background_image == 'undefined' && !_attrs.image) + { + for(var image in et2_button.default_background_images) + { + if (this.id.match(et2_button.default_background_images[image])) + { + _attrs.image = image; + _attrs.background_image = true; + break; + } + } + } + for(var name in et2_button.default_classes) + { + if (this.id.match(et2_button.default_classes[name])) + { + _attrs.class = (typeof _attrs.class == 'undefined' ? '' : _attrs.class+' ')+name; + break; + } + } + super.transformAttributes(_attrs); + } + + set_accesskey(key) + { + jQuery(this.node).attr("accesskey", key); + } + /** + * Set image and update current image + * + * @param _image + */ + set_image(_image) + { + this.options.image = _image; + this.update_image(); + } + /** + * Set readonly image and update current image + * + * @param _image + */ + set_ro_image(_image) + { + this.options.ro_image = _image; + this.update_image(); + } + /** + * Set current image (dont update options.image) + * + * @param _image + */ + update_image(_image?) + { + if(!this.isInTree() || !this.options.background_image && this.image == null) return; + + if (typeof _image == 'undefined') + _image = this.options.readonly ? (this.options.ro_image || this.options.image) : this.options.image; + + // Silently blank for percentages instead of warning about missing image - use a progress widget + if(_image.match(/^[0-9]+\%$/)) + { + _image = ""; + //this.egw().debug("warn", "Use a progress widget instead of percentage images", this); + } + + var found_image = false; + if(_image != "") + { + var src = this.egw().image(_image); + if(src) + { + found_image = true; + } + else if (_image[0] == '/' || _image.substr(0,4) == 'http') + { + src = _image; + found_image = true; + } + if(found_image) + { + if(this.image != null) + { + this.image.attr("src", src); + } + else if (this.options.background_image && this.btn) + { + this.btn.css("background-image","url("+src+")"); + this.btn.addClass('et2_button_with_image'); + } + } + } + if(!found_image) + { + this.set_label(this.label); + if(this.btn) + { + this.btn.css("background-image",""); + this.btn.removeClass('et2_button_with_image'); + } + } + } + + /** + * Set options.readonly and update image + * + * @param {boolean} _ro + */ + set_readonly(_ro) + { + if (_ro != this.options.readonly) + { + this.options.readonly = _ro; + + if (this.options.image || this.options.ro_image) + { + this.update_image(); + } + // dont show readonly buttons as clickable + if (this.btn || this.image) + { + (this.btn || this.image) + .toggleClass('et2_clickable', !_ro) + .toggleClass('et2_button_ro', _ro) + .css('cursor', _ro ? 'default' : 'pointer'); // temp. 'til it is removed from et2_button + } + } + } + + attachToDOM() + { + let ret = super.attachToDOM(); + + if (this.options.readonly && (this.btn || this.image)) + { + (this.btn || this.image) + .removeClass('et2_clickable') + .addClass('et2_button_ro') + .css('cursor', 'default'); // temp. 'til it is removed from et2_button + } + return ret; + } + + getDOMNode() + { + return this.btn ? this.btn[0] : (this.image ? this.image[0] : null); + } + + /** + * Overwritten to maintain an internal clicked attribute + * + * @param _ev + * @returns {Boolean} + */ + click(_ev) + { + // ignore click on readonly button + if (this.options.readonly) return false; + + this.clicked = true; + + if (!super.click.apply(this, arguments)) + { + this.clicked = false; + return false; + } + + // Submit the form + if (this.getType() != "buttononly") + { + this.getInstanceManager().submit(this, false, this.options.novalidate); //TODO: this only needs to be passed if it's in a datagrid + } + this.clicked = false; + return true; + } + + set_label(_value) + { + if (this.btn) + { + this.label = _value; + + this.btn.text(_value); + + if (_value && !this.image) + this.btn.addClass('et2_button_text'); + else + this.btn.removeClass('et2_button_text'); + } + if(this.image) + { + this.image.attr("alt", _value); + // Don't set title if there's a tooltip, browser may show both + if(!this.options.statustext) + { + this.image.attr("title",_value); + } + } + } + + /** + * Set tab index + * + * @param {number} index + */ + set_tabindex(index) + { + jQuery(this.btn).attr("tabindex", index); + } + + /** + * Implementation of the et2_IInput interface + */ + + /** + * Always return false as a button is never dirty + */ + isDirty() + { + return false; + } + + resetDirty() + { + } + + getValue() + { + if (this.clicked) + { + return true; + } + + // If "null" is returned, the result is not added to the submitted + // array. + return null; + } + + isValid() + { + return true; + } + + /** + * et2_IDetachedDOM + * + * @param {array} _attrs + */ + getDetachedAttributes(_attrs) + { + _attrs.push("label", "value", "class", "image", "ro_image", "onclick", "background_image" ); + } + + getDetachedNodes() + { + return [ + this.btn != null ? this.btn[0] : null, + this.image != null ? this.image[0] : null + ]; + } + + setDetachedAttributes(_nodes, _values) + { + // Datagrid puts in the row for null + this.btn = _nodes[0].nodeName[0] != '#' ? jQuery(_nodes[0]) : null; + this.image = jQuery(_nodes[1]); + + if (typeof _values["id"] != "undefined") + { + this.set_id(_values["id"]); + } + if (typeof _values["label"] != "undefined") + { + this.set_label(_values["label"]); + } + if (typeof _values["value"] != "undefined") + { + } + if (typeof _values["image"] != "undefined") + { + this.set_image(_values["image"]); + } + if (typeof _values["ro_image"] != "undefined") + { + this.set_ro_image(_values["ro_image"]); + } + if (typeof _values["class"] != "undefined") + { + this.set_class(_values["class"]); + } + + if (typeof _values["onclick"] != "undefined") + { + this.options.onclick = _values["onclick"]; + } + var type = this.getType(); + var attrs = jQuery.extend(_values, this.options); + var parent = this.getParent(); + jQuery(this.getDOMNode()).bind("click.et2_baseWidget", this, function(e) { + var widget = et2_createWidget(type,attrs,parent); + e.data = widget; + e.data.set_id(_values["id"]); + return e.data.click.call(e.data,e); + }); + } +} +et2_register_widget(et2_button, ["button", "buttononly"]); diff --git a/api/js/etemplate/et2_widget_checkbox.js b/api/js/etemplate/et2_widget_checkbox.js index 28c4c17e96..a4cc2f0f42 100644 --- a/api/js/etemplate/et2_widget_checkbox.js +++ b/api/js/etemplate/et2_widget_checkbox.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - JS Checkbox object * @@ -9,255 +10,256 @@ * @copyright Nathan Gray 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 - /vendor/bower-asset/jquery/dist/jquery.js; - et2_core_inputWidget; - et2_core_valueWidget; + /vendor/bower-asset/jquery/dist/jquery.js; + et2_core_inputWidget; + et2_core_valueWidget; */ - +var et2_core_widget_1 = require("./et2_core_widget"); +var et2_core_inputWidget_1 = require("./et2_core_inputWidget"); +var et2_core_inheritance_1 = require("./et2_core_inheritance"); /** * Class which implements the "checkbox" XET-Tag * * @augments et2_inputWidget */ -var et2_checkbox = (function(){ "use strict"; return et2_inputWidget.extend( -{ - attributes: { - "selected_value": { - "name": "Set value", - "type": "string", - "default": "true", - "description": "Value when checked" - }, - "unselected_value": { - "name": "Unset value", - "type": "string", - "default": "", - "description": "Value when not checked" - }, - "ro_true": { - "name": "Read only selected", - "type": "string", - "default": "X ", - "description": "What should be displayed when readonly and selected" - }, - "ro_false": { - "name": "Read only unselected", - "type": "string", - "default": "", - "description": "What should be displayed when readonly and not selected" - }, - "value": { - // Stop framework from messing with value - "type": "any" - }, - "toggle_on": { - "name": "Toggle on caption", - "type": "string", - "default": "", - "description": "String caption to show for ON status", - "translate": true - }, - "toggle_off": { - "name": "Toggle off caption", - "type": "string", - "default": "", - "description": "String caption to show OFF status", - "translate": true - } - }, - - legacyOptions: ["selected_value", "unselected_value", "ro_true", "ro_false"], - - /** - * Constructor - * - * @memberOf et2_checkbox - */ - init: function() { - this._super.apply(this, arguments); - - this.input = null; - - this.createInputWidget(); - - }, - - createInputWidget: function() { - this.input = jQuery(document.createElement("input")).attr("type", "checkbox"); - - this.input.addClass("et2_checkbox"); - - if (this.options.toggle_on || this.options.toggle_off) - { - var self = this; - // checkbox container - this.toggle = jQuery(document.createElement('span')) - .addClass('et2_checkbox_slideSwitch') - .append(this.input); - // update switch status on change - this.input.change(function(){ - self.getValue(); - return true; - }); - // switch container - var area = jQuery(document.createElement('span')).addClass('slideSwitch_container').appendTo(this.toggle); - // on span tag - var on = jQuery(document.createElement('span')).addClass('on').appendTo(area); - // off span tag - var off = jQuery(document.createElement('span')).addClass('off').appendTo(area); - on.text(this.options.toggle_on); - off.text(this.options.toggle_off); - - // handle a tag - var handle = jQuery(document.createElement('a')).appendTo(area); - this.setDOMNode(this.toggle[0]); - } - else - { - this.setDOMNode(this.input[0]); - } - - }, - - /** - * Override default to place checkbox before label, if there is no %s in the label - * - * @param {string} label - */ - set_label: function(label) { - if(label.length && label.indexOf('%s') < 0) - { - label = '%s'+label; - } - this._super.apply(this, [label]); - jQuery(this.getSurroundings()._widgetSurroundings).addClass('et2_checkbox_label'); - }, - /** - * Override default to match against set/unset value - * - * @param {string|boolean} _value - */ - set_value: function(_value) - { - // in php, our database storage and et2_checkType(): "0" == false - if (_value === "0" && this.options.selected_value != "0") - { - _value = false; - } - if(_value != this.value) { - if(_value == this.options.selected_value || - _value && this.options.selected_value == this.attributes.selected_value["default"] && - _value != this.options.unselected_value) { - if (this.options.toggle_on || this.options.toggle_off) this.toggle.addClass('switchOn'); - this.input.prop("checked", true); - } else { - this.input.prop("checked", false); - if (this.options.toggle_on || this.options.toggle_off) this.toggle.removeClass('switchOn'); - } - } - }, - - /** - * Disable checkbox on runtime - * - * @param {boolean} _ro - */ - set_readonly: function(_ro) - { - jQuery(this.getDOMNode()).attr('disabled', _ro); - }, - - /** - * Override default to return unchecked value - */ - getValue: function() { - if(this.input.prop("checked")) { - if (this.options.toggle_on || this.options.toggle_off) this.toggle.addClass('switchOn'); - return this.options.selected_value; - } else { - if (this.options.toggle_on || this.options.toggle_off) this.toggle.removeClass('switchOn'); - return this.options.unselected_value; - } - } -});}).call(this); -et2_register_widget(et2_checkbox, ["checkbox"]); - +var et2_checkbox = /** @class */ (function (_super) { + __extends(et2_checkbox, _super); + /** + * Constructor + * + * @memberOf et2_checkbox + */ + function et2_checkbox(_parent, _attrs, _child) { + var _this = + // Call the inherited constructor + _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_checkbox._attributes, _child || {})) || this; + _this.legacyOptions = ["selected_value", "unselected_value", "ro_true", "ro_false"]; + _this.input = null; + _this.toggle = null; + _this.input = null; + _this.createInputWidget(); + return _this; + } + et2_checkbox.prototype.createInputWidget = function () { + this.input = jQuery(document.createElement("input")).attr("type", "checkbox"); + this.input.addClass("et2_checkbox"); + if (this.options.toggle_on || this.options.toggle_off) { + var self_1 = this; + // checkbox container + this.toggle = jQuery(document.createElement('span')) + .addClass('et2_checkbox_slideSwitch') + .append(this.input); + // update switch status on change + this.input.change(function () { + self_1.getValue(); + return true; + }); + // switch container + var area = jQuery(document.createElement('span')).addClass('slideSwitch_container').appendTo(this.toggle); + // on span tag + var on = jQuery(document.createElement('span')).addClass('on').appendTo(area); + // off span tag + var off = jQuery(document.createElement('span')).addClass('off').appendTo(area); + on.text(this.options.toggle_on); + off.text(this.options.toggle_off); + // handle a tag + jQuery(document.createElement('a')).appendTo(area); + this.setDOMNode(this.toggle[0]); + } + else { + this.setDOMNode(this.input[0]); + } + }; + /** + * Override default to place checkbox before label, if there is no %s in the label + * + * @param {string} label + */ + et2_checkbox.prototype.set_label = function (label) { + if (label.length && label.indexOf('%s') < 0) { + label = '%s' + label; + } + _super.prototype.set_label.call(this, label); + jQuery(this.getSurroundings().getWidgetSurroundings()).addClass('et2_checkbox_label'); + }; + /** + * Override default to match against set/unset value + * + * @param {string|boolean} _value + */ + et2_checkbox.prototype.set_value = function (_value) { + // in php, our database storage and et2_checkType(): "0" == false + if (_value === "0" && this.options.selected_value != "0") { + _value = false; + } + if (_value != this.value) { + if (_value == this.options.selected_value || + _value && this.options.selected_value == this.attributes["selected_value"]["default"] && + _value != this.options.unselected_value) { + if (this.options.toggle_on || this.options.toggle_off) + this.toggle.addClass('switchOn'); + this.input.prop("checked", true); + } + else { + this.input.prop("checked", false); + if (this.options.toggle_on || this.options.toggle_off) + this.toggle.removeClass('switchOn'); + } + } + }; + /** + * Disable checkbox on runtime + * + * @param {boolean} _ro + */ + et2_checkbox.prototype.set_readonly = function (_ro) { + jQuery(this.getDOMNode()).attr('disabled', _ro); + }; + /** + * Override default to return unchecked value + */ + et2_checkbox.prototype.getValue = function () { + if (this.input.prop("checked")) { + if (this.options.toggle_on || this.options.toggle_off) + this.toggle.addClass('switchOn'); + return this.options.selected_value; + } + else { + if (this.options.toggle_on || this.options.toggle_off) + this.toggle.removeClass('switchOn'); + return this.options.unselected_value; + } + }; + et2_checkbox._attributes = { + "selected_value": { + "name": "Set value", + "type": "string", + "default": "true", + "description": "Value when checked" + }, + "unselected_value": { + "name": "Unset value", + "type": "string", + "default": "", + "description": "Value when not checked" + }, + "ro_true": { + "name": "Read only selected", + "type": "string", + "default": "X ", + "description": "What should be displayed when readonly and selected" + }, + "ro_false": { + "name": "Read only unselected", + "type": "string", + "default": "", + "description": "What should be displayed when readonly and not selected" + }, + "value": { + // Stop framework from messing with value + "type": "any" + }, + "toggle_on": { + "name": "Toggle on caption", + "type": "string", + "default": "", + "description": "String caption to show for ON status", + "translate": true + }, + "toggle_off": { + "name": "Toggle off caption", + "type": "string", + "default": "", + "description": "String caption to show OFF status", + "translate": true + } + }; + return et2_checkbox; +}(et2_core_inputWidget_1.et2_inputWidget)); +et2_core_widget_1.et2_register_widget(et2_checkbox, ["checkbox"]); /** - * et2_checkbox_ro is the dummy readonly implementation of the checkbox - * @augments et2_checkbox - */ -var et2_checkbox_ro = (function(){ "use strict"; return et2_checkbox.extend([et2_IDetachedDOM], -{ - /** - * Ignore unset value - */ - attributes: { - "unselected_value": { - "ignore": true - } - }, - - /** - * Constructor - * - * @memberOf et2_checkbox_ro - */ - init: function() { - this._super.apply(this, arguments); - - this.value = ""; - this.span = jQuery(document.createElement("span")) - .addClass("et2_checkbox_ro"); - - this.setDOMNode(this.span[0]); - }, - - /** - * note: checkbox is checked if even there is a value but not only if the _value is only "true" - * it's an exceptional validation for cases that we pass non boolean values as checkbox _value - * - * @param {string|boolean} _value - */ - set_value: function(_value) { - if(_value == this.options.selected_value ||_value && this.options.selected_value == this.attributes.selected_value["default"] && - _value != this.options.unselected_value) { - this.span.text(this.options.ro_true); - this.value = _value; - } else { - this.span.text(this.options.ro_false); - } - }, - - /** - * Code for implementing et2_IDetachedDOM - * - * @param {array} _attrs - */ - getDetachedAttributes: function(_attrs) - { - _attrs.push("value", "class"); - }, - - getDetachedNodes: function() - { - return [this.span[0]]; - }, - - setDetachedAttributes: function(_nodes, _values) - { - // Update the properties - if (typeof _values["value"] != "undefined") - { - this.span = jQuery(_nodes[0]); - this.set_value(_values["value"]); - } - - if (typeof _values["class"] != "undefined") - { - _nodes[0].setAttribute("class", _values["class"]); - } - } -});}).call(this); -et2_register_widget(et2_checkbox_ro, ["checkbox_ro"]); +* et2_checkbox_ro is the dummy readonly implementation of the checkbox +* @augments et2_checkbox +*/ +var et2_checkbox_ro = /** @class */ (function (_super) { + __extends(et2_checkbox_ro, _super); + /** + * Constructor + * + * @memberOf et2_checkbox_ro + */ + function et2_checkbox_ro(_parent, _attrs, _child) { + var _this = + // Call the inherited constructor + _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_checkbox_ro._attributes, _child || {})) || this; + _this.span = null; + _this.value = ""; + _this.span = jQuery(document.createElement("span")) + .addClass("et2_checkbox_ro"); + _this.setDOMNode(_this.span[0]); + return _this; + } + /** + * note: checkbox is checked if even there is a value but not only if the _value is only "true" + * it's an exceptional validation for cases that we pass non boolean values as checkbox _value + * + * @param {string|boolean} _value + */ + et2_checkbox_ro.prototype.set_value = function (_value) { + if (_value == this.options.selected_value || _value && this.options.selected_value == this.attributes["selected_value"]["default"] && + _value != this.options.unselected_value) { + this.span.text(this.options.ro_true); + this.value = _value; + } + else { + this.span.text(this.options.ro_false); + } + }; + /** + * Code for implementing et2_IDetachedDOM + * + * @param {array} _attrs + */ + et2_checkbox_ro.prototype.getDetachedAttributes = function (_attrs) { + _attrs.push("value", "class"); + }; + et2_checkbox_ro.prototype.getDetachedNodes = function () { + return [this.span[0]]; + }; + et2_checkbox_ro.prototype.setDetachedAttributes = function (_nodes, _values) { + // Update the properties + if (typeof _values["value"] != "undefined") { + this.span = jQuery(_nodes[0]); + this.set_value(_values["value"]); + } + if (typeof _values["class"] != "undefined") { + _nodes[0].setAttribute("class", _values["class"]); + } + }; + /** + * Ignore unset value + */ + et2_checkbox_ro._attributes = { + "unselected_value": { + "ignore": true + } + }; + return et2_checkbox_ro; +}(et2_checkbox)); +et2_core_widget_1.et2_register_widget(et2_checkbox_ro, ["checkbox_ro"]); +//# sourceMappingURL=et2_widget_checkbox.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_checkbox.ts b/api/js/etemplate/et2_widget_checkbox.ts new file mode 100644 index 0000000000..2b330e8e82 --- /dev/null +++ b/api/js/etemplate/et2_widget_checkbox.ts @@ -0,0 +1,276 @@ +/** + * EGroupware eTemplate2 - JS Checkbox object + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage api + * @link http://www.egroupware.org + * @author Nathan Gray + * @copyright Nathan Gray 2011 + * @version $Id$ + */ + +/*egw:uses + /vendor/bower-asset/jquery/dist/jquery.js; + et2_core_inputWidget; + et2_core_valueWidget; +*/ + +import {et2_register_widget, WidgetConfig} from "./et2_core_widget"; +import {et2_inputWidget} from "./et2_core_inputWidget"; +import {ClassWithAttributes} from "./et2_core_inheritance"; + +/** + * Class which implements the "checkbox" XET-Tag + * + * @augments et2_inputWidget + */ +class et2_checkbox extends et2_inputWidget +{ + static readonly _attributes : any = { + "selected_value": { + "name": "Set value", + "type": "string", + "default": "true", + "description": "Value when checked" + }, + "unselected_value": { + "name": "Unset value", + "type": "string", + "default": "", + "description": "Value when not checked" + }, + "ro_true": { + "name": "Read only selected", + "type": "string", + "default": "X ", + "description": "What should be displayed when readonly and selected" + }, + "ro_false": { + "name": "Read only unselected", + "type": "string", + "default": "", + "description": "What should be displayed when readonly and not selected" + }, + "value": { + // Stop framework from messing with value + "type": "any" + }, + "toggle_on": { + "name": "Toggle on caption", + "type": "string", + "default": "", + "description": "String caption to show for ON status", + "translate": true + }, + "toggle_off": { + "name": "Toggle off caption", + "type": "string", + "default": "", + "description": "String caption to show OFF status", + "translate": true + } + }; + + legacyOptions : string[] = ["selected_value", "unselected_value", "ro_true", "ro_false"]; + input : JQuery = null; + toggle : JQuery = null; + value : string | boolean; + + /** + * Constructor + * + * @memberOf et2_checkbox + */ + constructor(_parent, _attrs? : WidgetConfig, _child? : object) + { + // Call the inherited constructor + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_checkbox._attributes, _child || {})); + this.input = null; + this.createInputWidget(); + } + + createInputWidget() + { + this.input = jQuery(document.createElement("input")).attr("type", "checkbox"); + + this.input.addClass("et2_checkbox"); + + if (this.options.toggle_on || this.options.toggle_off) + { + let self = this; + // checkbox container + this.toggle = jQuery(document.createElement('span')) + .addClass('et2_checkbox_slideSwitch') + .append(this.input); + // update switch status on change + this.input.change(function(){ + self.getValue(); + return true; + }); + // switch container + let area = jQuery(document.createElement('span')).addClass('slideSwitch_container').appendTo(this.toggle); + // on span tag + let on = jQuery(document.createElement('span')).addClass('on').appendTo(area); + // off span tag + let off = jQuery(document.createElement('span')).addClass('off').appendTo(area); + on.text(this.options.toggle_on); + off.text(this.options.toggle_off); + + // handle a tag + jQuery(document.createElement('a')).appendTo(area); + this.setDOMNode(this.toggle[0]); + } + else + { + this.setDOMNode(this.input[0]); + } + } + + /** + * Override default to place checkbox before label, if there is no %s in the label + * + * @param {string} label + */ + set_label(label) { + if(label.length && label.indexOf('%s') < 0) + { + label = '%s'+label; + } + super.set_label(label); + jQuery(this.getSurroundings().getWidgetSurroundings()).addClass('et2_checkbox_label'); + } + + /** + * Override default to match against set/unset value + * + * @param {string|boolean} _value + */ + set_value(_value : string | boolean) + { + // in php, our database storage and et2_checkType(): "0" == false + if (_value === "0" && this.options.selected_value != "0") + { + _value = false; + } + if(_value != this.value) { + if(_value == this.options.selected_value || + _value && this.options.selected_value == this.attributes["selected_value"]["default"] && + _value != this.options.unselected_value) { + if (this.options.toggle_on || this.options.toggle_off) this.toggle.addClass('switchOn'); + this.input.prop("checked", true); + } else { + this.input.prop("checked", false); + if (this.options.toggle_on || this.options.toggle_off) this.toggle.removeClass('switchOn'); + } + } + } + + /** + * Disable checkbox on runtime + * + * @param {boolean} _ro + */ + set_readonly(_ro) + { + jQuery(this.getDOMNode()).attr('disabled', _ro); + } + + /** + * Override default to return unchecked value + */ + getValue() + { + if(this.input.prop("checked")) { + if (this.options.toggle_on || this.options.toggle_off) this.toggle.addClass('switchOn'); + return this.options.selected_value; + } else { + if (this.options.toggle_on || this.options.toggle_off) this.toggle.removeClass('switchOn'); + return this.options.unselected_value; + } + } +} +et2_register_widget(et2_checkbox, ["checkbox"]); + +/** +* et2_checkbox_ro is the dummy readonly implementation of the checkbox +* @augments et2_checkbox +*/ +class et2_checkbox_ro extends et2_checkbox implements et2_IDetachedDOM +{ + /** + * Ignore unset value + */ + static readonly _attributes : any = { + "unselected_value": { + "ignore": true + } + }; + + span : JQuery = null; + + /** + * Constructor + * + * @memberOf et2_checkbox_ro + */ + constructor(_parent, _attrs? : WidgetConfig, _child? : object) + { + // Call the inherited constructor + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_checkbox_ro._attributes, _child || {})); + + this.value = ""; + this.span = jQuery(document.createElement("span")) + .addClass("et2_checkbox_ro"); + + this.setDOMNode(this.span[0]); + } + + /** + * note: checkbox is checked if even there is a value but not only if the _value is only "true" + * it's an exceptional validation for cases that we pass non boolean values as checkbox _value + * + * @param {string|boolean} _value + */ + set_value(_value) + { + if(_value == this.options.selected_value ||_value && this.options.selected_value == this.attributes["selected_value"]["default"] && + _value != this.options.unselected_value) { + this.span.text(this.options.ro_true); + this.value = _value; + } else { + this.span.text(this.options.ro_false); + } + } + + /** + * Code for implementing et2_IDetachedDOM + * + * @param {array} _attrs + */ + getDetachedAttributes(_attrs) + { + _attrs.push("value", "class"); + } + + getDetachedNodes() + { + return [this.span[0]]; + } + + setDetachedAttributes(_nodes, _values) + { + // Update the properties + if (typeof _values["value"] != "undefined") + { + this.span = jQuery(_nodes[0]); + this.set_value(_values["value"]); + } + + if (typeof _values["class"] != "undefined") + { + _nodes[0].setAttribute("class", _values["class"]); + } + } +} +et2_register_widget(et2_checkbox_ro, ["checkbox_ro"]); \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_color.js b/api/js/etemplate/et2_widget_color.js index d33448217b..193d07e303 100644 --- a/api/js/etemplate/et2_widget_color.js +++ b/api/js/etemplate/et2_widget_color.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - JS Color picker object * @@ -9,109 +10,111 @@ * @copyright Nathan Gray 2012 * @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 - /vendor/bower-asset/jquery/dist/jquery.js; - et2_core_inputWidget; - et2_core_valueWidget; + /vendor/bower-asset/jquery/dist/jquery.js; + et2_core_inputWidget; + et2_core_valueWidget; */ - +var et2_core_widget_1 = require("./et2_core_widget"); +var et2_core_valueWidget_1 = require("./et2_core_valueWidget"); +var et2_core_inputWidget_1 = require("./et2_core_inputWidget"); +var et2_core_inheritance_1 = require("./et2_core_inheritance"); /** * Class which implements the "colorpicker" XET-Tag * - * @augments et2_inputWidget */ -var et2_color = (function(){ "use strict"; return et2_inputWidget.extend( -{ - attributes: { - }, - - /** - * Constructor - * - * @memberOf et2_color - */ - init: function() { - this._super.apply(this, arguments); - - // included via etemplate2.css - //this.egw().includeCSS("phpgwapi/js/jquery/jpicker/css/jPicker-1.1.6.min.css"); - this.input = this.$node = jQuery(""); - - this.setDOMNode(this.$node[0]); - this.set_value(this.options.value); - }, - - getValue: function() { - var value = this.$node.val(); - if(value === '#FFFFFF' || value === '#ffffff') - { - return ''; - } - return value; - }, - - set_value: function(color) { - if(!color) - { - color = '#ffffff'; - } - this.$node.val(color); - } -});}).call(this); -et2_register_widget(et2_color, ["colorpicker"]); - +var et2_color = /** @class */ (function (_super) { + __extends(et2_color, _super); + /** + * Constructor + */ + function et2_color(_parent, _attrs, _child) { + var _this = + // Call the inherited constructor + _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_color._attributes, _child || {})) || this; + // included via etemplate2.css + //this.egw().includeCSS("phpgwapi/js/jquery/jpicker/css/jPicker-1.1.6.min.css"); + _this.input = jQuery(""); + _this.setDOMNode(_this.input[0]); + return _this; + } + et2_color.prototype.getValue = function () { + var value = this.input.val(); + if (value === '#FFFFFF' || value === '#ffffff') { + return ''; + } + return value; + }; + et2_color.prototype.set_value = function (color) { + if (!color) { + color = '#ffffff'; + } + this.input.val(color); + }; + return et2_color; +}(et2_core_inputWidget_1.et2_inputWidget)); +exports.et2_color = et2_color; +et2_core_widget_1.et2_register_widget(et2_color, ["colorpicker"]); /** * et2_textbox_ro is the dummy readonly implementation of the textbox. * @augments et2_valueWidget */ -var et2_color_ro = (function(){ "use strict"; return et2_valueWidget.extend([et2_IDetachedDOM], -{ - /** - * Constructor - * - * @memberOf et2_color_ro - */ - init: function() { - this._super.apply(this, arguments); - - this.value = ""; - this.$node = jQuery(document.createElement("div")) - .addClass("et2_color"); - - this.setDOMNode(this.$node[0]); - }, - - set_value: function(_value) { - this.value = _value; - - if(!_value) _value = "inherit"; - this.$node.css("background-color", _value); - }, - /** - * Code for implementing et2_IDetachedDOM - * - * @param {array} _attrs array to add further attributes to - */ - getDetachedAttributes: function(_attrs) - { - _attrs.push("value"); - }, - - getDetachedNodes: function() - { - return [this.node]; - }, - - setDetachedAttributes: function(_nodes, _values) - { - this.$node = jQuery(_nodes[0]); - if(typeof _values["value"] != 'undefined') - { - this.set_value(_values["value"]); - } - } -});}).call(this); - -et2_register_widget(et2_color_ro, ["colorpicker_ro"]); - +var et2_color_ro = /** @class */ (function (_super) { + __extends(et2_color_ro, _super); + /** + * Constructor + * + * @memberOf et2_color_ro + */ + function et2_color_ro(_parent, _attrs, _child) { + var _this = + // Call the inherited constructor + _super.call(this, _parent, _attrs, _child || {}) || this; + _this.value = ""; + _this.$node = jQuery(document.createElement("div")) + .addClass("et2_color"); + _this.setDOMNode(_this.$node[0]); + return _this; + } + et2_color_ro.prototype.set_value = function (_value) { + this.value = _value; + if (!_value) + _value = "inherit"; + this.$node.css("background-color", _value); + }; + /** + * Code for implementing et2_IDetachedDOM + * + * @param {array} _attrs array to add further attributes to + */ + et2_color_ro.prototype.getDetachedAttributes = function (_attrs) { + _attrs.push("value"); + }; + et2_color_ro.prototype.getDetachedNodes = function () { + return [this.node]; + }; + et2_color_ro.prototype.setDetachedAttributes = function (_nodes, _values) { + this.$node = jQuery(_nodes[0]); + if (typeof _values["value"] != 'undefined') { + this.set_value(_values["value"]); + } + }; + return et2_color_ro; +}(et2_core_valueWidget_1.et2_valueWidget)); +exports.et2_color_ro = et2_color_ro; +et2_core_widget_1.et2_register_widget(et2_color_ro, ["colorpicker_ro"]); +//# sourceMappingURL=et2_widget_color.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_color.ts b/api/js/etemplate/et2_widget_color.ts new file mode 100644 index 0000000000..70df271e0c --- /dev/null +++ b/api/js/etemplate/et2_widget_color.ts @@ -0,0 +1,123 @@ +/** + * EGroupware eTemplate2 - JS Color picker object + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage api + * @link http://www.egroupware.org + * @author Nathan Gray + * @copyright Nathan Gray 2012 + * @version $Id$ + */ + +/*egw:uses + /vendor/bower-asset/jquery/dist/jquery.js; + et2_core_inputWidget; + et2_core_valueWidget; +*/ + +import {et2_register_widget, WidgetConfig} from "./et2_core_widget"; +import {et2_valueWidget} from "./et2_core_valueWidget"; +import {et2_inputWidget} from "./et2_core_inputWidget"; +import {ClassWithAttributes} from "./et2_core_inheritance"; + +/** + * Class which implements the "colorpicker" XET-Tag + * + */ +export class et2_color extends et2_inputWidget +{ + private input: JQuery; + /** + * Constructor + */ + constructor(_parent, _attrs? : WidgetConfig, _child? : object) + { + // Call the inherited constructor + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_color._attributes, _child || {})); + // included via etemplate2.css + //this.egw().includeCSS("phpgwapi/js/jquery/jpicker/css/jPicker-1.1.6.min.css"); + this.input = jQuery(""); + + this.setDOMNode(this.input[0]); + } + + getValue( ) + { + var value = this.input.val(); + if(value === '#FFFFFF' || value === '#ffffff') + { + return ''; + } + return value; + } + + set_value( color) + { + if(!color) + { + color = '#ffffff'; + } + this.input.val(color); + } +} +et2_register_widget(et2_color, ["colorpicker"]); + +/** + * et2_textbox_ro is the dummy readonly implementation of the textbox. + * @augments et2_valueWidget + */ +export class et2_color_ro extends et2_valueWidget implements et2_IDetachedDOM +{ + private value: string; + private $node: JQuery; + /** + * Constructor + * + * @memberOf et2_color_ro + */ + constructor(_parent, _attrs? : WidgetConfig, _child? : object) + { + // Call the inherited constructor + super(_parent, _attrs, _child || {}); + + this.value = ""; + this.$node = jQuery(document.createElement("div")) + .addClass("et2_color"); + + this.setDOMNode(this.$node[0]); + } + + set_value( _value) + { + this.value = _value; + + if(!_value) _value = "inherit"; + this.$node.css("background-color", _value); + } + /** + * Code for implementing et2_IDetachedDOM + * + * @param {array} _attrs array to add further attributes to + */ + getDetachedAttributes(_attrs) + { + _attrs.push("value"); + } + + getDetachedNodes() + { + return [this.node]; + } + + setDetachedAttributes(_nodes, _values) + { + this.$node = jQuery(_nodes[0]); + if(typeof _values["value"] != 'undefined') + { + this.set_value(_values["value"]); + } + } +} +et2_register_widget(et2_color_ro, ["colorpicker_ro"]); + diff --git a/api/js/etemplate/et2_widget_date.js b/api/js/etemplate/et2_widget_date.js index 9ae1a00fcc..f60caf6528 100644 --- a/api/js/etemplate/et2_widget_date.js +++ b/api/js/etemplate/et2_widget_date.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - JS Date object * @@ -7,17 +8,37 @@ * @link http://www.egroupware.org * @author Nathan Gray * @copyright Nathan Gray 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 - /vendor/bower-asset/jquery/dist/jquery.js; - /vendor/bower-asset/jquery-ui/jquery-ui.js; - lib/date; - et2_core_inputWidget; - et2_core_valueWidget; + /vendor/bower-asset/jquery/dist/jquery.js; + /vendor/bower-asset/jquery-ui/jquery-ui.js; + lib/date; + et2_core_inputWidget; + et2_core_valueWidget; */ - +require("./et2_core_common"); +var et2_core_inheritance_1 = require("./et2_core_inheritance"); +var et2_core_widget_1 = require("./et2_core_widget"); +var et2_core_valueWidget_1 = require("./et2_core_valueWidget"); +var et2_core_inputWidget_1 = require("./et2_core_inputWidget"); +require("./et2_types"); +var et2_core_DOMWidget_1 = require("./et2_core_DOMWidget"); +// all calls to jQueryUI.datetimepicker as jQuery.datepicker give errors which are currently suppressed with @ts-ignore +// adding npm package @types/jquery.ui.datetimepicker did NOT help :( /** * Class which implements the "date" XET-Tag * @@ -26,1519 +47,1344 @@ * * Widgets uses jQuery date- and time-picker for desktop browsers and * HTML5 input fields for mobile devices to get their native UI for date/time entry. - * - * @augments et2_inputWidget */ -var et2_date = (function(){ "use strict"; return et2_inputWidget.extend( -{ - attributes: { - "value": { - "type": "any" - }, - "type": { - "ignore": false - }, - "blur": { - "name": "Placeholder", - "type": "string", - "default": "", - "description": "This text get displayed if an input-field is empty and does not have the input-focus (blur). It can be used to show a default value or a kind of help-text." - }, - "data_format": { - "ignore": true, - "description": "Date/Time format. Can be set as an options to date widget", - "default": '' - }, - year_range: { - name: "Year range", - type: "string", - default: "c-10:c+10", - description: "The range of years displayed in the year drop-down: either relative to today's year (\"-nn:+nn\"), relative to the currently selected year (\"c-nn:c+nn\"), absolute (\"nnnn:nnnn\"), or combinations of these formats (\"nnnn:-nn\"). Note that this option only affects what appears in the drop-down, to restrict which dates may be selected use the min and/or max options." - }, - min: { - "name": "Minimum", - "type": "any", - "default": et2_no_init, - "description": 'Minimum allowed date. Multiple types supported:\ +var et2_date = /** @class */ (function (_super) { + __extends(et2_date, _super); + /** + * Constructor + */ + function et2_date(_parent, _attrs, _child) { + var _this = + // Call the inherited constructor + _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_date._attributes, _child || {})) || this; + _this.legacyOptions = ["data_format"]; + _this.input_date = null; + _this.is_mobile = false; + _this.date = new Date(); + _this.date.setUTCHours(0); + _this.date.setMinutes(0); + _this.date.setSeconds(0); + _this.createInputWidget(); + return _this; + } + et2_date.prototype.createInputWidget = function () { + this.span = jQuery(document.createElement(this.options.inline ? 'div' : "span")).addClass("et2_date"); + this.input_date = jQuery(document.createElement(this.options.inline ? "div" : "input")); + if (this.options.blur) + this.input_date.attr('placeholder', this.egw().lang(this.options.blur)); + this.input_date.addClass("et2_date").attr("type", "text") + .attr("size", 7) // strlen("10:00pm")=7 + .appendTo(this.span); + this.setDOMNode(this.span[0]); + // inline calendar is not existing in html5, so allways use datepicker instead + this.is_mobile = egwIsMobile() && !this.options.inline; + if (this.is_mobile) { + this.dateFormat = 'yy-mm-dd'; + this.timeFormat = 'HH:mm'; + switch (this.getType()) { + case 'date': + this.input_date.attr('type', 'date'); + break; + case 'date-time': + this.input_date.attr('type', 'datetime-local'); + break; + case 'date-timeonly': + this.input_date.addClass("et2_time"); + this.input_date.attr('type', 'time'); + break; + } + } + else { + this.dateFormat = this.egw().dateTimeFormat(this.egw().preference("dateformat")); + this.timeFormat = this.egw().preference("timeformat") == 12 ? "h:mmtt" : "HH:mm"; + // jQuery-UI date picker + if (this.getType() != 'date-timeonly') { + this.egw().calendar(this.input_date, this.getType() == "date-time"); + } + else { + this.input_date.addClass("et2_time"); + this.egw().time(this.input_date); + } + // Avoid collision of datepicker dialog with input field + var widget = this; + this.input_date.datepicker('option', 'beforeShow', function (input, inst) { + var cal = inst.dpDiv; + setTimeout(function () { + var $input = jQuery(input); + var inputOffset = $input.offset(); + // position the datepicker in freespace zone + // avoid datepicker calendar collision with input field + if (cal.height() + inputOffset.top > window.innerHeight) { + cal.position({ + my: "left center", + at: 'right bottom', + collision: 'flip fit', + of: input + }); + } + // Add tooltip to Today/Now button + jQuery('[data-handler="today"]', cal).attr('title', widget.getType() == 'date' ? egw.lang('Today') : egw.lang('Now')); + }, 0); + }) + .datepicker('option', 'onClose', function (dateText, inst) { + // Lose focus, avoids an issue with focus + // not allowing datepicker to re-open + inst.input.blur(); + }); + } + // Update internal value when changed + var self = this; + this.input_date.bind('change', function (e) { + self.set_value(this.value); + return false; + }); + // Framewok skips nulls, but null needs to be processed here + if (this.options.value == null) { + this.set_value(null); + } + }; + et2_date.prototype.set_type = function (_type) { + if (_type != this.getType()) { + _super.prototype.setType.call(this, _type); + this.createInputWidget(); + } + }; + /** + * Dynamic disable or enable datepicker + * + * @param {boolean} _ro + */ + et2_date.prototype.set_readonly = function (_ro) { + if (this.input_date && !this.input_date.attr('disabled') != !_ro) { + this.input_date.prop('disabled', !!_ro) + .datepicker('option', 'disabled', !!_ro); + } + }; + /** + * Set (full) year of current date + * + * @param {number} _value 4-digit year + */ + et2_date.prototype.set_year = function (_value) { + this.date.setUTCFullYear(_value); + this.set_value(this.date); + }; + /** + * Set month (1..12) of current date + * + * @param {number} _value 1..12 + */ + et2_date.prototype.set_month = function (_value) { + this.date.setUTCMonth(_value - 1); + this.set_value(this.date); + }; + /** + * Set day of current date + * + * @param {number} _value 1..31 + */ + et2_date.prototype.set_date = function (_value) { + this.date.setUTCDate(_value); + this.set_value(this.date); + }; + /** + * Set hour (0..23) of current date + * + * @param {number} _value 0..23 + */ + et2_date.prototype.set_hours = function (_value) { + this.date.setUTCHours(_value); + this.set_value(this.date); + }; + /** + * Set minute (0..59) of current date + * + * @param {number} _value 0..59 + */ + et2_date.prototype.set_minutes = function (_value) { + this.date.setUTCMinutes(_value); + this.set_value(this.date); + }; + /** + * Get (full) year of current date + * + * @return {number|null} 4-digit year or null for empty + */ + et2_date.prototype.get_year = function () { + return this.input_date.val() == "" ? null : this.date.getUTCFullYear(); + }; + /** + * Get month (1..12) of current date + * + * @return {number|null} 1..12 or null for empty + */ + et2_date.prototype.get_month = function () { + return this.input_date.val() == "" ? null : this.date.getUTCMonth() + 1; + }; + /** + * Get day of current date + * + * @return {number|null} 1..31 or null for empty + */ + et2_date.prototype.get_date = function () { + return this.input_date.val() == "" ? null : this.date.getUTCDate(); + }; + /** + * Get hour (0..23) of current date + * + * @return {number|null} 0..23 or null for empty + */ + et2_date.prototype.get_hours = function () { + return this.input_date.val() == "" ? null : this.date.getUTCHours(); + }; + /** + * Get minute (0..59) of current date + * + * @return {number|null} 0..59 or null for empty + */ + et2_date.prototype.get_minutes = function () { + return this.input_date.val() == "" ? null : this.date.getUTCMinutes(); + }; + /** + * Get timestamp + * + * You can use set_value to set a timestamp. + * + * @return {number|null} timestamp (seconds since 1970-01-01) + */ + et2_date.prototype.get_time = function () { + return this.input_date.val() == "" ? null : this.date.getTime(); + }; + /** + * The range of years displayed in the year drop-down: either relative + * to today's year ("-nn:+nn"), relative to the currently selected year + * ("c-nn:c+nn"), absolute ("nnnn:nnnn"), or combinations of these formats + * ("nnnn:-nn"). Note that this option only affects what appears in the + * drop-down, to restrict which dates may be selected use the min_date + * and/or max_date options. + * @param {string} _value + */ + et2_date.prototype.set_year_range = function (_value) { + if (this.input_date && this.getType() == 'date' && !this.is_mobile) { + this.input_date.datepicker('option', 'yearRange', _value); + } + this.options.year_range = _value; + }; + /** + * Set the minimum allowed date + * + * The minimum selectable date. When set to null, there is no minimum. + * Multiple types supported: + * Date: A date object containing the minimum date. + * Number: A number of days from today. For example 2 represents two days + * from today and -1 represents yesterday. + * String: A string in the format defined by the dateFormat option, or a + * relative date. Relative dates must contain value and period pairs; + * valid periods are "y" for years, "m" for months, "w" for weeks, and + * "d" for days. For example, "+1m +7d" represents one month and seven + * days from today. + * @param {Date|Number|String} _value + */ + et2_date.prototype.set_min = function (_value) { + if (this.input_date) { + if (this.is_mobile) { + this.input_date.attr('min', this._relativeDate(_value)); + } + else { + // Check for full timestamp + if (typeof _value == 'string' && _value.match(/(\d{4})-(\d{2})-(\d{2})T(\d{2})\:(\d{2})\:(\d{2})(?:\.\d{3})?(?:Z|[+-](\d{2})\:(\d{2}))/)) { + _value = new Date(_value); + // Add timezone offset back in, or formatDate will lose those hours + var formatDate = new Date(_value.valueOf() + this.date.getTimezoneOffset() * 60 * 1000); + if (this.getType() == 'date') { + _value = jQuery.datepicker.formatDate(this.dateFormat, formatDate); + } + } + this.input_date.datepicker('option', 'minDate', _value); + } + } + this.options.min = _value; + }; + /** + * Convert non html5 min or max attributes described above to timestamps + * + * @param {string|Date} _value + */ + et2_date.prototype._relativeDate = function (_value) { + if (typeof _value == 'string' && _value.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/)) + return _value; + // @ts-ignore + return jQuery.datepicker._determineDate(jQuery.datepicker, _value, this.date).toJSON(); + }; + /** + * Set the maximum allowed date + * + * The maximum selectable date. When set to null, there is no maximum. + * Multiple types supported: + * Date: A date object containing the maximum date. + * Number: A number of days from today. For example 2 represents two days + * from today and -1 represents yesterday. + * String: A string in the format defined by the dateFormat option, or a + * relative date. Relative dates must contain value and period pairs; + * valid periods are "y" for years, "m" for months, "w" for weeks, and + * "d" for days. For example, "+1m +7d" represents one month and seven + * days from today. + * @param {Date|Number|String} _value + */ + et2_date.prototype.set_max = function (_value) { + if (this.input_date) { + if (this.is_mobile) { + this.input_date.attr('max', this._relativeDate(_value)); + } + else { + // Check for full timestamp + if (typeof _value == 'string' && _value.match(/(\d{4})-(\d{2})-(\d{2})T(\d{2})\:(\d{2})\:(\d{2})(?:\.\d{3})?(?:Z|[+-](\d{2})\:(\d{2}))/)) { + _value = new Date(_value); + // Add timezone offset back in, or formatDate will lose those hours + var formatDate = new Date(_value.valueOf() + this.date.getTimezoneOffset() * 60 * 1000); + if (this.getType() == 'date') { + _value = jQuery.datepicker.formatDate(this.dateFormat, formatDate); + } + } + this.input_date.datepicker('option', 'maxDate', _value); + } + } + this.options.max = _value; + }; + /** + * Setting date + * + * @param {string|number|Date} _value supported are the following formats: + * - Date object with usertime as UTC value + * - string like Date.toJSON() + * - string or number with timestamp in usertime like server-side uses it + * - string starting with + or - to add/substract given number of seconds from current value, "+600" to add 10 minutes + */ + et2_date.prototype.set_value = function (_value) { + var old_value = this._oldValue; + if (_value === null || _value === "" || _value === undefined || + // allow 0 as empty-value for date and date-time widgets, as that is used a lot eg. in InfoLog + _value == 0 && (this.getType() == 'date-time' || this.getType() == 'date')) { + if (this.input_date) { + this.input_date.val(""); + } + if (this._oldValue !== et2_no_init && old_value !== _value) { + this.change(this.input_date); + } + this._oldValue = _value; + return; + } + // timestamp in usertime, convert to 'Y-m-d\\TH:i:s\\Z', as we do on server-side with equivalent of PHP date() + if (typeof _value == 'number' || typeof _value == 'string' && !isNaN(_value) && _value[0] != '+' && _value[0] != '-') { + _value = date('Y-m-d\\TH:i:s\\Z', _value); + } + // Check for full timestamp + if (typeof _value == 'string' && _value.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2})\:(\d{2})\:(\d{2})(?:\.\d{3})?(?:Z|[+-](\d{2})\:(\d{2})|)$/)) { + _value = new Date(_value); + } + // Handle just time as a string in the form H:i + if (typeof _value == 'string' && isNaN(_value)) { + try { + // silently fix skiped minutes or times with just one digit, as parser is quite pedantic ;-) + var fix_reg = new RegExp((this.getType() == "date-timeonly" ? '^' : ' ') + '([0-9]+)(:[0-9]*)?( ?(a|p)m?)?$', 'i'); + var matches = _value.match(fix_reg); + if (matches && (matches[1].length < 2 || matches[2] === undefined || matches[2].length < 3 || + matches[3] && matches[3] != 'am' && matches[3] != 'pm')) { + if (matches[1].length < 2 && !matches[3]) + matches[1] = '0' + matches[1]; + if (matches[2] === undefined) + matches[2] = ':00'; + while (matches[2].length < 3) + matches[2] = ':0' + matches[2].substr(1); + _value = _value.replace(fix_reg, (this.getType() == "date-timeonly" ? '' : ' ') + matches[1] + matches[2] + matches[3]); + if (matches[4] !== undefined) + matches[3] = matches[4].toLowerCase() == 'a' ? 'am' : 'pm'; + } + switch (this.getType()) { + case "date-timeonly": + // @ts-ignore + var parsed = jQuery.datepicker.parseTime(this.timeFormat, _value); + if (!parsed) // parseTime returns false + { + this.set_validation_error(this.egw().lang("'%1' has an invalid format !!!", _value)); + return; + } + this.set_validation_error(false); + // this.date is on current date, changing it in get_value() to 1970-01-01, gives a time-difference, if we are currently on DST + this.date.setDate(1); + this.date.setMonth(0); + this.date.setFullYear(1970); + // Avoid javascript timezone offset, hour is in 'user time' + this.date.setUTCHours(parsed.hour); + this.date.setMinutes(parsed.minute); + if (this.input_date.val() != _value) { + this.input_date.val(_value); + // @ts-ignore + this.input_date.timepicker('setTime', _value); + if (this._oldValue !== et2_no_init) { + this.change(this.input_date); + } + } + this._oldValue = this.date.toJSON(); + return; + default: + // Parse customfields's date with storage data_format to date object + // Or generally any date widgets with fixed date/time format + if (this.id.match(/^#/g) && this.options.value == _value || (this.options.data_format && this.options.value == _value)) { + switch (this.getType()) { + case 'date': + var parsed = jQuery.datepicker.parseDate(this.egw().dateTimeFormat(this.options.data_format), _value); + break; + case 'date-time': + var DTformat = this.options.data_format.split(' '); + // @ts-ignore + var parsed = jQuery.datepicker.parseDateTime(this.egw().dateTimeFormat(DTformat[0]), this.egw().dateTimeFormat(DTformat[1]), _value); + } + } + else // Parse other date widgets date with timepicker date/time format to date onject + { + // @ts-ignore + var parsed = jQuery.datepicker.parseDateTime(this.dateFormat, this.timeFormat, _value.replace('T', ' ')); + if (!parsed) { + this.set_validation_error(this.egw().lang("%1' han an invalid format !!!", _value)); + return; + } + } + // Update local variable, but remove the timezone offset that + // javascript adds when we parse + if (parsed) { + this.date = new Date(parsed.valueOf() - parsed.getTimezoneOffset() * 60000); + } + this.set_validation_error(false); + } + } + // catch exception from unparsable date and display it empty instead + catch (e) { + return this.set_value(null); + } + } + else if (typeof _value == 'object' && _value.date) { + this.date = _value.date; + } + else if (typeof _value == 'object' && _value.valueOf) { + this.date = _value; + } + else + // string starting with + or - --> add/substract number of seconds from current value + { + this.date.setTime(this.date.getTime() + 1000 * parseInt(_value)); + } + // Update input - popups do, but framework doesn't + _value = ''; + // Add timezone offset back in, or formatDate will lose those hours + var formatDate = new Date(this.date.valueOf() + this.date.getTimezoneOffset() * 60 * 1000); + if (this.getType() != 'date-timeonly') { + _value = jQuery.datepicker.formatDate(this.dateFormat, formatDate); + } + if (this.getType() != 'date') { + if (this.getType() != 'date-timeonly') + _value += this.is_mobile ? 'T' : ' '; + // @ts-ignore + _value += jQuery.datepicker.formatTime(this.timeFormat, { + hour: formatDate.getHours(), + minute: formatDate.getMinutes(), + seconds: 0, + timezone: 0 + }); + } + if (this.options.inline) { + this.input_date.datepicker("setDate", formatDate); + } + else { + this.input_date.val(_value); + } + if (this._oldValue !== et2_no_init && old_value != this.getValue()) { + this.change(this.input_date); + } + this._oldValue = _value; + }; + et2_date.prototype.getValue = function () { + if (this.input_date.val() == "") { + // User blanked the box + return null; + } + // date-timeonly returns just the seconds, without any date! + if (this.getType() == 'date-timeonly') { + this.date.setUTCDate(1); + this.date.setUTCMonth(0); + this.date.setUTCFullYear(1970); + } + else if (this.getType() == 'date') { + this.date.setUTCHours(0); + this.date.setUTCMinutes(0); + } + // Convert to timestamp - no seconds + this.date.setSeconds(0, 0); + return (this.date && typeof this.date.toJSON != 'undefined' && this.date.toJSON()) ? this.date.toJSON().replace(/\.\d{3}Z$/, 'Z') : this.date; + }; + et2_date._attributes = { + "value": { + "type": "any" + }, + "type": { + "ignore": false + }, + "blur": { + "name": "Placeholder", + "type": "string", + "default": "", + "description": "This text get displayed if an input-field is empty and does not have the input-focus (blur). It can be used to show a default value or a kind of help-text." + }, + "data_format": { + "ignore": true, + "description": "Date/Time format. Can be set as an options to date widget", + "default": '' + }, + year_range: { + name: "Year range", + type: "string", + default: "c-10:c+10", + description: "The range of years displayed in the year drop-down: either relative to today's year (\"-nn:+nn\"), relative to the currently selected year (\"c-nn:c+nn\"), absolute (\"nnnn:nnnn\"), or combinations of these formats (\"nnnn:-nn\"). Note that this option only affects what appears in the drop-down, to restrict which dates may be selected use the min and/or max options." + }, + min: { + "name": "Minimum", + "type": "any", + "default": et2_no_init, + "description": 'Minimum allowed date. Multiple types supported:\ Date: A date object containing the minimum date.\ Number: A number of days from today. For example 2 represents two days from today and -1 represents yesterday.\ String: A string in the user\'s date format, or a relative date. Relative dates must contain value and period pairs; valid periods are "y" for years, "m" for months, "w" for weeks, and "d" for days. For example, "+1m +7d" represents one month and seven days from today.' - }, - max: { - "name": "Maximum", - "type": "any", - "default": et2_no_init, - "description": 'Maximum allowed date. Multiple types supported:\ + }, + max: { + "name": "Maximum", + "type": "any", + "default": et2_no_init, + "description": 'Maximum allowed date. Multiple types supported:\ Date: A date object containing the maximum date.\ Number: A number of days from today. For example 2 represents two days from today and -1 represents yesterday.\ String: A string in the user\'s date format, or a relative date. Relative dates must contain value and period pairs; valid periods are "y" for years, "m" for months, "w" for weeks, and "d" for days. For example, "+1m +7d" represents one month and seven days from today.' - }, - inline: { - "name": "Inline", - "type": "boolean", - "default": false, - "description": "Instead of an input field with a popup calendar, the calendar is displayed inline, with no input field" - } - }, - - legacyOptions: ["data_format"], - - /** - * Constructor - * - * @memberOf et2_date - */ - init: function() - { - this._super.apply(this, arguments); - - this.date = new Date(); - this.date.setUTCHours(0); - this.date.setMinutes(0); - this.date.setSeconds(0); - this.input = null; - - this.createInputWidget(); - }, - - createInputWidget: function() - { - this.span = jQuery(document.createElement(this.options.inline ? 'div' : "span")).addClass("et2_date"); - - this.input_date = jQuery(document.createElement(this.options.inline ? "div" : "input")); - if (this.options.blur) this.input_date.attr('placeholder', this.egw().lang(this.options.blur)); - this.input_date.addClass("et2_date").attr("type", "text") - .attr("size", 7) // strlen("10:00pm")=7 - .appendTo(this.span); - - this.setDOMNode(this.span[0]); - - // inline calendar is not existing in html5, so allways use datepicker instead - this.is_mobile = egwIsMobile() && !this.options.inline; - - if (this.is_mobile) - { - this.dateFormat = 'yy-mm-dd'; - this.timeFormat = 'HH:mm'; - switch(this._type) - { - case 'date': - this.input_date.attr('type', 'date'); - break; - case 'date-time': - this.input_date.attr('type', 'datetime-local'); - break; - case 'date-timeonly': - this.input_date.addClass("et2_time"); - this.input_date.attr('type', 'time'); - break; - } - } - else - { - this.dateFormat = this.egw().dateTimeFormat(this.egw().preference("dateformat")); - this.timeFormat = this.egw().preference("timeformat") == 12 ? "h:mmtt" : "HH:mm"; - // jQuery-UI date picker - if(this._type != 'date-timeonly') - { - this.egw().calendar(this.input_date, this._type == "date-time"); - } - else - { - this.input_date.addClass("et2_time"); - this.egw().time(this.input_date); - } - - // Avoid collision of datepicker dialog with input field - var widget = this; - this.input_date.datepicker('option', 'beforeShow', function(input, inst){ - var cal = inst.dpDiv; - setTimeout(function () { - var $input = jQuery(input); - var inputOffset = $input.offset(); - // position the datepicker in freespace zone - // avoid datepicker calendar collision with input field - if (cal.height() + inputOffset.top > window.innerHeight) - { - cal.position({ - my: "left center", - at: 'right bottom', - collision: 'flip fit', - of: input - }); - } - // Add tooltip to Today/Now button - jQuery('[data-handler="today"]',cal).attr('title', - widget._type == 'date' ? egw.lang('Today') : egw.lang('Now') - ); - - },0); - }) - .datepicker('option','onClose', function(dateText, inst) { - // Lose focus, avoids an issue with focus - // not allowing datepicker to re-open - inst.input.blur(); - }); - } - - // Update internal value when changed - var self = this; - this.input_date.bind('change', function(e){ - self.set_value(this.value); - return false; - }); - - // Framewok skips nulls, but null needs to be processed here - if(this.options.value == null) - { - this.set_value(null); - } - }, - - set_type: function(_type) { - if(_type != this._type) - { - this._type = _type; - this.createInputWidget(); - } - }, - - /** - * Dynamic disable or enable datepicker - * - * @param {boolean} _ro - */ - set_readonly: function(_ro) - { - if (this.input_date && !this.input_date.attr('disabled') != !_ro) - { - this.input_date.attr('disabled', !!_ro) - .datepicker('option', 'disabled', !!_ro); - } - }, - - /** - * Set (full) year of current date - * - * @param {number} _value 4-digit year - */ - set_year: function(_value) - { - this.date.setUTCFullYear(_value); - this.set_value(this.date); - }, - /** - * Set month (1..12) of current date - * - * @param {number} _value 1..12 - */ - set_month: function(_value) - { - this.date.setUTCMonth(_value-1); - this.set_value(this.date); - }, - /** - * Set day of current date - * - * @param {number} _value 1..31 - */ - set_date: function(_value) - { - this.date.setUTCDate(_value); - this.set_value(this.date); - }, - /** - * Set hour (0..23) of current date - * - * @param {number} _value 0..23 - */ - set_hours: function(_value) - { - this.date.setUTCHours(_value); - this.set_value(this.date); - }, - /** - * Set minute (0..59) of current date - * - * @param {number} _value 0..59 - */ - set_minutes: function(_value) - { - this.date.setUTCMinutes(_value); - this.set_value(this.date); - }, - /** - * Get (full) year of current date - * - * @return {number|null} 4-digit year or null for empty - */ - get_year: function() - { - return this.input_date.val() == "" ? null : this.date.getUTCFullYear(); - }, - /** - * Get month (1..12) of current date - * - * @return {number|null} 1..12 or null for empty - */ - get_month: function() - { - return this.input_date.val() == "" ? null : this.date.getUTCMonth()+1; - }, - /** - * Get day of current date - * - * @return {number|null} 1..31 or null for empty - */ - get_date: function() - { - return this.input_date.val() == "" ? null : this.date.getUTCDate(); - }, - /** - * Get hour (0..23) of current date - * - * @return {number|null} 0..23 or null for empty - */ - get_hours: function() - { - return this.input_date.val() == "" ? null : this.date.getUTCHours(); - }, - /** - * Get minute (0..59) of current date - * - * @return {number|null} 0..59 or null for empty - */ - get_minutes: function() - { - return this.input_date.val() == "" ? null : this.date.getUTCMinutes(); - }, - /** - * Get timestamp - * - * You can use set_value to set a timestamp. - * - * @return {number|null} timestamp (seconds since 1970-01-01) - */ - get_time: function() - { - return this.input_date.val() == "" ? null : this.date.getTime(); - }, - - /** - * The range of years displayed in the year drop-down: either relative - * to today's year ("-nn:+nn"), relative to the currently selected year - * ("c-nn:c+nn"), absolute ("nnnn:nnnn"), or combinations of these formats - * ("nnnn:-nn"). Note that this option only affects what appears in the - * drop-down, to restrict which dates may be selected use the min_date - * and/or max_date options. - * @param {string} _value - */ - set_year_range: function(_value) - { - if(this.input_date && this._type == 'date' && !this.is_mobile) - { - this.input_date.datepicker('option','yearRange',_value); - } - this.options.year_range = _value; - }, - - /** - * Set the minimum allowed date - * - * The minimum selectable date. When set to null, there is no minimum. - * Multiple types supported: - * Date: A date object containing the minimum date. - * Number: A number of days from today. For example 2 represents two days - * from today and -1 represents yesterday. - * String: A string in the format defined by the dateFormat option, or a - * relative date. Relative dates must contain value and period pairs; - * valid periods are "y" for years, "m" for months, "w" for weeks, and - * "d" for days. For example, "+1m +7d" represents one month and seven - * days from today. - * @param {Date|Number|String} _value - */ - set_min: function(_value) - { - if(this.input_date) - { - if (this.is_mobile) - { - this.input_date.attr('min', this._relativeDate(_value)); - } - else - { - // Check for full timestamp - if(typeof _value == 'string' && _value.match(/(\d{4})-(\d{2})-(\d{2})T(\d{2})\:(\d{2})\:(\d{2})(?:\.\d{3})?(?:Z|[+-](\d{2})\:(\d{2}))/)) - { - _value = new Date(_value); - // Add timezone offset back in, or formatDate will lose those hours - var formatDate = new Date(_value.valueOf() + this.date.getTimezoneOffset() * 60 * 1000); - if(this._type == 'date') - { - _value = jQuery.datepicker.formatDate(this.dateFormat, formatDate); - } - } - this.input_date.datepicker('option','minDate',_value); - } - } - this.options.min = _value; - }, - - /** - * Convert non html5 min or max attributes described above to timestamps - * - * @param {string|Date} _value - */ - _relativeDate: function(_value) - { - if (typeof _value == 'string' && _value.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/)) return _value; - - return jQuery.datepicker._determineDate(jQuery.datepicker, _value, this.date).toJSON(); - }, - - /** - * Set the maximum allowed date - * - * The maximum selectable date. When set to null, there is no maximum. - * Multiple types supported: - * Date: A date object containing the maximum date. - * Number: A number of days from today. For example 2 represents two days - * from today and -1 represents yesterday. - * String: A string in the format defined by the dateFormat option, or a - * relative date. Relative dates must contain value and period pairs; - * valid periods are "y" for years, "m" for months, "w" for weeks, and - * "d" for days. For example, "+1m +7d" represents one month and seven - * days from today. - * @param {Date|Number|String} _value - */ - set_max: function(_value) - { - if(this.input_date) - { - if (this.is_mobile) - { - this.input_date.attr('max', this._relativeDate(_value)); - } - else - { - // Check for full timestamp - if(typeof _value == 'string' && _value.match(/(\d{4})-(\d{2})-(\d{2})T(\d{2})\:(\d{2})\:(\d{2})(?:\.\d{3})?(?:Z|[+-](\d{2})\:(\d{2}))/)) - { - _value = new Date(_value); - // Add timezone offset back in, or formatDate will lose those hours - var formatDate = new Date(_value.valueOf() + this.date.getTimezoneOffset() * 60 * 1000); - if(this._type == 'date') - { - _value = jQuery.datepicker.formatDate(this.dateFormat, formatDate); - } - } - this.input_date.datepicker('option','maxDate',_value); - } - } - this.options.max = _value; - }, - - /** - * Setting date - * - * @param {string|number|Date} _value supported are the following formats: - * - Date object with usertime as UTC value - * - string like Date.toJSON() - * - string or number with timestamp in usertime like server-side uses it - * - string starting with + or - to add/substract given number of seconds from current value, "+600" to add 10 minutes - */ - set_value: function(_value) - { - var old_value = this._oldValue; - if(_value === null || _value === "" || _value === undefined || - // allow 0 as empty-value for date and date-time widgets, as that is used a lot eg. in InfoLog - _value == 0 && (this._type == 'date-time' || this._type == 'date')) - { - if(this.input_date) - { - this.input_date.val(""); - } - if(this._oldValue !== et2_no_init && old_value !== _value) - { - this.change(this.input_date); - } - this._oldValue = _value; - return; - } - - // timestamp in usertime, convert to 'Y-m-d\\TH:i:s\\Z', as we do on server-side with equivalent of PHP date() - if (typeof _value == 'number' || typeof _value == 'string' && !isNaN(_value) && _value[0] != '+' && _value[0] != '-') - { - _value = date('Y-m-d\\TH:i:s\\Z', _value); - } - // Check for full timestamp - if(typeof _value == 'string' && _value.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2})\:(\d{2})\:(\d{2})(?:\.\d{3})?(?:Z|[+-](\d{2})\:(\d{2})|)$/)) - { - _value = new Date(_value); - } - // Handle just time as a string in the form H:i - if(typeof _value == 'string' && isNaN(_value)) - { - try { - // silently fix skiped minutes or times with just one digit, as parser is quite pedantic ;-) - var fix_reg = new RegExp((this._type == "date-timeonly"?'^':' ')+'([0-9]+)(:[0-9]*)?( ?(a|p)m?)?$','i'); - var matches = _value.match(fix_reg); - if (matches && (matches[1].length < 2 || matches[2] === undefined || matches[2].length < 3 || - matches[3] && matches[3] != 'am' && matches[3] != 'pm')) - { - if (matches[1].length < 2 && !matches[3]) matches[1] = '0'+matches[1]; - if (matches[2] === undefined) matches[2] = ':00'; - while (matches[2].length < 3) matches[2] = ':0'+matches[2].substr(1); - _value = _value.replace(fix_reg, (this._type == "date-timeonly"?'':' ')+matches[1]+matches[2]+matches[3]); - if (matches[4] !== undefined) matches[3] = matches[4].toLowerCase() == 'a' ? 'am' : 'pm'; - } - switch(this._type) - { - case "date-timeonly": - var parsed = jQuery.datepicker.parseTime(this.timeFormat, _value); - if (!parsed) // parseTime returns false - { - this.set_validation_error(this.egw().lang("'%1' has an invalid format !!!",_value)); - return; - } - this.set_validation_error(false); - // this.date is on current date, changing it in get_value() to 1970-01-01, gives a time-difference, if we are currently on DST - this.date.setDate(1); - this.date.setMonth(0); - this.date.setFullYear(1970); - // Avoid javascript timezone offset, hour is in 'user time' - this.date.setUTCHours(parsed.hour); - this.date.setMinutes(parsed.minute); - if(this.input_date.val() != _value) - { - this.input_date.val(_value); - this.input_date.timepicker('setTime',_value); - if (this._oldValue !== et2_no_init) - { - this.change(this.input_date); - } - } - this._oldValue = this.date.toJSON(); - return; - default: - // Parse customfields's date with storage data_format to date object - // Or generally any date widgets with fixed date/time format - if (this.id.match(/^#/g) && this.options.value == _value || (this.options.data_format && this.options.value == _value)) - { - switch (this._type) - { - case 'date': - var parsed = jQuery.datepicker.parseDate(this.egw().dateTimeFormat(this.options.data_format), _value); - break; - case 'date-time': - var DTformat = this.options.data_format.split(' '); - var parsed = jQuery.datepicker.parseDateTime(this.egw().dateTimeFormat(DTformat[0]),this.egw().dateTimeFormat(DTformat[1]), _value); - } - } - else // Parse other date widgets date with timepicker date/time format to date onject - { - var parsed = jQuery.datepicker.parseDateTime(this.dateFormat, - this.timeFormat, _value.replace('T', ' ')); - if(!parsed) - { - this.set_validation_error(this.egw().lang("%1' han an invalid format !!!",_value)); - return; - } - } - // Update local variable, but remove the timezone offset that - // javascript adds when we parse - if(parsed) - { - this.date = new Date(parsed.valueOf() - parsed.getTimezoneOffset() * 60000); - } - - this.set_validation_error(false); - } - } - // catch exception from unparsable date and display it empty instead - catch(e) { - return this.set_value(null); - } - } else if (typeof _value == 'object' && _value.date) { - this.date = _value.date; - } else if (typeof _value == 'object' && _value.valueOf) { - this.date = _value; - } else - // string starting with + or - --> add/substract number of seconds from current value - { - this.date.setTime(this.date.getTime()+1000*parseInt(_value)); - } - - // Update input - popups do, but framework doesn't - _value = ''; - // Add timezone offset back in, or formatDate will lose those hours - var formatDate = new Date(this.date.valueOf() + this.date.getTimezoneOffset() * 60 * 1000); - if(this._type != 'date-timeonly') - { - _value = jQuery.datepicker.formatDate(this.dateFormat, formatDate); - } - if(this._type != 'date') - { - if(this._type != 'date-timeonly') _value += this.is_mobile ? 'T' : ' '; - - _value += jQuery.datepicker.formatTime(this.timeFormat, { - hour: formatDate.getHours(), - minute: formatDate.getMinutes(), - seconds: 0, - timezone: 0 - }); - } - if(this.options.inline ) - { - this.input_date.datepicker("setDate",formatDate); - } - else - { - this.input_date.val(_value); - } - if(this._oldValue !== et2_no_init && old_value != this.getValue()) - { - this.change(this.input_date); - } - this._oldValue = _value; - }, - - getValue: function() { - if(this.input_date.val() == "") - { - // User blanked the box - return null; - } - // date-timeonly returns just the seconds, without any date! - if (this._type == 'date-timeonly') - { - this.date.setUTCDate(1); - this.date.setUTCMonth(0); - this.date.setUTCFullYear(1970); - } - else if (this._type == 'date') - { - this.date.setUTCHours(0); - this.date.setUTCMinutes(0); - } - - // Convert to timestamp - no seconds - this.date.setSeconds(0,0); - return (this.date && typeof this.date.toJSON != 'undefined' && this.date.toJSON())?this.date.toJSON().replace(/\.\d{3}Z$/, 'Z'):this.date; - } -});}).call(this); -et2_register_widget(et2_date, ["date", "date-time", "date-timeonly"]); - + }, + inline: { + "name": "Inline", + "type": "boolean", + "default": false, + "description": "Instead of an input field with a popup calendar, the calendar is displayed inline, with no input field" + } + }; + return et2_date; +}(et2_core_inputWidget_1.et2_inputWidget)); +exports.et2_date = et2_date; +et2_core_widget_1.et2_register_widget(et2_date, ["date", "date-time", "date-timeonly"]); /** - * @augments et2_date + * Class which implements the "date-duration" XET-Tag */ -var et2_date_duration = (function(){ "use strict"; return et2_date.extend( -{ - attributes: { - "data_format": { - "name": "Data format", - "default": "m", - "type": "string", - "description": "Units to read/store the data. 'd' = days (float), 'h' = hours (float), 'm' = minutes (int)." - }, - "display_format": { - "name": "Display format", - "default": "dhm", - "type": "string", - "description": "Permitted units for displaying the data. 'd' = days, 'h' = hours, 'm' = minutes. Use combinations to give a choice. Default is 'dh' = days or hours with selectbox." - }, - "percent_allowed": { - "name": "Percent allowed", - "default": false, - "type": "boolean", - "description": "Allows to enter a percentage." - }, - "hours_per_day": { - "name": "Hours per day", - "default": 8, - "type": "integer", - "description": "Number of hours in a day, for converting between hours and (working) days." - }, - "empty_not_0": { - "name": "0 or empty", - "default": false, - "type": "boolean", - "description": "Should the widget differ between 0 and empty, which get then returned as NULL" - }, - "short_labels": { - "name": "Short labels", - "default": false, - "type": "boolean", - "description": "use d/h/m instead of day/hour/minute" - }, - "step" : { - "name": "Step limit", - "default": 'any', - "type": "string", - "description": "Works with the min and max attributes to limit the increments at which a numeric or date-time value can be set." - } - }, - - legacyOptions: ["data_format","display_format", "hours_per_day", "empty_not_0", "short_labels"], - - time_formats: {"d":"d","h":"h","m":"m"}, - - /** - * Constructor - * - * @memberOf et2_date_duration - */ - init: function() { - this._super.apply(this, arguments); - - this.input = null; - - // Legacy option put percent in with display format - if(this.options.display_format.indexOf("%") != -1) - { - this.options.percent_allowed = true; - this.options.display_format = this.options.display_format.replace("%",""); - } - - // Clean formats - this.options.display_format = this.options.display_format.replace(/[^dhm]/,''); - if(!this.options.display_format) - { - this.options.display_format = this.attributes.display_format["default"]; - } - - // Get translations - this.time_formats = { - "d": this.options.short_labels ? this.egw().lang("d") : this.egw().lang("Days"), - "h": this.options.short_labels ? this.egw().lang("h") : this.egw().lang("Hours"), - "m": this.options.short_labels ? this.egw().lang("m") : this.egw().lang("Minutes") - }, - this.createInputWidget(); - }, - - createInputWidget: function() { - // Create nodes - this.node = jQuery(document.createElement("span")) - .addClass('et2_date_duration'); - this.duration = jQuery(document.createElement("input")) - .addClass('et2_date_duration') - .attr({type: 'number', size: 3, step:this.options.step, lang: this.egw().preference('number_format')[0] === "," ? "en-150": "en-001"}); - this.node.append(this.duration); - - if(this.options.display_format.length > 1) - { - this.format = jQuery(document.createElement("select")) - .addClass('et2_date_duration'); - this.node.append(this.format); - - for(var i = 0; i < this.options.display_format.length; i++) { - this.format.append(""); - } - } - else if (this.time_formats[this.options.display_format]) - { - this.format = jQuery(""+this.time_formats[this.options.display_format]+"").appendTo(this.node); - } - else - { - this.format = jQuery(""+this.time_formats["m"]+"").appendTo(this.node); - } - var self = this; - // seems the 'invalid' event doesn't work in all browsers, eg. FF therefore - // we use focusout event to check the valifdity of input right after user - // enters the value. - this.duration.on('focusout', function(){if(!self.duration[0].checkValidity()) return self.duration.change();}); - }, - - /** - * Clientside validation - * - * @param {array} _messages - */ - isValid: function(_messages) - { - var ok = true; - // if we have a html5 validation error, show it, as this.input.val() will be empty! - if (this.duration && this.duration[0] && this.duration[0].validationMessage && !this.duration[0].validity.stepMismatch) - { - _messages.push(this.duration[0].validationMessage); - ok = false; - } - return this._super.apply(this, arguments) && ok; - }, - - attachToDOM: function() { - var node = this.getInputNode(); - if (node) - { - jQuery(node).bind("change.et2_inputWidget", this, function(e) { - e.data.change(this); - }); - } - et2_DOMWidget.prototype.attachToDOM.apply(this, arguments); - }, - getDOMNode: function() { - return this.node[0]; - }, - getInputNode: function() { - return this.duration[0]; - }, - - /** - * Use id on node, same as DOMWidget - * - * @param {string} _value id to set - */ - set_id: function(_value) { - this.id = _value; - - var node = this.getDOMNode(this); - if (node) - { - if (_value != "") - { - node.setAttribute("id", this.getInstanceManager().uniqueId+'_'+this.id); - } - else - { - node.removeAttribute("id"); - } - } - }, - set_value: function(_value) { - this.options.value = _value; - - var display = this._convert_to_display(_value); - - // Set display - if(this.duration[0].nodeName == "INPUT") - { - this.duration.val(display.value); - } - else - { - this.duration.text(display.value + " "); - } - - // Set unit as figured for display - if(display.unit != this.options.display_format) - { - if(this.format && this.format.children().length > 1) { - jQuery("option[value='"+display.unit+"']",this.format).attr('selected','selected'); - } - else - { - this.format.text(display.unit ? this.time_formats[display.unit] : ''); - } - } - }, - - /** - * Converts the value in data format into value in display format. - * - * @param _value int/float Data in data format - * - * @return Object {value: Value in display format, unit: unit for display} - */ - _convert_to_display: function(_value) { - if (_value) - { - // Put value into minutes for further processing - switch(this.options.data_format) - { - case 'd': - _value *= this.options.hours_per_day; - // fall-through - case 'h': - _value *= 60; - break; - } - } - - // Figure out best unit for display - var _unit = this.options.display_format == "d" ? "d" : "h"; - if (this.options.display_format.indexOf('m') > -1 && _value && _value < 60) - { - _unit = 'm'; - } - else if (this.options.display_format.indexOf('d') > -1 && _value >= 60*this.options.hours_per_day) - { - _unit = 'd'; - } - _value = this.options.empty_not_0 && _value === '' || !this.options.empty_not_0 && !_value ? '' : - (_unit == 'm' ? parseInt( _value) : (Math.round((_value / 60.0 / (_unit == 'd' ? this.options.hours_per_day : 1))*100)/100)); - - if(_value === '') _unit = ''; - - // use decimal separator from user prefs - var format = this.egw().preference('number_format'); - var sep = format ? format[0] : '.'; - if (typeof _value == 'string' && format && sep && sep != '.') - { - _value = _value.replace('.',sep); - } - - return {value: _value, unit:_unit}; - }, - - /** - * Change displayed value into storage value and return - */ - getValue: function() { - var value = this.duration.val().replace(',', '.'); - if(value === '') - { - return this.options.empty_not_0 ? '' : 0; - } - // Put value into minutes for further processing - switch(this.format && this.format.val() ? this.format.val() : this.options.display_format) - { - case 'd': - value *= this.options.hours_per_day; - // fall-through - case 'h': - value *= 60; - break; - } - // Minutes should be an integer. Floating point math. - value = Math.round(value); - - switch(this.options.data_format) - { - case 'd': - value /= this.options.hours_per_day; - // fall-through - case 'h': - value /= 60.0; - break; - } - return value; - } -});}).call(this); -et2_register_widget(et2_date_duration, ["date-duration"]); - +var et2_date_duration = /** @class */ (function (_super) { + __extends(et2_date_duration, _super); + /** + * Constructor + */ + function et2_date_duration(_parent, _attrs, _child) { + var _this = + // Call the inherited constructor + _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_date_duration._attributes, _child || {})) || this; + _this.legacyOptions = ["data_format", "display_format", "hours_per_day", "empty_not_0", "short_labels"]; + _this.format = null; + // Legacy option put percent in with display format + if (_this.options.display_format.indexOf("%") != -1) { + _this.options.percent_allowed = true; + _this.options.display_format = _this.options.display_format.replace("%", ""); + } + // Clean formats + _this.options.display_format = _this.options.display_format.replace(/[^dhm]/, ''); + if (!_this.options.display_format) { + // @ts-ignore + _this.options.display_format = _this.attributes.display_format["default"]; + } + // Get translations + _this.time_formats = { + "d": _this.options.short_labels ? _this.egw().lang("d") : _this.egw().lang("Days"), + "h": _this.options.short_labels ? _this.egw().lang("h") : _this.egw().lang("Hours"), + "m": _this.options.short_labels ? _this.egw().lang("m") : _this.egw().lang("Minutes") + }, + _this.createInputWidget(); + return _this; + } + et2_date_duration.prototype.createInputWidget = function () { + // Create nodes + this.node = jQuery(document.createElement("span")) + .addClass('et2_date_duration'); + this.duration = jQuery(document.createElement("input")) + .addClass('et2_date_duration') + .attr({ type: 'number', size: 3, step: this.options.step, lang: this.egw().preference('number_format')[0] === "," ? "en-150" : "en-001" }); + this.node.append(this.duration); + var self = this; + // seems the 'invalid' event doesn't work in all browsers, eg. FF therefore + // we use focusout event to check the valifdity of input right after user + // enters the value. + this.duration.on('focusout', function () { + if (!self.duration[0].checkValidity()) + return self.duration.change(); + }); + }; + /** + * Clientside validation + * + * @param {array} _messages + */ + et2_date_duration.prototype.isValid = function (_messages) { + var ok = true; + // if we have a html5 validation error, show it, as this.input.val() will be empty! + if (this.duration && this.duration[0] && + this.duration[0].validationMessage && + !this.duration[0].validity.stepMismatch) { + _messages.push(this.duration[0].validationMessage); + ok = false; + } + return _super.prototype.isValid.call(this, _messages) && ok; + }; + et2_date_duration.prototype.attachToDOM = function () { + var node = this.getInputNode(); + if (node) { + jQuery(node).bind("change.et2_inputWidget", this, function (e) { + e.data.change(this); + }); + } + return et2_core_DOMWidget_1.et2_DOMWidget.prototype.attachToDOM.apply(this, arguments); + }; + et2_date_duration.prototype.getDOMNode = function () { + return this.node[0]; + }; + et2_date_duration.prototype.getInputNode = function () { + return this.duration[0]; + }; + /** + * Use id on node, same as DOMWidget + * + * @param {string} _value id to set + */ + et2_date_duration.prototype.set_id = function (_value) { + this.id = _value; + var node = this.getDOMNode(); + if (node) { + if (_value != "") { + node.setAttribute("id", this.getInstanceManager().uniqueId + '_' + this.id); + } + else { + node.removeAttribute("id"); + } + } + }; + et2_date_duration.prototype.set_value = function (_value) { + this.options.value = _value; + var display = this._convert_to_display(parseFloat(_value)); + // Set display + if (this.duration[0].nodeName == "INPUT") { + this.duration.val(display.value); + } + else { + this.duration.text(display.value + " "); + } + // Set unit as figured for display + if (display.unit != this.options.display_format) { + if (this.format && this.format.children().length > 1) { + jQuery("option[value='" + display.unit + "']", this.format).attr('selected', 'selected'); + } + else { + this.format.text(display.unit ? this.time_formats[display.unit] : ''); + } + } + }; + et2_date_duration.prototype.set_display_format = function (format) { + if (format.length <= 1) { + this.node.remove('select.et2_date_duration'); + this.format = null; + } + this.options.display_format = format; + if ((this.format == null || this.format.is('select')) && (this.options.display_format.length <= 1 || this.options.readonly)) { + if (this.format) { + this.format.remove(); + } + this.format = jQuery(document.createElement('span')).appendTo(this.node); + } + if (this.options.display_format.length > 1 && !this.options.readonly) { + if (this.format && !this.format.is('select')) { + this.format.remove(); + this.format = null; + } + if (!this.format) { + this.format = jQuery(document.createElement("select")) + .addClass('et2_date_duration'); + this.node.append(this.format); + } + this.format.empty(); + for (var i = 0; i < this.options.display_format.length; i++) { + this.format.append(""); + } + } + else if (this.time_formats[this.options.display_format]) { + this.format.text(this.time_formats[this.options.display_format]); + } + else { + this.format.text(this.time_formats["m"]); + } + }; + /** + * Converts the value in data format into value in display format. + * + * @param _value int/float Data in data format + * + * @return Object {value: Value in display format, unit: unit for display} + */ + et2_date_duration.prototype._convert_to_display = function (_value) { + if (_value) { + // Put value into minutes for further processing + switch (this.options.data_format) { + case 'd': + _value *= this.options.hours_per_day; + // fall-through + case 'h': + _value *= 60; + break; + } + } + // Figure out best unit for display + var _unit = this.options.display_format == "d" ? "d" : "h"; + if (this.options.display_format.indexOf('m') > -1 && _value && _value < 60) { + _unit = 'm'; + } + else if (this.options.display_format.indexOf('d') > -1 && _value >= 60 * this.options.hours_per_day) { + _unit = 'd'; + } + _value = this.options.empty_not_0 && _value === '' || !this.options.empty_not_0 && !_value ? '' : + (_unit == 'm' ? parseInt(_value) : (Math.round((_value / 60.0 / (_unit == 'd' ? this.options.hours_per_day : 1)) * 100) / 100)); + if (_value === '') + _unit = ''; + // use decimal separator from user prefs + var format = this.egw().preference('number_format'); + var sep = format ? format[0] : '.'; + if (typeof _value == 'string' && format && sep && sep != '.') { + _value = _value.replace('.', sep); + } + return { value: _value, unit: _unit }; + }; + /** + * Change displayed value into storage value and return + */ + et2_date_duration.prototype.getValue = function () { + var value = this.duration.val().replace(',', '.'); + if (value === '') { + return this.options.empty_not_0 ? '' : 0; + } + // Put value into minutes for further processing + switch (this.format && this.format.val() ? this.format.val() : this.options.display_format) { + case 'd': + value *= this.options.hours_per_day; + // fall-through + case 'h': + value *= 60; + break; + } + // Minutes should be an integer. Floating point math. + value = Math.round(value); + switch (this.options.data_format) { + case 'd': + value /= this.options.hours_per_day; + // fall-through + case 'h': + value /= 60.0; + break; + } + return value; + }; + et2_date_duration._attributes = { + "data_format": { + "name": "Data format", + "default": "m", + "type": "string", + "description": "Units to read/store the data. 'd' = days (float), 'h' = hours (float), 'm' = minutes (int)." + }, + "display_format": { + "name": "Display format", + "default": "dhm", + "type": "string", + "description": "Permitted units for displaying the data. 'd' = days, 'h' = hours, 'm' = minutes. Use combinations to give a choice. Default is 'dh' = days or hours with selectbox." + }, + "percent_allowed": { + "name": "Percent allowed", + "default": false, + "type": "boolean", + "description": "Allows to enter a percentage." + }, + "hours_per_day": { + "name": "Hours per day", + "default": 8, + "type": "integer", + "description": "Number of hours in a day, for converting between hours and (working) days." + }, + "empty_not_0": { + "name": "0 or empty", + "default": false, + "type": "boolean", + "description": "Should the widget differ between 0 and empty, which get then returned as NULL" + }, + "short_labels": { + "name": "Short labels", + "default": false, + "type": "boolean", + "description": "use d/h/m instead of day/hour/minute" + }, + "step": { + "name": "Step limit", + "default": 'any', + "type": "string", + "description": "Works with the min and max attributes to limit the increments at which a numeric or date-time value can be set." + } + }; + return et2_date_duration; +}(et2_date)); +exports.et2_date_duration = et2_date_duration; +et2_core_widget_1.et2_register_widget(et2_date_duration, ["date-duration"]); /** - * @augments et2_date_duration + * r/o date-duration */ -var et2_date_duration_ro = (function(){ "use strict"; return et2_date_duration.extend([et2_IDetachedDOM], -{ - /** - * @memberOf et2_date_duration_ro - */ - createInputWidget: function() { - this.node = jQuery(document.createElement("span")); - this.duration = jQuery(document.createElement("span")).appendTo(this.node); - this.format = jQuery(document.createElement("span")).appendTo(this.node); - }, - - /** - * Code for implementing et2_IDetachedDOM - * Fast-clonable read-only widget that only deals with DOM nodes, not the widget tree - */ - - /** - * Build a list of attributes which can be set when working in the - * "detached" mode in the _attrs array which is provided - * by the calling code. - * - * @param {array} _attrs array to add further attributes to - */ - getDetachedAttributes: function(_attrs) { - _attrs.push("value"); - }, - - /** - * Returns an array of DOM nodes. The (relativly) same DOM-Nodes have to be - * passed to the "setDetachedAttributes" function in the same order. - * - * @return {array} - */ - getDetachedNodes: function() { - return [this.duration[0], this.format[0]]; - }, - - /** - * Sets the given associative attribute->value array and applies the - * attributes to the given DOM-Node. - * - * @param _nodes is an array of nodes which has 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) { - for(var i = 0; i < _nodes.length; i++) { - // Clear the node - for (var j = _nodes[i].childNodes.length - 1; j >= 0; j--) - { - _nodes[i].removeChild(_nodes[i].childNodes[j]); - } - } - if(typeof _values.value !== 'undefined') - { - _values.value = parseFloat(_values.value); - } - if(_values.value) - { - var display = this._convert_to_display(_values.value); - _nodes[0].appendChild(document.createTextNode(display.value)); - _nodes[1].appendChild(document.createTextNode(display.unit)); - } - } - -});}).call(this); -et2_register_widget(et2_date_duration_ro, ["date-duration_ro"]); - +var et2_date_duration_ro = /** @class */ (function (_super) { + __extends(et2_date_duration_ro, _super); + function et2_date_duration_ro() { + return _super !== null && _super.apply(this, arguments) || this; + } + et2_date_duration_ro.prototype.createInputWidget = function () { + this.node = jQuery(document.createElement("span")); + this.duration = jQuery(document.createElement("span")).appendTo(this.node); + this.format = jQuery(document.createElement("span")).appendTo(this.node); + }; + /** + * Code for implementing et2_IDetachedDOM + * Fast-clonable read-only widget that only deals with DOM nodes, not the widget tree + */ + /** + * Build a list of attributes which can be set when working in the + * "detached" mode in the _attrs array which is provided + * by the calling code. + * + * @param {array} _attrs array to add further attributes to + */ + et2_date_duration_ro.prototype.getDetachedAttributes = function (_attrs) { + _attrs.push("value"); + }; + /** + * Returns an array of DOM nodes. The (relativly) same DOM-Nodes have to be + * passed to the "setDetachedAttributes" function in the same order. + * + * @return {array} + */ + et2_date_duration_ro.prototype.getDetachedNodes = function () { + return [this.duration[0], this.format[0]]; + }; + /** + * Sets the given associative attribute->value array and applies the + * attributes to the given DOM-Node. + * + * @param _nodes is an array of nodes which has 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. + */ + et2_date_duration_ro.prototype.setDetachedAttributes = function (_nodes, _values) { + for (var i = 0; i < _nodes.length; i++) { + // Clear the node + for (var j = _nodes[i].childNodes.length - 1; j >= 0; j--) { + _nodes[i].removeChild(_nodes[i].childNodes[j]); + } + } + if (typeof _values.value !== 'undefined') { + _values.value = parseFloat(_values.value); + } + if (_values.value) { + var display = this._convert_to_display(_values.value); + _nodes[0].appendChild(document.createTextNode(display.value)); + _nodes[1].appendChild(document.createTextNode(display.unit)); + } + }; + return et2_date_duration_ro; +}(et2_date_duration)); +exports.et2_date_duration_ro = et2_date_duration_ro; +et2_core_widget_1.et2_register_widget(et2_date_duration_ro, ["date-duration_ro"]); /** * et2_date_ro is the readonly implementation of some date widget. - * @augments et2_valueWidget */ -var et2_date_ro = (function(){ "use strict"; return et2_valueWidget.extend([et2_IDetachedDOM], -{ - /** - * Ignore all more advanced attributes. - */ - attributes: { - "value": { - "type": "string" - }, - "type": { - "ignore": false - }, - "data_format": { - "ignore": true, - "description": "Format data is in. This is not used client-side because it's always a timestamp client side." - }, - min: {ignore: true}, - max: {ignore: true}, - year_range: {ignore: true} - }, - - legacyOptions: ["data_format"], - - /** - * Internal container for working easily with dates - */ - date: new Date(), - - /** - * Constructor - * - * @memberOf et2_date_ro - */ - init: function() { - this._super.apply(this, arguments); - this._labelContainer = jQuery(document.createElement("label")) - .addClass("et2_label"); - this.value = ""; - this.span = jQuery(document.createElement(this._type == "date-since" || this._type == "date-time_today" ? "span" : "time")) - .addClass("et2_date_ro et2_label") - .appendTo(this._labelContainer); - - this.setDOMNode(this._labelContainer[0]); - }, - - set_value: function(_value) { - if(typeof _value == 'undefined') _value = 0; - - this.value = _value; - - if(_value == 0 || _value == null) - { - this.span.attr("datetime", "").text(""); - return; - } - - if(typeof _value == 'string' && _value.match(/(\d{4})-(\d{2})-(\d{2})T(\d{2})\:(\d{2})\:(\d{2})(?:\.\d{3})?(?:Z|[+-](\d{2})\:(\d{2}))/)) - { - this.date = new Date(_value); - this.date = new Date(this.date.valueOf() + (this.date.getTimezoneOffset()*60*1000)); - } - else if(typeof _value == 'string' && (isNaN(_value) || - this.options.data_format && this.options.data_format.substr(0,3) === 'Ymd')) - { - try { - // data_format is not handled server-side for custom-fields in nextmatch - // as parseDateTime requires a separate between date and time, we fix the value here - switch (this.options.data_format) - { - case 'Ymd': - case 'YmdHi': - case 'YmdHis': - _value = _value.substr(0, 4)+'-'+_value.substr(4, 2)+'-'+_value.substr(6, 2)+' '+ - (_value.substr(8, 2) || '00')+':'+(_value.substr(10, 2) || '00')+':'+(_value.substr(12, 2) || '00'); - break; - } - // parseDateTime to handle string PHP: DateTime local date/time format - var parsed = (typeof jQuery.datepicker.parseDateTime("yy-mm-dd","hh:mm:ss", _value) !='undefined')? - jQuery.datepicker.parseDateTime("yy-mm-dd","hh:mm:ss", _value): - jQuery.datepicker.parseDateTime(this.egw().preference('dateformat'),this.egw().preference('timeformat') == '24' ? 'H:i' : 'g:i a', _value); - } - // display unparsable dates as empty - catch(e) { - this.span.attr("datetime", "").text(""); - return; - } - var text = new Date(parsed); - - // Update local variable, but remove the timezone offset that javascript adds - if(parsed) - { - this.date = new Date(text.valueOf() - (text.getTimezoneOffset()*60*1000)); - } - - // JS dates use milliseconds - this.date.setTime(text.valueOf()); - } - else - { - // _value is timestamp in usertime, ready to be used with date() function identical to PHP date() - this.date = _value; - } - var display = this.date.toString(); - - switch(this._type) { - case "time_or_date": - case "date-time_today": - // Today - just the time - if(date('Y-m-d', this.date) == date('Y-m-d')) - { - display = date(this.egw().preference('timeformat') == '24' ? 'H:i' : 'g:i a', this.date); - } - else if (this._type === "time_or_date") - { - display = date(this.egw().preference('dateformat'), this.date); - } - // Before today - date and time - else - { - display = date(this.egw().preference('dateformat') + " " + - (this.egw().preference('timeformat') == '24' ? 'H:i' : 'g:i a'), this.date); - } - break; - case "date": - display = date(this.egw().preference('dateformat'), this.date); - break; - case "date-timeonly": - display = date(this.egw().preference('timeformat') == '24' ? 'H:i' : 'g:i a', this.date); - break; - case "date-time": - display = date(this.egw().preference('dateformat') + " " + - (this.egw().preference('timeformat') == '24' ? 'H:i' : 'g:i a'), this.date); - break; - case "date-since": - var unit2label = { - 'Y': 'years', - 'm': 'month', - 'd': 'days', - 'H': 'hours', - 'i': 'minutes', - 's': 'seconds' - }; - var unit2s = { - 'Y': 31536000, - 'm': 2628000, - 'd': 86400, - 'H': 3600, - 'i': 60, - 's': 1 - }; - var d = new Date(); - var diff = Math.round(d.valueOf() / 1000) - Math.round(this.date.valueOf()/1000); - display = ''; - - for(var unit in unit2s) - { - var unit_s = unit2s[unit]; - if (diff >= unit_s || unit == 's') - { - display = Math.round(diff/unit_s,1)+' '+this.egw().lang(unit2label[unit]); - break; - } - } - break; - } - this.span.attr("datetime", date("Y-m-d H:i:s",this.date)).text(display); - }, - - set_label: function(label) - { - // Remove current label - this._labelContainer.contents() - .filter(function(){ return this.nodeType == 3; }).remove(); - - var parts = et2_csvSplit(label, 2, "%s"); - this._labelContainer.prepend(parts[0]); - this._labelContainer.append(parts[1]); - this.label = label; - - // add class if label is empty - this._labelContainer.toggleClass('et2_label_empty', !label || !parts[0]); - }, - - /** - * 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 array to add further attributes to - */ - getDetachedAttributes: function(_attrs) { - _attrs.push("label", "value","class"); - }, - - /** - * Returns an array of DOM nodes. The (relatively) same DOM-Nodes have to be - * passed to the "setDetachedAttributes" function in the same order. - * - * @return {array} - */ - getDetachedNodes: function() { - return [this._labelContainer[0], this.span[0]]; - }, - - /** - * 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) { - this._labelContainer = jQuery(_nodes[0]); - this.span = jQuery(_nodes[1]); - - this.set_value(_values["value"]); - if(_values["label"]) - { - this.set_label(_values["label"]); - } - if(_values["class"]) - { - this.span.addClass(_values["class"]); - } - } -});}).call(this); -et2_register_widget(et2_date_ro, ["date_ro", "date-time_ro", "date-since", "date-time_today", "time_or_date", "date-timeonly_ro"]); - - +var et2_date_ro = /** @class */ (function (_super) { + __extends(et2_date_ro, _super); + /** + * Constructor + */ + function et2_date_ro(_parent, _attrs, _child) { + var _this = + // Call the inherited constructor + _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_date_ro._attributes, _child || {})) || this; + /** + * Internal container for working easily with dates + */ + _this.date = new Date(); + _this.value = ""; + _this._labelContainer = jQuery(document.createElement("label")) + .addClass("et2_label"); + _this.span = jQuery(document.createElement(_this.getType() == "date-since" || _this.getType() == "date-time_today" ? "span" : "time")) + .addClass("et2_date_ro et2_label") + .appendTo(_this._labelContainer); + _this.setDOMNode(_this._labelContainer[0]); + return _this; + } + et2_date_ro.prototype.set_value = function (_value) { + if (typeof _value == 'undefined') + _value = 0; + this.value = _value; + if (_value == 0 || _value == null) { + this.span.attr("datetime", "").text(""); + return; + } + if (typeof _value == 'string' && _value.match(/(\d{4})-(\d{2})-(\d{2})T(\d{2})\:(\d{2})\:(\d{2})(?:\.\d{3})?(?:Z|[+-](\d{2})\:(\d{2}))/)) { + this.date = new Date(_value); + this.date = new Date(this.date.valueOf() + (this.date.getTimezoneOffset() * 60 * 1000)); + } + else if (typeof _value == 'string' && (isNaN(_value) || + this.options.data_format && this.options.data_format.substr(0, 3) === 'Ymd')) { + try { + // data_format is not handled server-side for custom-fields in nextmatch + // as parseDateTime requires a separate between date and time, we fix the value here + switch (this.options.data_format) { + case 'Ymd': + case 'YmdHi': + case 'YmdHis': + _value = _value.substr(0, 4) + '-' + _value.substr(4, 2) + '-' + _value.substr(6, 2) + ' ' + + (_value.substr(8, 2) || '00') + ':' + (_value.substr(10, 2) || '00') + ':' + (_value.substr(12, 2) || '00'); + break; + } + // parseDateTime to handle string PHP: DateTime local date/time format + // @ts-ignore + var parsed = (typeof jQuery.datepicker.parseDateTime("yy-mm-dd", "hh:mm:ss", _value) != 'undefined') ? + // @ts-ignore + jQuery.datepicker.parseDateTime("yy-mm-dd", "hh:mm:ss", _value) : + // @ts-ignore + jQuery.datepicker.parseDateTime(this.egw().preference('dateformat'), this.egw().preference('timeformat') == '24' ? 'H:i' : 'g:i a', _value); + } + // display unparsable dates as empty + catch (e) { + this.span.attr("datetime", "").text(""); + return; + } + var text = new Date(parsed); + // Update local variable, but remove the timezone offset that javascript adds + if (parsed) { + this.date = new Date(text.valueOf() - (text.getTimezoneOffset() * 60 * 1000)); + } + // JS dates use milliseconds + this.date.setTime(text.valueOf()); + } + else { + // _value is timestamp in usertime, ready to be used with date() function identical to PHP date() + this.date = _value; + } + var display = this.date.toString(); + switch (this.getType()) { + case "time_or_date": + case "date-time_today": + // Today - just the time + if (date('Y-m-d', this.date) == date('Y-m-d')) { + display = date(this.egw().preference('timeformat') == '24' ? 'H:i' : 'g:i a', this.date); + } + else if (this.getType() === "time_or_date") { + display = date(this.egw().preference('dateformat'), this.date); + } + // Before today - date and time + else { + display = date(this.egw().preference('dateformat') + " " + + (this.egw().preference('timeformat') == '24' ? 'H:i' : 'g:i a'), this.date); + } + break; + case "date": + display = date(this.egw().preference('dateformat'), this.date); + break; + case "date-timeonly": + display = date(this.egw().preference('timeformat') == '24' ? 'H:i' : 'g:i a', this.date); + break; + case "date-time": + display = date(this.egw().preference('dateformat') + " " + + (this.egw().preference('timeformat') == '24' ? 'H:i' : 'g:i a'), this.date); + break; + case "date-since": + var unit2label = { + 'Y': 'years', + 'm': 'month', + 'd': 'days', + 'H': 'hours', + 'i': 'minutes', + 's': 'seconds' + }; + var unit2s = { + 'Y': 31536000, + 'm': 2628000, + 'd': 86400, + 'H': 3600, + 'i': 60, + 's': 1 + }; + var d = new Date(); + var diff = Math.round(d.valueOf() / 1000) - Math.round(this.date.valueOf() / 1000); + display = ''; + for (var unit in unit2s) { + var unit_s = unit2s[unit]; + if (diff >= unit_s || unit == 's') { + display = Math.round(diff / unit_s) + ' ' + this.egw().lang(unit2label[unit]); + break; + } + } + break; + } + this.span.attr("datetime", date("Y-m-d H:i:s", this.date)).text(display); + }; + et2_date_ro.prototype.set_label = function (label) { + // Remove current label + this._labelContainer.contents() + .filter(function () { return this.nodeType == 3; }).remove(); + var parts = et2_csvSplit(label, 2, "%s"); + this._labelContainer.prepend(parts[0]); + this._labelContainer.append(parts[1]); + this.label = label; + // add class if label is empty + this._labelContainer.toggleClass('et2_label_empty', !label || !parts[0]); + }; + /** + * 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 array to add further attributes to + */ + et2_date_ro.prototype.getDetachedAttributes = function (_attrs) { + _attrs.push("label", "value", "class"); + }; + /** + * Returns an array of DOM nodes. The (relatively) same DOM-Nodes have to be + * passed to the "setDetachedAttributes" function in the same order. + * + * @return {array} + */ + et2_date_ro.prototype.getDetachedNodes = function () { + return [this._labelContainer[0], this.span[0]]; + }; + /** + * 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. + */ + et2_date_ro.prototype.setDetachedAttributes = function (_nodes, _values) { + this._labelContainer = jQuery(_nodes[0]); + this.span = jQuery(_nodes[1]); + this.set_value(_values["value"]); + if (_values["label"]) { + this.set_label(_values["label"]); + } + if (_values["class"]) { + this.span.addClass(_values["class"]); + } + }; + /** + * Ignore all more advanced attributes. + */ + et2_date_ro._attributes = { + "value": { + "type": "string" + }, + "type": { + "ignore": false + }, + "data_format": { + "ignore": true, + "description": "Format data is in. This is not used client-side because it's always a timestamp client side." + }, + min: { ignore: true }, + max: { ignore: true }, + year_range: { ignore: true } + }; + return et2_date_ro; +}(et2_core_valueWidget_1.et2_valueWidget)); +exports.et2_date_ro = et2_date_ro; +et2_core_widget_1.et2_register_widget(et2_date_ro, ["date_ro", "date-time_ro", "date-since", "date-time_today", "time_or_date", "date-timeonly_ro"]); /** * Widget for selecting a date range - * - * @augments et2_inputWidget */ -var et2_date_range = (function(){ "use strict"; return et2_inputWidget.extend({ - attributes: { - value: { - "type": "any", - "description": "An object with keys 'from' and 'to' for absolute ranges, or a relative range string" - }, - relative: { - name: 'Relative', - type: 'boolean', - description: 'Is the date range relative (this week) or absolute (2016-02-15 - 2016-02-21). This will affect the value returned.' - } - }, - - /** - * Constructor - * - * @memberOf et2_number - */ - init: function init() { - this._super.apply(this, arguments); - - this.div = jQuery(document.createElement('div')) - .attr({ class:'et2_date_range'}); - - this.from = null; - this.to = null; - this.select = null; - - // Set domid - this.set_id(this.id); - - this.setDOMNode(this.div[0]); - this._createWidget(); - - this.set_relative(this.options.relative || false); - }, - - _createWidget: function createInputWidget() { - var widget = this; - - this.from = et2_createWidget('date',{ - id: this.id+'[from]', - blur: egw.lang('From'), - onchange: function() { widget.to.set_min(widget.from.getValue()); } - },this); - this.to = et2_createWidget('date',{ - id: this.id+'[to]', - blur: egw.lang('To'), - onchange: function() {widget.from.set_max(widget.to.getValue()); } - },this); - this.select = et2_createWidget('select',{ - id: this.id+'[relative]', - select_options: et2_date_range.relative_dates, - empty_label: this.options.blur || 'All' - },this); - this.select.loadingFinished(); - }, - - /** - * Function which allows iterating over the complete widget tree. - * Overridden here to avoid problems with children when getting value - * - * @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); - } - }, - - /** - * Toggles relative or absolute dates - * - * @param {boolean} _value - */ - set_relative: function set_relative(_value) - { - this.options.relative = _value; - if(this.options.relative) - { - jQuery(this.from.getDOMNode()).hide(); - jQuery(this.to.getDOMNode()).hide(); - } - else - { - jQuery(this.select.getDOMNode()).hide(); - } - }, - - set_value: function set_value(value) - { - if(!value || typeof value == 'null') - { - this.select.set_value(''); - this.from.set_value(null); - this.to.set_value(null); - } - - // Relative - if(value && typeof value === 'string') - { - this._set_relative_value(value); - - } - else if(value && typeof value.from === 'undefined' && value[0]) - { - value = { - from: value[0], - to: value[1] || new Date().valueOf()/1000 - }; - } - else if (value && value.from && value.to) - { - this.from.set_value(value.from); - this.to.set_value(value.to); - } - }, - - getValue: function getValue() - { - return this.options.relative ? - this.select.getValue() : - { from: this.from.getValue(), to: this.to.getValue() }; - }, - - _set_relative_value: function(_value) - { - if(this.options.relative) - { - jQuery(this.select.getDOMNode()).show(); - } - // Show description - this.select.set_value(_value); - - var tempDate = new Date(); - var today = new Date(tempDate.getFullYear(), tempDate.getMonth(), tempDate.getDate(),0,-tempDate.getTimezoneOffset(),0); - - // Use strings to avoid references - this.from.set_value(today.toJSON()); - this.to.set_value(today.toJSON()); - - var relative = null; - for(var index in et2_date_range.relative_dates) - { - if(et2_date_range.relative_dates[index].value === _value) - { - relative = et2_date_range.relative_dates[index]; - break; - } - } - if(relative) - { - var dates = ["from","to"]; - var value = today.toJSON(); - for(var i = 0; i < dates.length; i++) - { - var date = dates[i]; - if(typeof relative[date] == "function") - { - value = relative[date](new Date(value)); - } - else - { - value = this[date]._relativeDate(relative[date]); - } - this[date].set_value(value); - } - } - } -});}).call(this); -et2_register_widget(et2_date_range, ["date-range"]); -// Static part of the date range class -jQuery.extend(et2_date_range, -{ - // Class Constants - relative_dates: [ - // Start and end are relative offsets, see et2_date.set_min() - // or Date objects - { - value: 'Today', - label: 'Today', - from: function(date) {return date;}, - to: function(date) {return date;} - }, - { - label: 'Yesterday', - value: 'Yesterday', - from: function(date) { - date.setUTCDate(date.getUTCDate() - 1); - return date; - }, - to: '' - }, - { - label: 'This week', - value: 'This week', - from: function(date) {return egw.week_start(date);}, - to: function(date) { - date.setUTCDate(date.getUTCDate() + 6); - return date; - } - }, - { - label: 'Last week', - value: 'Last week', - from: function(date) { - var d = egw.week_start(date); - d.setUTCDate(d.getUTCDate() - 7); - return d; - }, - to: function(date) { - date.setUTCDate(date.getUTCDate() + 6); - return date; - } - }, - { - label: 'This month', - value: 'This month', - from: function(date) - { - date.setUTCDate(1); - return date; - }, - to: function(date) - { - date.setUTCMonth(date.getUTCMonth()+1); - date.setUTCDate(0); - return date; - } - }, - { - label: 'Last month', - value: 'Last month', - from: function(date) - { - date.setUTCMonth(date.getUTCMonth() - 1); - date.setUTCDate(1); - return date; - }, - to: function(date) - { - date.setUTCMonth(date.getUTCMonth()+1); - date.setUTCDate(0); - return date; - } - }, - { - label: 'Last 3 months', - value: 'Last 3 months', - from: function(date) - { - date.setUTCMonth(date.getUTCMonth() - 2); - date.setUTCDate(1); - return date; - }, - to: function(date) - { - date.setUTCMonth(date.getUTCMonth()+3); - date.setUTCDate(0); - return date; - } - }, - /* - 'This quarter'=> array(0,0,0,0, 0,0,0,0), // Just a marker, needs special handling - 'Last quarter'=> array(0,-4,0,0, 0,-4,0,0), // Just a marker - */ - { - label: 'This year', - value: 'This year', - from: function(d) { - d.setUTCMonth(0); - d.setUTCDate(1); - return d; - }, - to: function(d) { - d.setUTCMonth(11); - d.setUTCDate(31); - return d; - } - }, - { - label: 'Last year', - value: 'Last year', - from: function(d) { - d.setUTCMonth(0); - d.setUTCDate(1); - d.setUTCYear(d.getUTCYear() - 1); - return d; - }, - to: function(d) { - d.setUTCMonth(11); - d.setUTCDate(31); - d.setUTCYear(d.getUTCYear() - 1); - return d; - } - } - /* Still needed? - '2 years ago' => array(-2,0,0,0, -1,0,0,0), - '3 years ago' => array(-3,0,0,0, -2,0,0,0), - */ - ] -}); +var et2_date_range = /** @class */ (function (_super) { + __extends(et2_date_range, _super); + /** + * Constructor + */ + function et2_date_range(_parent, _attrs, _child) { + var _this = + // Call the inherited constructor + _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_date_range._attributes, _child || {})) || this; + _this.div = jQuery(document.createElement('div')) + .attr({ class: 'et2_date_range' }); + _this.from = null; + _this.to = null; + _this.select = null; + // Set domid + _this.set_id(_this.id); + _this.setDOMNode(_this.div[0]); + _this._createWidget(); + _this.set_relative(_this.options.relative || false); + return _this; + } + et2_date_range.prototype._createWidget = function () { + var widget = this; + this.from = et2_core_widget_1.et2_createWidget('date', { + id: this.id + '[from]', + blur: egw.lang('From'), + onchange: function () { widget.to.set_min(widget.from.getValue()); } + }, this); + this.to = et2_core_widget_1.et2_createWidget('date', { + id: this.id + '[to]', + blur: egw.lang('To'), + onchange: function () { widget.from.set_max(widget.to.getValue()); } + }, this); + this.select = et2_core_widget_1.et2_createWidget('select', { + id: this.id + '[relative]', + select_options: et2_date_range.relative_dates, + empty_label: this.options.blur || 'All' + }, this); + this.select.loadingFinished(); + }; + /** + * Function which allows iterating over the complete widget tree. + * Overridden here to avoid problems with children when getting value + * + * @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_date_range.prototype.iterateOver = function (_callback, _context, _type) { + if (typeof _type == "undefined") { + _type = et2_core_widget_1.et2_widget; + } + if (this.isInTree() && this.instanceOf(_type)) { + _callback.call(_context, this); + } + }; + /** + * Toggles relative or absolute dates + * + * @param {boolean} _value + */ + et2_date_range.prototype.set_relative = function (_value) { + this.options.relative = _value; + if (this.options.relative) { + jQuery(this.from.getDOMNode()).hide(); + jQuery(this.to.getDOMNode()).hide(); + } + else { + jQuery(this.select.getDOMNode()).hide(); + } + }; + et2_date_range.prototype.set_value = function (value) { + // @ts-ignore + if (!value || typeof value == 'null') { + this.select.set_value(''); + this.from.set_value(null); + this.to.set_value(null); + } + // Relative + if (value && typeof value === 'string') { + this._set_relative_value(value); + } + else if (value && typeof value.from === 'undefined' && value[0]) { + value = { + from: value[0], + to: value[1] || new Date().valueOf() / 1000 + }; + } + else if (value && value.from && value.to) { + this.from.set_value(value.from); + this.to.set_value(value.to); + } + }; + et2_date_range.prototype.getValue = function () { + return this.options.relative ? + this.select.getValue() : + { from: this.from.getValue(), to: this.to.getValue() }; + }; + et2_date_range.prototype._set_relative_value = function (_value) { + if (this.options.relative) { + jQuery(this.select.getDOMNode()).show(); + } + // Show description + this.select.set_value(_value); + var tempDate = new Date(); + var today = new Date(tempDate.getFullYear(), tempDate.getMonth(), tempDate.getDate(), 0, -tempDate.getTimezoneOffset(), 0); + // Use strings to avoid references + this.from.set_value(today.toJSON()); + this.to.set_value(today.toJSON()); + var relative = null; + for (var index in et2_date_range.relative_dates) { + if (et2_date_range.relative_dates[index].value === _value) { + relative = et2_date_range.relative_dates[index]; + break; + } + } + if (relative) { + var dates = ["from", "to"]; + var value = today.toJSON(); + for (var i = 0; i < dates.length; i++) { + var date = dates[i]; + if (typeof relative[date] == "function") { + value = relative[date](new Date(value)); + } + else { + value = this[date]._relativeDate(relative[date]); + } + this[date].set_value(value); + } + } + }; + et2_date_range._attributes = { + value: { + "type": "any", + "description": "An object with keys 'from' and 'to' for absolute ranges, or a relative range string" + }, + relative: { + name: 'Relative', + type: 'boolean', + description: 'Is the date range relative (this week) or absolute (2016-02-15 - 2016-02-21). This will affect the value returned.' + } + }; + // Class Constants + et2_date_range.relative_dates = [ + // Start and end are relative offsets, see et2_date.set_min() + // or Date objects + { + value: 'Today', + label: 'Today', + from: function (date) { return date; }, + to: function (date) { return date; } + }, + { + label: 'Yesterday', + value: 'Yesterday', + from: function (date) { + date.setUTCDate(date.getUTCDate() - 1); + return date; + }, + to: '' + }, + { + label: 'This week', + value: 'This week', + from: function (date) { return egw.week_start(date); }, + to: function (date) { + date.setUTCDate(date.getUTCDate() + 6); + return date; + } + }, + { + label: 'Last week', + value: 'Last week', + from: function (date) { + var d = egw.week_start(date); + d.setUTCDate(d.getUTCDate() - 7); + return d; + }, + to: function (date) { + date.setUTCDate(date.getUTCDate() + 6); + return date; + } + }, + { + label: 'This month', + value: 'This month', + from: function (date) { + date.setUTCDate(1); + return date; + }, + to: function (date) { + date.setUTCMonth(date.getUTCMonth() + 1); + date.setUTCDate(0); + return date; + } + }, + { + label: 'Last month', + value: 'Last month', + from: function (date) { + date.setUTCMonth(date.getUTCMonth() - 1); + date.setUTCDate(1); + return date; + }, + to: function (date) { + date.setUTCMonth(date.getUTCMonth() + 1); + date.setUTCDate(0); + return date; + } + }, + { + label: 'Last 3 months', + value: 'Last 3 months', + from: function (date) { + date.setUTCMonth(date.getUTCMonth() - 2); + date.setUTCDate(1); + return date; + }, + to: function (date) { + date.setUTCMonth(date.getUTCMonth() + 3); + date.setUTCDate(0); + return date; + } + }, + /* + 'This quarter'=> array(0,0,0,0, 0,0,0,0), // Just a marker, needs special handling + 'Last quarter'=> array(0,-4,0,0, 0,-4,0,0), // Just a marker + */ + { + label: 'This year', + value: 'This year', + from: function (d) { + d.setUTCMonth(0); + d.setUTCDate(1); + return d; + }, + to: function (d) { + d.setUTCMonth(11); + d.setUTCDate(31); + return d; + } + }, + { + label: 'Last year', + value: 'Last year', + from: function (d) { + d.setUTCMonth(0); + d.setUTCDate(1); + d.setUTCYear(d.getUTCYear() - 1); + return d; + }, + to: function (d) { + d.setUTCMonth(11); + d.setUTCDate(31); + d.setUTCYear(d.getUTCYear() - 1); + return d; + } + } + /* Still needed? + '2 years ago' => array(-2,0,0,0, -1,0,0,0), + '3 years ago' => array(-3,0,0,0, -2,0,0,0), + */ + ]; + return et2_date_range; +}(et2_core_inputWidget_1.et2_inputWidget)); +exports.et2_date_range = et2_date_range; +et2_core_widget_1.et2_register_widget(et2_date_range, ["date-range"]); +//# sourceMappingURL=et2_widget_date.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_date.ts b/api/js/etemplate/et2_widget_date.ts new file mode 100644 index 0000000000..85d93ae0bc --- /dev/null +++ b/api/js/etemplate/et2_widget_date.ts @@ -0,0 +1,1628 @@ +/** + * EGroupware eTemplate2 - JS Date object + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage api + * @link http://www.egroupware.org + * @author Nathan Gray + * @copyright Nathan Gray 2011 + */ + +/*egw:uses + /vendor/bower-asset/jquery/dist/jquery.js; + /vendor/bower-asset/jquery-ui/jquery-ui.js; + lib/date; + et2_core_inputWidget; + et2_core_valueWidget; +*/ + +import './et2_core_common'; +import {ClassWithAttributes} from "./et2_core_inheritance"; +import {et2_createWidget, et2_register_widget, et2_widget, WidgetConfig} from "./et2_core_widget"; +import {et2_valueWidget} from './et2_core_valueWidget' +import {et2_inputWidget} from './et2_core_inputWidget' +import {et2_selectbox} from './et2_widget_selectbox' +import './et2_types'; +import {et2_DOMWidget} from "./et2_core_DOMWidget"; + +// lib/date.js: +declare function date (format : string, timestamp? : string | number | Date); + +// all calls to jQueryUI.datetimepicker as jQuery.datepicker give errors which are currently suppressed with @ts-ignore +// adding npm package @types/jquery.ui.datetimepicker did NOT help :( + +/** + * Class which implements the "date" XET-Tag + * + * Dates are passed to the server in ISO8601 format ("Y-m-d\TH:i:sP"), and data_format is + * handled server-side. + * + * Widgets uses jQuery date- and time-picker for desktop browsers and + * HTML5 input fields for mobile devices to get their native UI for date/time entry. + */ +export class et2_date extends et2_inputWidget +{ + static readonly _attributes: any = { + "value": { + "type": "any" + }, + "type": { + "ignore": false + }, + "blur": { + "name": "Placeholder", + "type": "string", + "default": "", + "description": "This text get displayed if an input-field is empty and does not have the input-focus (blur). It can be used to show a default value or a kind of help-text." + }, + "data_format": { + "ignore": true, + "description": "Date/Time format. Can be set as an options to date widget", + "default": '' + }, + year_range: { + name: "Year range", + type: "string", + default: "c-10:c+10", + description: "The range of years displayed in the year drop-down: either relative to today's year (\"-nn:+nn\"), relative to the currently selected year (\"c-nn:c+nn\"), absolute (\"nnnn:nnnn\"), or combinations of these formats (\"nnnn:-nn\"). Note that this option only affects what appears in the drop-down, to restrict which dates may be selected use the min and/or max options." + }, + min: { + "name": "Minimum", + "type": "any", + "default": et2_no_init, + "description": 'Minimum allowed date. Multiple types supported:\ +Date: A date object containing the minimum date.\ +Number: A number of days from today. For example 2 represents two days from today and -1 represents yesterday.\ +String: A string in the user\'s date format, or a relative date. Relative dates must contain value and period pairs; valid periods are "y" for years, "m" for months, "w" for weeks, and "d" for days. For example, "+1m +7d" represents one month and seven days from today.' + }, + max: { + "name": "Maximum", + "type": "any", + "default": et2_no_init, + "description": 'Maximum allowed date. Multiple types supported:\ +Date: A date object containing the maximum date.\ +Number: A number of days from today. For example 2 represents two days from today and -1 represents yesterday.\ +String: A string in the user\'s date format, or a relative date. Relative dates must contain value and period pairs; valid periods are "y" for years, "m" for months, "w" for weeks, and "d" for days. For example, "+1m +7d" represents one month and seven days from today.' + }, + inline: { + "name": "Inline", + "type": "boolean", + "default": false, + "description": "Instead of an input field with a popup calendar, the calendar is displayed inline, with no input field" + } + }; + + legacyOptions: string[] = ["data_format"]; + date: Date; + span: JQuery; + input_date: JQuery = null; + is_mobile: boolean = false; + dateFormat: string; + timeFormat: string; + + /** + * Constructor + */ + constructor(_parent, _attrs? : WidgetConfig, _child? : object) + { + // Call the inherited constructor + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_date._attributes, _child || {})); + + this.date = new Date(); + this.date.setUTCHours(0); + this.date.setMinutes(0); + this.date.setSeconds(0); + + this.createInputWidget(); + } + + createInputWidget() + { + this.span = jQuery(document.createElement(this.options.inline ? 'div' : "span")).addClass("et2_date"); + + this.input_date = jQuery(document.createElement(this.options.inline ? "div" : "input")); + if (this.options.blur) this.input_date.attr('placeholder', this.egw().lang(this.options.blur)); + this.input_date.addClass("et2_date").attr("type", "text") + .attr("size", 7) // strlen("10:00pm")=7 + .appendTo(this.span); + + this.setDOMNode(this.span[0]); + + // inline calendar is not existing in html5, so allways use datepicker instead + this.is_mobile = egwIsMobile() && !this.options.inline; + + if (this.is_mobile) + { + this.dateFormat = 'yy-mm-dd'; + this.timeFormat = 'HH:mm'; + switch(this.getType()) + { + case 'date': + this.input_date.attr('type', 'date'); + break; + case 'date-time': + this.input_date.attr('type', 'datetime-local'); + break; + case 'date-timeonly': + this.input_date.addClass("et2_time"); + this.input_date.attr('type', 'time'); + break; + } + } + else + { + this.dateFormat = this.egw().dateTimeFormat(this.egw().preference("dateformat")); + this.timeFormat = this.egw().preference("timeformat") == 12 ? "h:mmtt" : "HH:mm"; + // jQuery-UI date picker + if(this.getType() != 'date-timeonly') + { + this.egw().calendar(this.input_date, this.getType() == "date-time"); + } + else + { + this.input_date.addClass("et2_time"); + this.egw().time(this.input_date); + } + + // Avoid collision of datepicker dialog with input field + var widget = this; + this.input_date.datepicker('option', 'beforeShow', function(input, inst){ + var cal = inst.dpDiv; + setTimeout(function () { + var $input = jQuery(input); + var inputOffset = $input.offset(); + // position the datepicker in freespace zone + // avoid datepicker calendar collision with input field + if (cal.height() + inputOffset.top > window.innerHeight) + { + cal.position({ + my: "left center", + at: 'right bottom', + collision: 'flip fit', + of: input + }); + } + // Add tooltip to Today/Now button + jQuery('[data-handler="today"]',cal).attr('title', + widget.getType() == 'date' ? egw.lang('Today') : egw.lang('Now') + ); + + },0); + }) + .datepicker('option','onClose', function(dateText, inst) { + // Lose focus, avoids an issue with focus + // not allowing datepicker to re-open + inst.input.blur(); + }); + } + + // Update internal value when changed + var self = this; + this.input_date.bind('change', function(e){ + self.set_value(this.value); + return false; + }); + + // Framewok skips nulls, but null needs to be processed here + if(this.options.value == null) + { + this.set_value(null); + } + } + + set_type(_type) + { + if(_type != this.getType()) + { + super.setType(_type); + this.createInputWidget(); + } + } + + /** + * Dynamic disable or enable datepicker + * + * @param {boolean} _ro + */ + set_readonly(_ro) + { + if (this.input_date && !this.input_date.attr('disabled') != !_ro) + { + this.input_date.prop('disabled', !!_ro) + .datepicker('option', 'disabled', !!_ro); + } + } + + /** + * Set (full) year of current date + * + * @param {number} _value 4-digit year + */ + set_year(_value) + { + this.date.setUTCFullYear(_value); + this.set_value(this.date); + } + /** + * Set month (1..12) of current date + * + * @param {number} _value 1..12 + */ + set_month(_value) + { + this.date.setUTCMonth(_value-1); + this.set_value(this.date); + } + /** + * Set day of current date + * + * @param {number} _value 1..31 + */ + set_date(_value) + { + this.date.setUTCDate(_value); + this.set_value(this.date); + } + /** + * Set hour (0..23) of current date + * + * @param {number} _value 0..23 + */ + set_hours(_value) + { + this.date.setUTCHours(_value); + this.set_value(this.date); + } + /** + * Set minute (0..59) of current date + * + * @param {number} _value 0..59 + */ + set_minutes(_value) + { + this.date.setUTCMinutes(_value); + this.set_value(this.date); + } + /** + * Get (full) year of current date + * + * @return {number|null} 4-digit year or null for empty + */ + get_year() + { + return this.input_date.val() == "" ? null : this.date.getUTCFullYear(); + } + /** + * Get month (1..12) of current date + * + * @return {number|null} 1..12 or null for empty + */ + get_month() + { + return this.input_date.val() == "" ? null : this.date.getUTCMonth()+1; + } + /** + * Get day of current date + * + * @return {number|null} 1..31 or null for empty + */ + get_date() + { + return this.input_date.val() == "" ? null : this.date.getUTCDate(); + } + /** + * Get hour (0..23) of current date + * + * @return {number|null} 0..23 or null for empty + */ + get_hours() + { + return this.input_date.val() == "" ? null : this.date.getUTCHours(); + } + /** + * Get minute (0..59) of current date + * + * @return {number|null} 0..59 or null for empty + */ + get_minutes() + { + return this.input_date.val() == "" ? null : this.date.getUTCMinutes(); + } + /** + * Get timestamp + * + * You can use set_value to set a timestamp. + * + * @return {number|null} timestamp (seconds since 1970-01-01) + */ + get_time() + { + return this.input_date.val() == "" ? null : this.date.getTime(); + } + + /** + * The range of years displayed in the year drop-down: either relative + * to today's year ("-nn:+nn"), relative to the currently selected year + * ("c-nn:c+nn"), absolute ("nnnn:nnnn"), or combinations of these formats + * ("nnnn:-nn"). Note that this option only affects what appears in the + * drop-down, to restrict which dates may be selected use the min_date + * and/or max_date options. + * @param {string} _value + */ + set_year_range(_value) + { + if(this.input_date && this.getType() == 'date' && !this.is_mobile) + { + this.input_date.datepicker('option','yearRange',_value); + } + this.options.year_range = _value; + } + + /** + * Set the minimum allowed date + * + * The minimum selectable date. When set to null, there is no minimum. + * Multiple types supported: + * Date: A date object containing the minimum date. + * Number: A number of days from today. For example 2 represents two days + * from today and -1 represents yesterday. + * String: A string in the format defined by the dateFormat option, or a + * relative date. Relative dates must contain value and period pairs; + * valid periods are "y" for years, "m" for months, "w" for weeks, and + * "d" for days. For example, "+1m +7d" represents one month and seven + * days from today. + * @param {Date|Number|String} _value + */ + set_min(_value) + { + if(this.input_date) + { + if (this.is_mobile) + { + this.input_date.attr('min', this._relativeDate(_value)); + } + else + { + // Check for full timestamp + if(typeof _value == 'string' && _value.match(/(\d{4})-(\d{2})-(\d{2})T(\d{2})\:(\d{2})\:(\d{2})(?:\.\d{3})?(?:Z|[+-](\d{2})\:(\d{2}))/)) + { + _value = new Date(_value); + // Add timezone offset back in, or formatDate will lose those hours + var formatDate = new Date(_value.valueOf() + this.date.getTimezoneOffset() * 60 * 1000); + if(this.getType() == 'date') + { + _value = jQuery.datepicker.formatDate(this.dateFormat, formatDate); + } + } + this.input_date.datepicker('option','minDate',_value); + } + } + this.options.min = _value; + } + + /** + * Convert non html5 min or max attributes described above to timestamps + * + * @param {string|Date} _value + */ + _relativeDate(_value) + { + if (typeof _value == 'string' && _value.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/)) return _value; + + // @ts-ignore + return jQuery.datepicker._determineDate(jQuery.datepicker, _value, this.date).toJSON(); + } + + /** + * Set the maximum allowed date + * + * The maximum selectable date. When set to null, there is no maximum. + * Multiple types supported: + * Date: A date object containing the maximum date. + * Number: A number of days from today. For example 2 represents two days + * from today and -1 represents yesterday. + * String: A string in the format defined by the dateFormat option, or a + * relative date. Relative dates must contain value and period pairs; + * valid periods are "y" for years, "m" for months, "w" for weeks, and + * "d" for days. For example, "+1m +7d" represents one month and seven + * days from today. + * @param {Date|Number|String} _value + */ + set_max(_value) + { + if(this.input_date) + { + if (this.is_mobile) + { + this.input_date.attr('max', this._relativeDate(_value)); + } + else + { + // Check for full timestamp + if(typeof _value == 'string' && _value.match(/(\d{4})-(\d{2})-(\d{2})T(\d{2})\:(\d{2})\:(\d{2})(?:\.\d{3})?(?:Z|[+-](\d{2})\:(\d{2}))/)) + { + _value = new Date(_value); + // Add timezone offset back in, or formatDate will lose those hours + var formatDate = new Date(_value.valueOf() + this.date.getTimezoneOffset() * 60 * 1000); + if(this.getType() == 'date') + { + _value = jQuery.datepicker.formatDate(this.dateFormat, formatDate); + } + } + this.input_date.datepicker('option','maxDate',_value); + } + } + this.options.max = _value; + } + + /** + * Setting date + * + * @param {string|number|Date} _value supported are the following formats: + * - Date object with usertime as UTC value + * - string like Date.toJSON() + * - string or number with timestamp in usertime like server-side uses it + * - string starting with + or - to add/substract given number of seconds from current value, "+600" to add 10 minutes + */ + set_value(_value) + { + var old_value = this._oldValue; + if(_value === null || _value === "" || _value === undefined || + // allow 0 as empty-value for date and date-time widgets, as that is used a lot eg. in InfoLog + _value == 0 && (this.getType() == 'date-time' || this.getType() == 'date')) + { + if(this.input_date) + { + this.input_date.val(""); + } + if(this._oldValue !== et2_no_init && old_value !== _value) + { + this.change(this.input_date); + } + this._oldValue = _value; + return; + } + + // timestamp in usertime, convert to 'Y-m-d\\TH:i:s\\Z', as we do on server-side with equivalent of PHP date() + if (typeof _value == 'number' || typeof _value == 'string' && !isNaN(_value) && _value[0] != '+' && _value[0] != '-') + { + _value = date('Y-m-d\\TH:i:s\\Z', _value); + } + // Check for full timestamp + if(typeof _value == 'string' && _value.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2})\:(\d{2})\:(\d{2})(?:\.\d{3})?(?:Z|[+-](\d{2})\:(\d{2})|)$/)) + { + _value = new Date(_value); + } + // Handle just time as a string in the form H:i + if(typeof _value == 'string' && isNaN(_value)) + { + try { + // silently fix skiped minutes or times with just one digit, as parser is quite pedantic ;-) + var fix_reg = new RegExp((this.getType() == "date-timeonly"?'^':' ')+'([0-9]+)(:[0-9]*)?( ?(a|p)m?)?$','i'); + var matches = _value.match(fix_reg); + if (matches && (matches[1].length < 2 || matches[2] === undefined || matches[2].length < 3 || + matches[3] && matches[3] != 'am' && matches[3] != 'pm')) + { + if (matches[1].length < 2 && !matches[3]) matches[1] = '0'+matches[1]; + if (matches[2] === undefined) matches[2] = ':00'; + while (matches[2].length < 3) matches[2] = ':0'+matches[2].substr(1); + _value = _value.replace(fix_reg, (this.getType() == "date-timeonly"?'':' ')+matches[1]+matches[2]+matches[3]); + if (matches[4] !== undefined) matches[3] = matches[4].toLowerCase() == 'a' ? 'am' : 'pm'; + } + switch(this.getType()) + { + case "date-timeonly": + // @ts-ignore + var parsed = jQuery.datepicker.parseTime(this.timeFormat, _value); + if (!parsed) // parseTime returns false + { + this.set_validation_error(this.egw().lang("'%1' has an invalid format !!!",_value)); + return; + } + this.set_validation_error(false); + // this.date is on current date, changing it in get_value() to 1970-01-01, gives a time-difference, if we are currently on DST + this.date.setDate(1); + this.date.setMonth(0); + this.date.setFullYear(1970); + // Avoid javascript timezone offset, hour is in 'user time' + this.date.setUTCHours(parsed.hour); + this.date.setMinutes(parsed.minute); + if(this.input_date.val() != _value) + { + this.input_date.val(_value); + // @ts-ignore + this.input_date.timepicker('setTime',_value); + if (this._oldValue !== et2_no_init) + { + this.change(this.input_date); + } + } + this._oldValue = this.date.toJSON(); + return; + default: + // Parse customfields's date with storage data_format to date object + // Or generally any date widgets with fixed date/time format + if (this.id.match(/^#/g) && this.options.value == _value || (this.options.data_format && this.options.value == _value)) + { + switch (this.getType()) + { + case 'date': + var parsed = jQuery.datepicker.parseDate(this.egw().dateTimeFormat(this.options.data_format), _value); + break; + case 'date-time': + var DTformat = this.options.data_format.split(' '); + // @ts-ignore + var parsed = jQuery.datepicker.parseDateTime(this.egw().dateTimeFormat(DTformat[0]),this.egw().dateTimeFormat(DTformat[1]), _value); + } + } + else // Parse other date widgets date with timepicker date/time format to date onject + { + // @ts-ignore + var parsed = jQuery.datepicker.parseDateTime(this.dateFormat, + this.timeFormat, _value.replace('T', ' ')); + if(!parsed) + { + this.set_validation_error(this.egw().lang("%1' han an invalid format !!!",_value)); + return; + } + } + // Update local variable, but remove the timezone offset that + // javascript adds when we parse + if(parsed) + { + this.date = new Date(parsed.valueOf() - parsed.getTimezoneOffset() * 60000); + } + + this.set_validation_error(false); + } + } + // catch exception from unparsable date and display it empty instead + catch(e) { + return this.set_value(null); + } + } else if (typeof _value == 'object' && _value.date) { + this.date = _value.date; + } else if (typeof _value == 'object' && _value.valueOf) { + this.date = _value; + } else + // string starting with + or - --> add/substract number of seconds from current value + { + this.date.setTime(this.date.getTime()+1000*parseInt(_value)); + } + + // Update input - popups do, but framework doesn't + _value = ''; + // Add timezone offset back in, or formatDate will lose those hours + var formatDate = new Date(this.date.valueOf() + this.date.getTimezoneOffset() * 60 * 1000); + if(this.getType() != 'date-timeonly') + { + _value = jQuery.datepicker.formatDate(this.dateFormat, formatDate); + } + if(this.getType() != 'date') + { + if(this.getType() != 'date-timeonly') _value += this.is_mobile ? 'T' : ' '; + + // @ts-ignore + _value += jQuery.datepicker.formatTime(this.timeFormat, { + hour: formatDate.getHours(), + minute: formatDate.getMinutes(), + seconds: 0, + timezone: 0 + }); + } + if(this.options.inline ) + { + this.input_date.datepicker("setDate",formatDate); + } + else + { + this.input_date.val(_value); + } + if(this._oldValue !== et2_no_init && old_value != this.getValue()) + { + this.change(this.input_date); + } + this._oldValue = _value; + } + + getValue() + { + if(this.input_date.val() == "") + { + // User blanked the box + return null; + } + // date-timeonly returns just the seconds, without any date! + if (this.getType() == 'date-timeonly') + { + this.date.setUTCDate(1); + this.date.setUTCMonth(0); + this.date.setUTCFullYear(1970); + } + else if (this.getType() == 'date') + { + this.date.setUTCHours(0); + this.date.setUTCMinutes(0); + } + + // Convert to timestamp - no seconds + this.date.setSeconds(0,0); + return (this.date && typeof this.date.toJSON != 'undefined' && this.date.toJSON())?this.date.toJSON().replace(/\.\d{3}Z$/, 'Z'):this.date; + } +} +et2_register_widget(et2_date, ["date", "date-time", "date-timeonly"]); + +/** + * Class which implements the "date-duration" XET-Tag + */ +export class et2_date_duration extends et2_date +{ + static readonly _attributes: any = { + "data_format": { + "name": "Data format", + "default": "m", + "type": "string", + "description": "Units to read/store the data. 'd' = days (float), 'h' = hours (float), 'm' = minutes (int)." + }, + "display_format": { + "name": "Display format", + "default": "dhm", + "type": "string", + "description": "Permitted units for displaying the data. 'd' = days, 'h' = hours, 'm' = minutes. Use combinations to give a choice. Default is 'dh' = days or hours with selectbox." + }, + "percent_allowed": { + "name": "Percent allowed", + "default": false, + "type": "boolean", + "description": "Allows to enter a percentage." + }, + "hours_per_day": { + "name": "Hours per day", + "default": 8, + "type": "integer", + "description": "Number of hours in a day, for converting between hours and (working) days." + }, + "empty_not_0": { + "name": "0 or empty", + "default": false, + "type": "boolean", + "description": "Should the widget differ between 0 and empty, which get then returned as NULL" + }, + "short_labels": { + "name": "Short labels", + "default": false, + "type": "boolean", + "description": "use d/h/m instead of day/hour/minute" + }, + "step" : { + "name": "Step limit", + "default": 'any', + "type": "string", + "description": "Works with the min and max attributes to limit the increments at which a numeric or date-time value can be set." + } + }; + + legacyOptions: string[] = ["data_format","display_format", "hours_per_day", "empty_not_0", "short_labels"]; + + time_formats: {"d":"d","h":"h","m":"m"}; + + // @ts-ignore baseWidget defines node as HTMLElement + node: JQuery; + duration: JQuery; + format: JQuery = null; + + /** + * Constructor + */ + constructor(_parent, _attrs? : WidgetConfig, _child? : object) + { + // Call the inherited constructor + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_date_duration._attributes, _child || {})); + + // Legacy option put percent in with display format + if(this.options.display_format.indexOf("%") != -1) + { + this.options.percent_allowed = true; + this.options.display_format = this.options.display_format.replace("%",""); + } + + // Clean formats + this.options.display_format = this.options.display_format.replace(/[^dhm]/,''); + if(!this.options.display_format) + { + // @ts-ignore + this.options.display_format = this.attributes.display_format["default"]; + } + + // Get translations + this.time_formats = { + "d": this.options.short_labels ? this.egw().lang("d") : this.egw().lang("Days"), + "h": this.options.short_labels ? this.egw().lang("h") : this.egw().lang("Hours"), + "m": this.options.short_labels ? this.egw().lang("m") : this.egw().lang("Minutes") + }, + this.createInputWidget(); + } + + createInputWidget() + { + // Create nodes + this.node = jQuery(document.createElement("span")) + .addClass('et2_date_duration'); + this.duration = jQuery(document.createElement("input")) + .addClass('et2_date_duration') + .attr({type: 'number', size: 3, step:this.options.step, lang: this.egw().preference('number_format')[0] === "," ? "en-150": "en-001"}); + this.node.append(this.duration); + + var self = this; + // seems the 'invalid' event doesn't work in all browsers, eg. FF therefore + // we use focusout event to check the valifdity of input right after user + // enters the value. + this.duration.on('focusout', function() { + if(!(self.duration[0]).checkValidity()) + return self.duration.change(); + }); + } + + /** + * Clientside validation + * + * @param {array} _messages + */ + isValid(_messages) + { + var ok = true; + // if we have a html5 validation error, show it, as this.input.val() will be empty! + if (this.duration && this.duration[0] && + (this.duration[0]).validationMessage && + !(this.duration[0]).validity.stepMismatch) + { + _messages.push((this.duration[0]).validationMessage); + ok = false; + } + return super.isValid(_messages) && ok; + } + + attachToDOM() + { + var node = this.getInputNode(); + if (node) + { + jQuery(node).bind("change.et2_inputWidget", this, function(e) { + e.data.change(this); + }); + } + return et2_DOMWidget.prototype.attachToDOM.apply(this, arguments); + } + + getDOMNode() + { + return this.node[0]; + } + + getInputNode() + { + return this.duration[0]; + } + + /** + * Use id on node, same as DOMWidget + * + * @param {string} _value id to set + */ + set_id(_value) + { + this.id = _value; + + var node = this.getDOMNode(); + if (node) + { + if (_value != "") + { + node.setAttribute("id", this.getInstanceManager().uniqueId+'_'+this.id); + } + else + { + node.removeAttribute("id"); + } + } + } + + set_value(_value) + { + this.options.value = _value; + + var display = this._convert_to_display(parseFloat(_value)); + + // Set display + if(this.duration[0].nodeName == "INPUT") + { + this.duration.val(display.value); + } + else + { + this.duration.text(display.value + " "); + } + + // Set unit as figured for display + if(display.unit != this.options.display_format) + { + if(this.format && this.format.children().length > 1) { + jQuery("option[value='"+display.unit+"']",this.format).attr('selected','selected'); + } + else + { + this.format.text(display.unit ? this.time_formats[display.unit] : ''); + } + } + } + + set_display_format(format) + { + if (format.length <= 1) + { + this.node.remove('select.et2_date_duration'); + this.format = null; + } + this.options.display_format = format; + if((this.format == null || this.format.is('select')) && (this.options.display_format.length <= 1 || this.options.readonly)) + { + if (this.format) + { + this.format.remove(); + } + this.format = jQuery(document.createElement('span')).appendTo(this.node); + } + if(this.options.display_format.length > 1 && !this.options.readonly) + { + if(this.format && !this.format.is('select')) { + this.format.remove(); + this.format = null; + } + if(!this.format) + { + this.format = jQuery(document.createElement("select")) + .addClass('et2_date_duration'); + this.node.append(this.format); + } + + this.format.empty(); + for(var i = 0; i < this.options.display_format.length; i++) { + this.format.append(""); + } + } + else if (this.time_formats[this.options.display_format]) + { + this.format.text(this.time_formats[this.options.display_format]); + } + else + { + this.format.text(this.time_formats["m"]); + } + } + + /** + * Converts the value in data format into value in display format. + * + * @param _value int/float Data in data format + * + * @return Object {value: Value in display format, unit: unit for display} + */ + _convert_to_display(_value) + { + if (_value) + { + // Put value into minutes for further processing + switch(this.options.data_format) + { + case 'd': + _value *= this.options.hours_per_day; + // fall-through + case 'h': + _value *= 60; + break; + } + } + + // Figure out best unit for display + var _unit = this.options.display_format == "d" ? "d" : "h"; + if (this.options.display_format.indexOf('m') > -1 && _value && _value < 60) + { + _unit = 'm'; + } + else if (this.options.display_format.indexOf('d') > -1 && _value >= 60*this.options.hours_per_day) + { + _unit = 'd'; + } + _value = this.options.empty_not_0 && _value === '' || !this.options.empty_not_0 && !_value ? '' : + (_unit == 'm' ? parseInt( _value) : (Math.round((_value / 60.0 / (_unit == 'd' ? this.options.hours_per_day : 1))*100)/100)); + + if(_value === '') _unit = ''; + + // use decimal separator from user prefs + var format = this.egw().preference('number_format'); + var sep = format ? format[0] : '.'; + if (typeof _value == 'string' && format && sep && sep != '.') + { + _value = _value.replace('.',sep); + } + + return {value: _value, unit:_unit}; + } + + /** + * Change displayed value into storage value and return + */ + getValue() + { + var value = this.duration.val().replace(',', '.'); + if(value === '') + { + return this.options.empty_not_0 ? '' : 0; + } + // Put value into minutes for further processing + switch(this.format && this.format.val() ? this.format.val() : this.options.display_format) + { + case 'd': + value *= this.options.hours_per_day; + // fall-through + case 'h': + value *= 60; + break; + } + // Minutes should be an integer. Floating point math. + value = Math.round(value); + + switch(this.options.data_format) + { + case 'd': + value /= this.options.hours_per_day; + // fall-through + case 'h': + value /= 60.0; + break; + } + return value; + } +} +et2_register_widget(et2_date_duration, ["date-duration"]); + +/** + * r/o date-duration + */ +export class et2_date_duration_ro extends et2_date_duration implements et2_IDetachedDOM +{ + createInputWidget() + { + this.node = jQuery(document.createElement("span")); + this.duration = jQuery(document.createElement("span")).appendTo(this.node); + this.format = jQuery(document.createElement("span")).appendTo(this.node); + } + + /** + * Code for implementing et2_IDetachedDOM + * Fast-clonable read-only widget that only deals with DOM nodes, not the widget tree + */ + + /** + * Build a list of attributes which can be set when working in the + * "detached" mode in the _attrs array which is provided + * by the calling code. + * + * @param {array} _attrs array to add further attributes to + */ + getDetachedAttributes(_attrs) + { + _attrs.push("value"); + } + + /** + * Returns an array of DOM nodes. The (relativly) same DOM-Nodes have to be + * passed to the "setDetachedAttributes" function in the same order. + * + * @return {array} + */ + getDetachedNodes() + { + return [this.duration[0], this.format[0]]; + } + + /** + * Sets the given associative attribute->value array and applies the + * attributes to the given DOM-Node. + * + * @param _nodes is an array of nodes which has 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, _values) + { + for(var i = 0; i < _nodes.length; i++) { + // Clear the node + for (var j = _nodes[i].childNodes.length - 1; j >= 0; j--) + { + _nodes[i].removeChild(_nodes[i].childNodes[j]); + } + } + if(typeof _values.value !== 'undefined') + { + _values.value = parseFloat(_values.value); + } + if(_values.value) + { + var display = this._convert_to_display(_values.value); + _nodes[0].appendChild(document.createTextNode(display.value)); + _nodes[1].appendChild(document.createTextNode(display.unit)); + } + } + +} +et2_register_widget(et2_date_duration_ro, ["date-duration_ro"]); + +/** + * et2_date_ro is the readonly implementation of some date widget. + */ +export class et2_date_ro extends et2_valueWidget implements et2_IDetachedDOM +{ + /** + * Ignore all more advanced attributes. + */ + static readonly _attributes: any = { + "value": { + "type": "string" + }, + "type": { + "ignore": false + }, + "data_format": { + "ignore": true, + "description": "Format data is in. This is not used client-side because it's always a timestamp client side." + }, + min: {ignore: true}, + max: {ignore: true}, + year_range: {ignore: true} + }; + + legacyOptions: ["data_format"]; + + /** + * Internal container for working easily with dates + */ + date: Date = new Date(); + value: string = ""; + span: JQuery; + + /** + * Constructor + */ + constructor(_parent, _attrs? : WidgetConfig, _child? : object) + { + // Call the inherited constructor + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_date_ro._attributes, _child || {})); + + this._labelContainer = jQuery(document.createElement("label")) + .addClass("et2_label"); + + this.span = jQuery(document.createElement(this.getType() == "date-since" || this.getType() == "date-time_today" ? "span" : "time")) + .addClass("et2_date_ro et2_label") + .appendTo(this._labelContainer); + + this.setDOMNode(this._labelContainer[0]); + } + + set_value(_value) + { + if(typeof _value == 'undefined') _value = 0; + + this.value = _value; + + if(_value == 0 || _value == null) + { + this.span.attr("datetime", "").text(""); + return; + } + + if(typeof _value == 'string' && _value.match(/(\d{4})-(\d{2})-(\d{2})T(\d{2})\:(\d{2})\:(\d{2})(?:\.\d{3})?(?:Z|[+-](\d{2})\:(\d{2}))/)) + { + this.date = new Date(_value); + this.date = new Date(this.date.valueOf() + (this.date.getTimezoneOffset()*60*1000)); + } + else if(typeof _value == 'string' && (isNaN(_value) || + this.options.data_format && this.options.data_format.substr(0,3) === 'Ymd')) + { + try { + // data_format is not handled server-side for custom-fields in nextmatch + // as parseDateTime requires a separate between date and time, we fix the value here + switch (this.options.data_format) + { + case 'Ymd': + case 'YmdHi': + case 'YmdHis': + _value = _value.substr(0, 4)+'-'+_value.substr(4, 2)+'-'+_value.substr(6, 2)+' '+ + (_value.substr(8, 2) || '00')+':'+(_value.substr(10, 2) || '00')+':'+(_value.substr(12, 2) || '00'); + break; + } + // parseDateTime to handle string PHP: DateTime local date/time format + // @ts-ignore + var parsed = (typeof jQuery.datepicker.parseDateTime("yy-mm-dd","hh:mm:ss", _value) !='undefined')? + // @ts-ignore + jQuery.datepicker.parseDateTime("yy-mm-dd","hh:mm:ss", _value): + // @ts-ignore + jQuery.datepicker.parseDateTime(this.egw().preference('dateformat'),this.egw().preference('timeformat') == '24' ? 'H:i' : 'g:i a', _value); + } + // display unparsable dates as empty + catch(e) { + this.span.attr("datetime", "").text(""); + return; + } + var text = new Date(parsed); + + // Update local variable, but remove the timezone offset that javascript adds + if(parsed) + { + this.date = new Date(text.valueOf() - (text.getTimezoneOffset()*60*1000)); + } + + // JS dates use milliseconds + this.date.setTime(text.valueOf()); + } + else + { + // _value is timestamp in usertime, ready to be used with date() function identical to PHP date() + this.date = _value; + } + var display = this.date.toString(); + + switch(this.getType()) { + case "time_or_date": + case "date-time_today": + // Today - just the time + if(date('Y-m-d', this.date) == date('Y-m-d')) + { + display = date(this.egw().preference('timeformat') == '24' ? 'H:i' : 'g:i a', this.date); + } + else if (this.getType() === "time_or_date") + { + display = date(this.egw().preference('dateformat'), this.date); + } + // Before today - date and time + else + { + display = date(this.egw().preference('dateformat') + " " + + (this.egw().preference('timeformat') == '24' ? 'H:i' : 'g:i a'), this.date); + } + break; + case "date": + display = date(this.egw().preference('dateformat'), this.date); + break; + case "date-timeonly": + display = date(this.egw().preference('timeformat') == '24' ? 'H:i' : 'g:i a', this.date); + break; + case "date-time": + display = date(this.egw().preference('dateformat') + " " + + (this.egw().preference('timeformat') == '24' ? 'H:i' : 'g:i a'), this.date); + break; + case "date-since": + var unit2label = { + 'Y': 'years', + 'm': 'month', + 'd': 'days', + 'H': 'hours', + 'i': 'minutes', + 's': 'seconds' + }; + var unit2s = { + 'Y': 31536000, + 'm': 2628000, + 'd': 86400, + 'H': 3600, + 'i': 60, + 's': 1 + }; + var d = new Date(); + var diff = Math.round(d.valueOf() / 1000) - Math.round(this.date.valueOf()/1000); + display = ''; + + for(var unit in unit2s) + { + var unit_s = unit2s[unit]; + if (diff >= unit_s || unit == 's') + { + display = Math.round(diff/unit_s)+' '+this.egw().lang(unit2label[unit]); + break; + } + } + break; + } + this.span.attr("datetime", date("Y-m-d H:i:s",this.date)).text(display); + } + + set_label(label) + { + // Remove current label + this._labelContainer.contents() + .filter(function(){ return this.nodeType == 3; }).remove(); + + var parts = et2_csvSplit(label, 2, "%s"); + this._labelContainer.prepend(parts[0]); + this._labelContainer.append(parts[1]); + this.label = label; + + // add class if label is empty + this._labelContainer.toggleClass('et2_label_empty', !label || !parts[0]); + } + + /** + * 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 array to add further attributes to + */ + getDetachedAttributes(_attrs) + { + _attrs.push("label", "value","class"); + } + + /** + * Returns an array of DOM nodes. The (relatively) same DOM-Nodes have to be + * passed to the "setDetachedAttributes" function in the same order. + * + * @return {array} + */ + getDetachedNodes() + { + return [this._labelContainer[0], this.span[0]]; + } + + /** + * 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, _values) + { + this._labelContainer = jQuery(_nodes[0]); + this.span = jQuery(_nodes[1]); + + this.set_value(_values["value"]); + if(_values["label"]) + { + this.set_label(_values["label"]); + } + if(_values["class"]) + { + this.span.addClass(_values["class"]); + } + } +} +et2_register_widget(et2_date_ro, ["date_ro", "date-time_ro", "date-since", "date-time_today", "time_or_date", "date-timeonly_ro"]); + + +/** + * Widget for selecting a date range + */ +export class et2_date_range extends et2_inputWidget +{ + static readonly _attributes: any = { + value: { + "type": "any", + "description": "An object with keys 'from' and 'to' for absolute ranges, or a relative range string" + }, + relative: { + name: 'Relative', + type: 'boolean', + description: 'Is the date range relative (this week) or absolute (2016-02-15 - 2016-02-21). This will affect the value returned.' + } + }; + + div: JQuery; + from: et2_date; + to: et2_date; + select: et2_selectbox; + + /** + * Constructor + */ + constructor(_parent, _attrs? : WidgetConfig, _child? : object) + { + // Call the inherited constructor + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_date_range._attributes, _child || {})); + + this.div = jQuery(document.createElement('div')) + .attr({ class:'et2_date_range'}); + + this.from = null; + this.to = null; + this.select = null; + + // Set domid + this.set_id(this.id); + + this.setDOMNode(this.div[0]); + this._createWidget(); + + this.set_relative(this.options.relative || false); + } + + _createWidget() + { + var widget = this; + + this.from = et2_createWidget('date',{ + id: this.id+'[from]', + blur: egw.lang('From'), + onchange() { widget.to.set_min(widget.from.getValue()); } + },this); + this.to = et2_createWidget('date',{ + id: this.id+'[to]', + blur: egw.lang('To'), + onchange() {widget.from.set_max(widget.to.getValue()); } + },this); + this.select = et2_createWidget('select',{ + id: this.id+'[relative]', + select_options: et2_date_range.relative_dates, + empty_label: this.options.blur || 'All' + },this); + this.select.loadingFinished(); + } + + /** + * Function which allows iterating over the complete widget tree. + * Overridden here to avoid problems with children when getting value + * + * @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); + } + } + + /** + * Toggles relative or absolute dates + * + * @param {boolean} _value + */ + set_relative(_value) + { + this.options.relative = _value; + if(this.options.relative) + { + jQuery(this.from.getDOMNode()).hide(); + jQuery(this.to.getDOMNode()).hide(); + } + else + { + jQuery(this.select.getDOMNode()).hide(); + } + } + + set_value(value) + { + // @ts-ignore + if(!value || typeof value == 'null') + { + this.select.set_value(''); + this.from.set_value(null); + this.to.set_value(null); + } + + // Relative + if(value && typeof value === 'string') + { + this._set_relative_value(value); + + } + else if(value && typeof value.from === 'undefined' && value[0]) + { + value = { + from: value[0], + to: value[1] || new Date().valueOf()/1000 + }; + } + else if (value && value.from && value.to) + { + this.from.set_value(value.from); + this.to.set_value(value.to); + } + } + + getValue() + { + return this.options.relative ? + this.select.getValue() : + { from: this.from.getValue(), to: this.to.getValue() }; + } + + _set_relative_value(_value) + { + if(this.options.relative) + { + jQuery(this.select.getDOMNode()).show(); + } + // Show description + this.select.set_value(_value); + + var tempDate = new Date(); + var today = new Date(tempDate.getFullYear(), tempDate.getMonth(), tempDate.getDate(),0,-tempDate.getTimezoneOffset(),0); + + // Use strings to avoid references + this.from.set_value(today.toJSON()); + this.to.set_value(today.toJSON()); + + var relative = null; + for(var index in et2_date_range.relative_dates) + { + if(et2_date_range.relative_dates[index].value === _value) + { + relative = et2_date_range.relative_dates[index]; + break; + } + } + if(relative) + { + var dates = ["from","to"]; + var value = today.toJSON(); + for(var i = 0; i < dates.length; i++) + { + var date = dates[i]; + if(typeof relative[date] == "function") + { + value = relative[date](new Date(value)); + } + else + { + value = this[date]._relativeDate(relative[date]); + } + this[date].set_value(value); + } + } + } + + // Class Constants + static readonly relative_dates = [ + // Start and end are relative offsets, see et2_date.set_min() + // or Date objects + { + value: 'Today', + label: 'Today', + from(date) {return date;}, + to(date) {return date;} + }, + { + label: 'Yesterday', + value: 'Yesterday', + from(date) { + date.setUTCDate(date.getUTCDate() - 1); + return date; + }, + to: '' + }, + { + label: 'This week', + value: 'This week', + from(date) {return egw.week_start(date);}, + to(date) { + date.setUTCDate(date.getUTCDate() + 6); + return date; + } + }, + { + label: 'Last week', + value: 'Last week', + from(date) { + var d = egw.week_start(date); + d.setUTCDate(d.getUTCDate() - 7); + return d; + }, + to(date) { + date.setUTCDate(date.getUTCDate() + 6); + return date; + } + }, + { + label: 'This month', + value: 'This month', + from(date) + { + date.setUTCDate(1); + return date; + }, + to(date) + { + date.setUTCMonth(date.getUTCMonth()+1); + date.setUTCDate(0); + return date; + } + }, + { + label: 'Last month', + value: 'Last month', + from(date) + { + date.setUTCMonth(date.getUTCMonth() - 1); + date.setUTCDate(1); + return date; + }, + to(date) + { + date.setUTCMonth(date.getUTCMonth()+1); + date.setUTCDate(0); + return date; + } + }, + { + label: 'Last 3 months', + value: 'Last 3 months', + from(date) + { + date.setUTCMonth(date.getUTCMonth() - 2); + date.setUTCDate(1); + return date; + }, + to(date) + { + date.setUTCMonth(date.getUTCMonth()+3); + date.setUTCDate(0); + return date; + } + }, + /* + 'This quarter'=> array(0,0,0,0, 0,0,0,0), // Just a marker, needs special handling + 'Last quarter'=> array(0,-4,0,0, 0,-4,0,0), // Just a marker + */ + { + label: 'This year', + value: 'This year', + from(d) { + d.setUTCMonth(0); + d.setUTCDate(1); + return d; + }, + to(d) { + d.setUTCMonth(11); + d.setUTCDate(31); + return d; + } + }, + { + label: 'Last year', + value: 'Last year', + from(d) { + d.setUTCMonth(0); + d.setUTCDate(1); + d.setUTCYear(d.getUTCYear() - 1); + return d; + }, + to(d) { + d.setUTCMonth(11); + d.setUTCDate(31); + d.setUTCYear(d.getUTCYear() - 1); + return d; + } + } + /* Still needed? + '2 years ago' => array(-2,0,0,0, -1,0,0,0), + '3 years ago' => array(-3,0,0,0, -2,0,0,0), + */ + ]; +} +et2_register_widget(et2_date_range, ["date-range"]); diff --git a/api/js/etemplate/et2_widget_description.js b/api/js/etemplate/et2_widget_description.js index 83cfa4f299..c90be913d9 100644 --- a/api/js/etemplate/et2_widget_description.js +++ b/api/js/etemplate/et2_widget_description.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - JS Description object * @@ -6,410 +7,360 @@ * @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 __()); + }; +})(); +var _a; +Object.defineProperty(exports, "__esModule", { value: true }); /*egw:uses - /vendor/bower-asset/jquery/dist/jquery.js; - et2_core_baseWidget; - expose; + /vendor/bower-asset/jquery/dist/jquery.js; + et2_core_baseWidget; + expose; */ - +require("./et2_core_common"); +var et2_core_inheritance_1 = require("./et2_core_inheritance"); +var et2_core_widget_1 = require("./et2_core_widget"); +var et2_core_baseWidget_1 = require("./et2_core_baseWidget"); +require("./et2_types"); /** * Class which implements the "description" XET-Tag - * - * @augments et2_baseWidget */ -var et2_description = (function(){ "use strict"; return expose(et2_baseWidget.extend([et2_IDetachedDOM], -{ - attributes: { - "label": { - "name": "Label", - "default": "", - "type": "string", - "description": "The label is displayed by default in front (for radiobuttons behind) each widget (if not empty). If you want to specify a different position, use a '%s' in the label, which gets replaced by the widget itself. Eg. '%s Name' to have the label Name behind a checkbox. The label can contain variables, as descript for name. If the label starts with a '@' it is replaced by the value of the content-array at this index (with the '@'-removed and after expanding the variables).", - "translate": true - }, - "value": { - "name": "Value", - "type": "string", - "description": "Displayed text", - "translate": "!no_lang", - "default": "" - }, - - /** - * Options converted from the "options"-attribute. - */ - "font_style": { - "name": "Font Style", - "type": "string", - "description": "Style may be a compositum of \"b\" and \"i\" which " + - " renders the text bold and/or italic." - }, - "href": { - "name": "Link URL", - "type": "string", - "description": "Link URL, empty if you don't wan't to display a link." - }, - "activate_links": { - "name": "Replace URLs", - "type": "boolean", - "default": false, - "description": "If set, URLs in the text are automatically replaced " + - "by links" - }, - "for": { - "name": "Label for widget", - "type": "string", - "description": "Marks the text as label for the given widget." - }, - "extra_link_target": { - "name": "Link target", - "type": "string", - "default": "_browser", - "description": "Link target for href attribute" - }, - "extra_link_popup": { - "name": "Popup", - "type": "string", - "description": "widthxheight, if popup should be used, eg. 640x480" - }, - "expose_view":{ - name: "Expose view", - type: "boolean", - default: false, - description: "Clicking on description with href value would popup an expose view, and will show content referenced by href." - }, - mime:{ - name: "Mime type", - type: "string", - default: '', - description: "Mime type of the registered link" - }, - mime_data:{ - name: "Mime data", - type: "string", - default: '', - description: "hash for data stored on service-side with egw_link::(get|set)_data()" - }, - hover_action: { - "name": "hover action", - "type": "js", - "default": et2_no_init, - "description": "JS code which is executed when clicking on action button. This action is explicitly for attached nodes, like in nm." - }, - hover_action_title: { - "name": "hover action title", - "type": "string", - "default": "Edit", - "description": "Text to show as tooltip of defined action" - } - - }, - - legacyOptions: ["font_style", "href", "activate_links", "for", - "extra_link_target", "extra_link_popup", "statustext"], - - /** - * Constructor - * - * @memberOf et2_description - */ - init: function() { - this._super.apply(this, arguments); - - // Create the span/label tag which contains the label text - this.span = jQuery(document.createElement(this.options["for"] ? "label" : "span")) - .addClass("et2_label"); - - - - et2_insertLinkText(this._parseText(this.options.value), this.span[0], - this.options.href ? this.options.extra_link_target : '_blank'); - - this.setDOMNode(this.span[0]); - }, - - transformAttributes: function(_attrs) { - this._super.apply(this, arguments); - - if (this.id) - { - var val = this.getArrayMgr("content").getEntry(this.id); - - if (val) - { - _attrs["value"] = val; - } - } - }, - - doLoadingFinished: function() { - - this._super.apply(this, arguments); - - // Get the real id of the 'for' widget - var for_widget = null; - if (this.options["for"] && ( - (for_widget = this.getParent().getWidgetById(this.options.for)) || - (for_widget = this.getRoot().getWidgetById(this.options.for)) - ) && for_widget && for_widget.id) - { - if(for_widget.dom_id) - { - this.span.attr("for", for_widget.dom_id); - } - else - { - // Target widget is not done yet, need to wait - var tab_deferred = jQuery.Deferred(); - window.setTimeout(function() { - this.span.attr("for", for_widget.dom_id); - tab_deferred.resolve(); - }.bind(this),0); - - return tab_deferred.promise(); - } - } - return true; - }, - - set_label: function(_value) { - // Abort if ther was no change in the label - if (_value == this.label) - { - return; - } - - if (_value) - { - // Create the label container if it didn't exist yet - if (this._labelContainer == null) - { - this._labelContainer = jQuery(document.createElement("label")) - .addClass("et2_label"); - this.getSurroundings().insertDOMNode(this._labelContainer[0]); - } - - // Clear the label container. - this._labelContainer.empty(); - - // Create the placeholder element and set it - var ph = document.createElement("span"); - this.getSurroundings().setWidgetPlaceholder(ph); - - // Split the label at the "%s" - var parts = et2_csvSplit(_value, 2, "%s"); - - // Update the content of the label container - for (var i = 0; i < parts.length; i++) - { - if (parts[i]) - { - this._labelContainer.append(document.createTextNode(parts[i])); - } - if (i == 0) - { - this._labelContainer.append(ph); - } - } - - // add class if label is empty - this._labelContainer.toggleClass('et2_label_empty', !_value || !parts[0]); - } - else - { - // Delete the labelContainer from the surroundings object - if (this._labelContainer) - { - this.getSurroundings().removeDOMNode(this._labelContainer[0]); - } - this._labelContainer = null; - } - - // Update the surroundings in order to reflect the change in the label - this.getSurroundings().update(); - - // Copy the given value - this.label = _value; - }, - /** - * Function to get media content to feed the expose - * @param {type} _value - * @returns {Array|Array.getMedia.mediaContent} - */ - getMedia: function (_value) - { - var base_url = egw.webserverUrl.match(/^\//,'ig')?egw(window).window.location.origin :''; - var mediaContent = []; - if (_value) - { - mediaContent = [{ - title: this.options.label, - href: base_url + _value, - type: this.options.type + "/*", - thumbnail: base_url + _value - }]; - if (_value.match(/\/webdav.php/,'ig')) mediaContent[0]["download_href"] = base_url + _value + '?download'; - } - return mediaContent; - }, - set_value: function(_value) { - if (!_value) _value = ""; - if (!this.options.no_lang) _value = this.egw().lang(_value); - if (this.options.value && (this.options.value+"").indexOf('%s') != -1) - { - _value = this.options.value.replace(/%s/g, _value); - } - et2_insertLinkText(this._parseText(_value), - this.span[0], - this.options.href ? this.options.extra_link_target : '_blank' - ); - // Add hover action button (Edit) - if (this.options.hover_action) - { - this._build_hover_action(); - } - if(this.options.extra_link_popup || this.options.mime) - { - var href = this.options.href; - var mime_data = this.options.mime_data; - var self= this; - var $span = this.options.mime_data? jQuery(this.span): jQuery('a',this.span); - $span.click(function(e) { - if (self.options.expose_view && typeof self.options.mime !='undefined' && self.options.mime.match(self.mime_regexp,'ig')) - { - self._init_blueimp_gallery(e, href); - } - else - { - egw(window).open_link(mime_data || href, self.options.extra_link_target, self.options.extra_link_popup, null, null, self.options.mime); - } - e.preventDefault(); - return false; - }); - } - }, - - _parseText: function(_value) { - if (this.options.href) - { - var href = this.options.href; - if (href.indexOf('/')==-1 && href.split('.').length >= 3 && - !(href.indexOf('mailto:')!=-1 || href.indexOf('://') != -1 || href.indexOf('javascript:') != -1) - ) - { - href = "/index.php?menuaction="+href; - } - if (href.charAt(0) == '/') // link relative to eGW - { - href = egw.link(href); - } - return [{ - "href": href, - "text": _value - }]; - } - else if (this.options.activate_links) - { - return et2_activateLinks(_value); - } - else - { - return [_value]; - } - }, - - set_font_style: function(_value) { - this.font_style = _value; - - this.span.toggleClass("et2_bold", _value.indexOf("b") >= 0); - this.span.toggleClass("et2_italic", _value.indexOf("i") >= 0); - }, - - /** - * Code for implementing et2_IDetachedDOM - * - * @param {array} _attrs - */ - getDetachedAttributes: function(_attrs) - { - _attrs.push("value", "class", "href"); - }, - - getDetachedNodes: function() - { - return [this.span[0]]; - }, - - setDetachedAttributes: function(_nodes, _values, _data) - { - // Update the properties - var updateLink = false; - if (typeof _values["href"] != "undefined") - { - updateLink = true; - this.options.href = _values["href"]; - } - - if (typeof _values["value"] != "undefined" || (updateLink && (_values["value"] || this.options.value))) - { - this.span = jQuery(_nodes[0]); - this.set_value(_values["value"]); - } - - if (typeof _values["class"] != "undefined") - { - _nodes[0].setAttribute("class", _values["class"]); - } - - // Add hover action button (Edit), _data is nm's row data - if (this.options.hover_action) - { - this._build_hover_action(_data); - } - }, - - /** - * Builds button for hover action - * @param {object} _data - */ - _build_hover_action: function(_data) - { - var content = _data && _data.content ? _data.content: undefined; - var widget = this; - this.span.off().on('mouseenter', jQuery.proxy(function(event) { - event.stopImmediatePropagation(); - var self = this; - this.span.tooltip({ - items: 'span.et2_label', - position: {my:"right top", at:"left top", collision:"flipfit"}, - tooltipClass: "et2_email_popup", - content: function() - { - return jQuery('') - .on('click', function() { - widget.options.hover_action.call(self, self.widget, content); - }); - }, - close: function( event, ui ) - { - ui.tooltip.hover( - function () { - jQuery(this).stop(true).fadeTo(400, 1); - }, - function () { - jQuery(this).fadeOut("400", function(){ jQuery(this).remove();}); - } - ); - } - }) - .tooltip("open"); - }, {widget: this, span: this.span})); - } -}));}).call(this); -et2_register_widget(et2_description, ["description", "label"]); - +var et2_description = /** @class */ (function (_super) { + __extends(et2_description, _super); + function et2_description() { + return _super !== null && _super.apply(this, arguments) || this; + } + return et2_description; +}(expose((_a = /** @class */ (function (_super) { + __extends(et2_description, _super); + /** + * Constructor + */ + function et2_description(_parent, _attrs, _child) { + var _this = + // Call the inherited constructor + _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_description._attributes, _child || {})) || this; + _this.legacyOptions = ["font_style", "href", "activate_links", "for", + "extra_link_target", "extra_link_popup", "statustext"]; + _this._labelContainer = null; + // Create the span/label tag which contains the label text + _this.span = jQuery(document.createElement(_this.options["for"] ? "label" : "span")) + .addClass("et2_label"); + et2_insertLinkText(_this._parseText(_this.options.value), _this.span[0], _this.options.href ? _this.options.extra_link_target : '_blank'); + _this.setDOMNode(_this.span[0]); + return _this; + } + et2_description.prototype.transformAttributes = function (_attrs) { + _super.prototype.transformAttributes.call(this, _attrs); + if (this.id) { + var val = this.getArrayMgr("content").getEntry(this.id); + if (val) { + _attrs["value"] = val; + } + } + }; + et2_description.prototype.doLoadingFinished = function () { + _super.prototype.doLoadingFinished.call(this); + // Get the real id of the 'for' widget + var for_widget = null; + if (this.options["for"] && ((for_widget = this.getParent().getWidgetById(this.options.for)) || + (for_widget = this.getRoot().getWidgetById(this.options.for))) && for_widget && for_widget.id) { + if (for_widget.dom_id) { + this.span.attr("for", for_widget.dom_id); + } + else { + // Target widget is not done yet, need to wait + var tab_deferred = jQuery.Deferred(); + window.setTimeout(function () { + this.span.attr("for", for_widget.dom_id); + tab_deferred.resolve(); + }.bind(this), 0); + return tab_deferred.promise(); + } + } + return true; + }; + et2_description.prototype.set_label = function (_value) { + // Abort if ther was no change in the label + if (_value == this.label) { + return; + } + if (_value) { + // Create the label container if it didn't exist yet + if (this._labelContainer == null) { + this._labelContainer = jQuery(document.createElement("label")) + .addClass("et2_label"); + this.getSurroundings().insertDOMNode(this._labelContainer[0]); + } + // Clear the label container. + this._labelContainer.empty(); + // Create the placeholder element and set it + var ph = document.createElement("span"); + this.getSurroundings().setWidgetPlaceholder(ph); + // Split the label at the "%s" + var parts = et2_csvSplit(_value, 2, "%s"); + // Update the content of the label container + for (var i = 0; i < parts.length; i++) { + if (parts[i]) { + this._labelContainer.append(document.createTextNode(parts[i])); + } + if (i == 0) { + this._labelContainer.append(ph); + } + } + // add class if label is empty + this._labelContainer.toggleClass('et2_label_empty', !_value || !parts[0]); + } + else { + // Delete the labelContainer from the surroundings object + if (this._labelContainer) { + this.getSurroundings().removeDOMNode(this._labelContainer[0]); + } + this._labelContainer = null; + } + // Update the surroundings in order to reflect the change in the label + this.getSurroundings().update(); + // Copy the given value + this.label = _value; + }; + /** + * Function to get media content to feed the expose + * @param {type} _value + * @returns {Array|Array.getMedia.mediaContent} + */ + et2_description.prototype.getMedia = function (_value) { + var base_url = egw.webserverUrl.match(new RegExp(/^\//, 'ig')) ? egw(window).window.location.origin : ''; + var mediaContent = []; + if (_value) { + mediaContent = [{ + title: this.options.label, + href: base_url + _value, + type: this.options.type + "/*", + thumbnail: base_url + _value + }]; + if (_value.match(/\/webdav.php/, 'ig')) + mediaContent[0]["download_href"] = base_url + _value + '?download'; + } + return mediaContent; + }; + et2_description.prototype.set_value = function (_value) { + if (!_value) + _value = ""; + if (!this.options.no_lang) + _value = this.egw().lang(_value); + if (this.options.value && (this.options.value + "").indexOf('%s') != -1) { + _value = this.options.value.replace(/%s/g, _value); + } + et2_insertLinkText(this._parseText(_value), this.span[0], this.options.href ? this.options.extra_link_target : '_blank'); + // Add hover action button (Edit) + if (this.options.hover_action) { + this._build_hover_action(); + } + if (this.options.extra_link_popup || this.options.mime) { + var href = this.options.href; + var mime_data = this.options.mime_data; + var self = this; + var $span = this.options.mime_data ? jQuery(this.span) : jQuery('a', this.span); + $span.click(function (e) { + if (self.options.expose_view && typeof self.options.mime != 'undefined' && self.options.mime.match(self.mime_regexp, 'ig')) { + self._init_blueimp_gallery(e, href); + } + else { + egw(window).open_link(mime_data || href, self.options.extra_link_target, self.options.extra_link_popup, null, null, self.options.mime); + } + e.preventDefault(); + return false; + }); + } + }; + et2_description.prototype._parseText = function (_value) { + if (this.options.href) { + var href = this.options.href; + if (href.indexOf('/') == -1 && href.split('.').length >= 3 && + !(href.indexOf('mailto:') != -1 || href.indexOf('://') != -1 || href.indexOf('javascript:') != -1)) { + href = "/index.php?menuaction=" + href; + } + if (href.charAt(0) == '/') // link relative to eGW + { + href = egw.link(href); + } + return [{ + "href": href, + "text": _value + }]; + } + else if (this.options.activate_links) { + return et2_activateLinks(_value); + } + else { + return [_value]; + } + }; + et2_description.prototype.set_font_style = function (_value) { + this.font_style = _value; + this.span.toggleClass("et2_bold", _value.indexOf("b") >= 0); + this.span.toggleClass("et2_italic", _value.indexOf("i") >= 0); + }; + /** + * Code for implementing et2_IDetachedDOM + * + * @param {array} _attrs + */ + et2_description.prototype.getDetachedAttributes = function (_attrs) { + _attrs.push("value", "class", "href"); + }; + et2_description.prototype.getDetachedNodes = function () { + return [this.span[0]]; + }; + et2_description.prototype.setDetachedAttributes = function (_nodes, _values, _data) { + // Update the properties + var updateLink = false; + if (typeof _values["href"] != "undefined") { + updateLink = true; + this.options.href = _values["href"]; + } + if (typeof _values["value"] != "undefined" || (updateLink && (_values["value"] || this.options.value))) { + this.span = jQuery(_nodes[0]); + this.set_value(_values["value"]); + } + if (typeof _values["class"] != "undefined") { + _nodes[0].setAttribute("class", _values["class"]); + } + // Add hover action button (Edit), _data is nm's row data + if (this.options.hover_action) { + this._build_hover_action(_data); + } + }; + /** + * Builds button for hover action + * @param {object} _data + */ + et2_description.prototype._build_hover_action = function (_data) { + var content = _data && _data.content ? _data.content : undefined; + var widget = this; + this.span.off().on('mouseenter', jQuery.proxy(function (event) { + event.stopImmediatePropagation(); + var self = this; + this.span.tooltip({ + items: 'span.et2_label', + position: { my: "right top", at: "left top", collision: "flipfit" }, + tooltipClass: "et2_email_popup", + content: function () { + return jQuery('') + .on('click', function () { + widget.options.hover_action.call(self, self.widget, content); + }); + }, + close: function (event, ui) { + ui.tooltip.hover(function () { + jQuery(this).stop(true).fadeTo(400, 1); + }, function () { + jQuery(this).fadeOut("400", function () { jQuery(this).remove(); }); + }); + } + }) + .tooltip("open"); + }, { widget: this, span: this.span })); + }; + return et2_description; + }(et2_core_baseWidget_1.et2_baseWidget)), + _a._attributes = { + "label": { + "name": "Label", + "default": "", + "type": "string", + "description": "The label is displayed by default in front (for radiobuttons behind) each widget (if not empty). If you want to specify a different position, use a '%s' in the label, which gets replaced by the widget itself. Eg. '%s Name' to have the label Name behind a checkbox. The label can contain variables, as descript for name. If the label starts with a '@' it is replaced by the value of the content-array at this index (with the '@'-removed and after expanding the variables).", + "translate": true + }, + "value": { + "name": "Value", + "type": "string", + "description": "Displayed text", + "translate": "!no_lang", + "default": "" + }, + /** + * Options converted from the "options"-attribute. + */ + "font_style": { + "name": "Font Style", + "type": "string", + "description": "Style may be a compositum of \"b\" and \"i\" which " + + " renders the text bold and/or italic." + }, + "href": { + "name": "Link URL", + "type": "string", + "description": "Link URL, empty if you don't wan't to display a link." + }, + "activate_links": { + "name": "Replace URLs", + "type": "boolean", + "default": false, + "description": "If set, URLs in the text are automatically replaced " + + "by links" + }, + "for": { + "name": "Label for widget", + "type": "string", + "description": "Marks the text as label for the given widget." + }, + "extra_link_target": { + "name": "Link target", + "type": "string", + "default": "_browser", + "description": "Link target for href attribute" + }, + "extra_link_popup": { + "name": "Popup", + "type": "string", + "description": "widthxheight, if popup should be used, eg. 640x480" + }, + "expose_view": { + name: "Expose view", + type: "boolean", + default: false, + description: "Clicking on description with href value would popup an expose view, and will show content referenced by href." + }, + mime: { + name: "Mime type", + type: "string", + default: '', + description: "Mime type of the registered link" + }, + mime_data: { + name: "Mime data", + type: "string", + default: '', + description: "hash for data stored on service-side with egw_link::(get|set)_data()" + }, + hover_action: { + "name": "hover action", + "type": "js", + "default": et2_no_init, + "description": "JS code which is executed when clicking on action button. This action is explicitly for attached nodes, like in nm." + }, + hover_action_title: { + "name": "hover action title", + "type": "string", + "default": "Edit", + "description": "Text to show as tooltip of defined action" + } + }, + _a)))); +exports.et2_description = et2_description; +; +et2_core_widget_1.et2_register_widget(et2_description, ["description", "label"]); +//# sourceMappingURL=et2_widget_description.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_description.ts b/api/js/etemplate/et2_widget_description.ts new file mode 100644 index 0000000000..c3cd63a0b3 --- /dev/null +++ b/api/js/etemplate/et2_widget_description.ts @@ -0,0 +1,427 @@ +/** + * EGroupware eTemplate2 - JS Description object + * + * @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 + /vendor/bower-asset/jquery/dist/jquery.js; + et2_core_baseWidget; + expose; +*/ + +import './et2_core_common'; +import {ClassWithAttributes} from "./et2_core_inheritance"; +import {et2_register_widget, WidgetConfig} from "./et2_core_widget"; +import {et2_baseWidget} from './et2_core_baseWidget' +import './et2_types'; + +/** + * Class which implements the "description" XET-Tag + */ +export class et2_description extends expose(class et2_description extends et2_baseWidget implements et2_IDetachedDOM, et2_IExposable +{ + static readonly _attributes : any = { + "label": { + "name": "Label", + "default": "", + "type": "string", + "description": "The label is displayed by default in front (for radiobuttons behind) each widget (if not empty). If you want to specify a different position, use a '%s' in the label, which gets replaced by the widget itself. Eg. '%s Name' to have the label Name behind a checkbox. The label can contain variables, as descript for name. If the label starts with a '@' it is replaced by the value of the content-array at this index (with the '@'-removed and after expanding the variables).", + "translate": true + }, + "value": { + "name": "Value", + "type": "string", + "description": "Displayed text", + "translate": "!no_lang", + "default": "" + }, + + /** + * Options converted from the "options"-attribute. + */ + "font_style": { + "name": "Font Style", + "type": "string", + "description": "Style may be a compositum of \"b\" and \"i\" which " + + " renders the text bold and/or italic." + }, + "href": { + "name": "Link URL", + "type": "string", + "description": "Link URL, empty if you don't wan't to display a link." + }, + "activate_links": { + "name": "Replace URLs", + "type": "boolean", + "default": false, + "description": "If set, URLs in the text are automatically replaced " + + "by links" + }, + "for": { + "name": "Label for widget", + "type": "string", + "description": "Marks the text as label for the given widget." + }, + "extra_link_target": { + "name": "Link target", + "type": "string", + "default": "_browser", + "description": "Link target for href attribute" + }, + "extra_link_popup": { + "name": "Popup", + "type": "string", + "description": "widthxheight, if popup should be used, eg. 640x480" + }, + "expose_view":{ + name: "Expose view", + type: "boolean", + default: false, + description: "Clicking on description with href value would popup an expose view, and will show content referenced by href." + }, + mime:{ + name: "Mime type", + type: "string", + default: '', + description: "Mime type of the registered link" + }, + mime_data:{ + name: "Mime data", + type: "string", + default: '', + description: "hash for data stored on service-side with egw_link::(get|set)_data()" + }, + hover_action: { + "name": "hover action", + "type": "js", + "default": et2_no_init, + "description": "JS code which is executed when clicking on action button. This action is explicitly for attached nodes, like in nm." + }, + hover_action_title: { + "name": "hover action title", + "type": "string", + "default": "Edit", + "description": "Text to show as tooltip of defined action" + } + + }; + + legacyOptions: string[] = ["font_style", "href", "activate_links", "for", + "extra_link_target", "extra_link_popup", "statustext"]; + + span: JQuery; + label: string; + private _labelContainer: JQuery = null; + font_style: string; + mime_regexp: any; + + /** + * Constructor + */ + constructor(_parent, _attrs? : WidgetConfig, _child? : object) + { + // Call the inherited constructor + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_description._attributes, _child || {})); + + // Create the span/label tag which contains the label text + this.span = jQuery(document.createElement(this.options["for"] ? "label" : "span")) + .addClass("et2_label"); + + et2_insertLinkText(this._parseText(this.options.value), this.span[0], + this.options.href ? this.options.extra_link_target : '_blank'); + + this.setDOMNode(this.span[0]); + } + + transformAttributes(_attrs) + { + super.transformAttributes(_attrs); + + if (this.id) + { + var val = this.getArrayMgr("content").getEntry(this.id); + + if (val) + { + _attrs["value"] = val; + } + } + } + + doLoadingFinished() + { + super.doLoadingFinished(); + + // Get the real id of the 'for' widget + var for_widget = null; + if (this.options["for"] && ( + (for_widget = this.getParent().getWidgetById(this.options.for)) || + (for_widget = this.getRoot().getWidgetById(this.options.for)) + ) && for_widget && for_widget.id) + { + if(for_widget.dom_id) + { + this.span.attr("for", for_widget.dom_id); + } + else + { + // Target widget is not done yet, need to wait + var tab_deferred = jQuery.Deferred(); + window.setTimeout(function() { + this.span.attr("for", for_widget.dom_id); + tab_deferred.resolve(); + }.bind(this),0); + + return tab_deferred.promise(); + } + } + return true; + } + + set_label(_value) + { + // Abort if ther was no change in the label + if (_value == this.label) + { + return; + } + + if (_value) + { + // Create the label container if it didn't exist yet + if (this._labelContainer == null) + { + this._labelContainer = jQuery(document.createElement("label")) + .addClass("et2_label"); + this.getSurroundings().insertDOMNode(this._labelContainer[0]); + } + + // Clear the label container. + this._labelContainer.empty(); + + // Create the placeholder element and set it + var ph = document.createElement("span"); + this.getSurroundings().setWidgetPlaceholder(ph); + + // Split the label at the "%s" + var parts = et2_csvSplit(_value, 2, "%s"); + + // Update the content of the label container + for (var i = 0; i < parts.length; i++) + { + if (parts[i]) + { + this._labelContainer.append(document.createTextNode(parts[i])); + } + if (i == 0) + { + this._labelContainer.append(ph); + } + } + + // add class if label is empty + this._labelContainer.toggleClass('et2_label_empty', !_value || !parts[0]); + } + else + { + // Delete the labelContainer from the surroundings object + if (this._labelContainer) + { + this.getSurroundings().removeDOMNode(this._labelContainer[0]); + } + this._labelContainer = null; + } + + // Update the surroundings in order to reflect the change in the label + this.getSurroundings().update(); + + // Copy the given value + this.label = _value; + } + + /** + * Function to get media content to feed the expose + * @param {type} _value + * @returns {Array|Array.getMedia.mediaContent} + */ + getMedia(_value) + { + let base_url = egw.webserverUrl.match(new RegExp(/^\//,'ig'))?egw(window).window.location.origin :''; + let mediaContent = []; + if (_value) + { + mediaContent = [{ + title: this.options.label, + href: base_url + _value, + type: this.options.type + "/*", + thumbnail: base_url + _value + }]; + if (_value.match(/\/webdav.php/,'ig')) mediaContent[0]["download_href"] = base_url + _value + '?download'; + } + return mediaContent; + } + + set_value(_value) + { + if (!_value) _value = ""; + if (!this.options.no_lang) _value = this.egw().lang(_value); + if (this.options.value && (this.options.value+"").indexOf('%s') != -1) + { + _value = this.options.value.replace(/%s/g, _value); + } + et2_insertLinkText(this._parseText(_value), + this.span[0], + this.options.href ? this.options.extra_link_target : '_blank' + ); + // Add hover action button (Edit) + if (this.options.hover_action) + { + this._build_hover_action(); + } + if(this.options.extra_link_popup || this.options.mime) + { + var href = this.options.href; + var mime_data = this.options.mime_data; + var self= this; + var $span = this.options.mime_data? jQuery(this.span): jQuery('a',this.span); + $span.click(function(e) { + if (self.options.expose_view && typeof self.options.mime !='undefined' && self.options.mime.match(self.mime_regexp,'ig')) + { + self._init_blueimp_gallery(e, href); + } + else + { + egw(window).open_link(mime_data || href, self.options.extra_link_target, self.options.extra_link_popup, null, null, self.options.mime); + } + e.preventDefault(); + return false; + }); + } + } + + _parseText(_value) + { + if (this.options.href) + { + var href = this.options.href; + if (href.indexOf('/')==-1 && href.split('.').length >= 3 && + !(href.indexOf('mailto:')!=-1 || href.indexOf('://') != -1 || href.indexOf('javascript:') != -1) + ) + { + href = "/index.php?menuaction="+href; + } + if (href.charAt(0) == '/') // link relative to eGW + { + href = egw.link(href); + } + return [{ + "href": href, + "text": _value + }]; + } + else if (this.options.activate_links) + { + return et2_activateLinks(_value); + } + else + { + return [_value]; + } + } + + set_font_style(_value) + { + this.font_style = _value; + + this.span.toggleClass("et2_bold", _value.indexOf("b") >= 0); + this.span.toggleClass("et2_italic", _value.indexOf("i") >= 0); + } + + /** + * Code for implementing et2_IDetachedDOM + * + * @param {array} _attrs + */ + getDetachedAttributes(_attrs) + { + _attrs.push("value", "class", "href"); + } + + getDetachedNodes() + { + return [this.span[0]]; + } + + setDetachedAttributes(_nodes, _values, _data?) + { + // Update the properties + var updateLink = false; + if (typeof _values["href"] != "undefined") + { + updateLink = true; + this.options.href = _values["href"]; + } + + if (typeof _values["value"] != "undefined" || (updateLink && (_values["value"] || this.options.value))) + { + this.span = jQuery(_nodes[0]); + this.set_value(_values["value"]); + } + + if (typeof _values["class"] != "undefined") + { + _nodes[0].setAttribute("class", _values["class"]); + } + + // Add hover action button (Edit), _data is nm's row data + if (this.options.hover_action) + { + this._build_hover_action(_data); + } + } + + /** + * Builds button for hover action + * @param {object} _data + */ + _build_hover_action(_data?) + { + var content = _data && _data.content ? _data.content: undefined; + var widget = this; + this.span.off().on('mouseenter', jQuery.proxy(function(event) { + event.stopImmediatePropagation(); + var self = this; + this.span.tooltip({ + items: 'span.et2_label', + position: {my:"right top", at:"left top", collision:"flipfit"}, + tooltipClass: "et2_email_popup", + content() + { + return jQuery('') + .on('click', function() { + widget.options.hover_action.call(self, self.widget, content); + }); + }, + close( event, ui ) + { + ui.tooltip.hover( + function () { + jQuery(this).stop(true).fadeTo(400, 1); + }, + function () { + jQuery(this).fadeOut("400", function(){ jQuery(this).remove();}); + } + ); + } + }) + .tooltip("open"); + }, {widget: this, span: this.span})); + } +}){}; +et2_register_widget(et2_description, ["description", "label"]); diff --git a/api/js/etemplate/et2_widget_dialog.js b/api/js/etemplate/et2_widget_dialog.js index b7af0f6e6c..7a0d541bed 100644 --- a/api/js/etemplate/et2_widget_dialog.js +++ b/api/js/etemplate/et2_widget_dialog.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - JS Dialog Widget class * @@ -8,12 +9,28 @@ * @copyright Nathan Gray 2013 * @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_widget; - /vendor/bower-asset/jquery-ui/jquery-ui.js; + /vendor/bower-asset/jquery-ui/jquery-ui.js; */ - +var et2_core_widget_1 = require("./et2_core_widget"); +var et2_core_widget_2 = require("./et2_core_widget"); +var et2_widget_button_1 = require("./et2_widget_button"); +var et2_core_inheritance_1 = require("./et2_core_inheritance"); /** * A common dialog widget that makes it easy to imform users or prompt for information. * @@ -87,799 +104,701 @@ * @augments et2_widget * @see http://api.jqueryui.com/dialog/ */ -var et2_dialog = (function(){ "use strict"; return et2_widget.extend( -{ - attributes: { - callback: { - name: "Callback", - type: "js", - description: "Callback function is called with the value when the dialog is closed", - "default": function(button_id) {egw.debug("log","Button ID: %d",button_id);} - }, - beforeClose: { - name: "before close callback", - type: "js", - description: "Callback function before dialog is closed, return false to prevent that", - "default": function() {} - }, - message: { - name: "Message", - type: "string", - description: "Dialog message (plain text, no html)", - "default": "Somebody forgot to set this..." - }, - dialog_type: { - name: "Dialog type", - type: "integer", - description: "To use a pre-defined dialog style, use et2_dialog.ERROR_MESSAGE, INFORMATION_MESSAGE,WARNING_MESSAGE,QUESTION_MESSAGE,PLAIN_MESSAGE constants. Default is et2_dialog.PLAIN_MESSAGE", - "default": 0 //this.PLAIN_MESSAGE - }, - buttons: { - name: "Buttons", - type: "any", - "default": 0, //this.BUTTONS_OK, - description: "Buttons that appear at the bottom of the dialog. You can use the constants et2_dialog.BUTTONS_OK, BUTTONS_YES_NO, BUTTONS_YES_NO_CANCEL, BUTTONS_OK_CANCEL, or pass in an array for full control" - }, - icon: { - name: "Icon", - type: "string", - description: "URL of an icon for the dialog. If omitted, an icon based on dialog_type will be used.", - "default": "" - }, - title: { - name: "Title", - type: "string", - description: "Title for the dialog box (plain text, no html)", - "default": "" - }, - modal: { - name: "Modal", - type: "boolean", - description: "Prevent the user from interacting with the page", - "default": true - }, - resizable: { - name: "Resizable", - type: "boolean", - description: "Allow the user to resize the dialog", - "default": true - }, - value: { - "name": "Value", - "description": "The (default) value of the dialog. Use with template.", - "type": "any", - "default": et2_no_init - }, - template: { - "name": "Template", - "description": "Instead of displaying a simple message, a full template can be loaded instead. Set defaults with value.", - "type": "string", - "default": et2_no_init - }, - minWidth: { - name: "minimum width", - type: "integer", - description: "Define minimum width of dialog", - "default": 0 - }, - minHeight: { - name: "minimum height", - type: "integer", - description: "Define minimum height of dialog", - "default": 0 - }, - width: { - name: "width", - type: "string", - description: "Define width of dialog, the default is auto", - "default": 'auto' - }, - height: { - name: "height", - type: "string", - description: "Define width of dialog, the default is auto", - "default": 'auto' - }, - position: { - name: "position", - type: "string", - description: "Define position of dialog in the main window", - default: "center" - } - }, - - /** - * Details for dialog type options - */ - _dialog_types: [ - //PLAIN_MESSAGE: 0 - "", - //INFORMATION_MESSAGE: 1, - "dialog_info", - //QUESTION_MESSAGE: 2, - "dialog_help", - //WARNING_MESSAGE: 3, - "dialog_warning", - //ERROR_MESSAGE: 4, - "dialog_error" - ], - - _buttons: [ - /* - Pre-defined Button combos - - button ids copied from et2_dialog static, since the constants are not defined yet - - image get replaced by 'style="background-image: url('+egw.image(image)+')' for an image prefixing text - */ - //BUTTONS_OK: 0, - [{"button_id": 1,"text": 'ok', id: 'dialog[ok]', image: 'check', "default":true}], - //BUTTONS_OK_CANCEL: 1, - [ - {"button_id": 1,"text": 'ok', id: 'dialog[ok]', image: 'check', "default":true}, - {"button_id": 0,"text": 'cancel', id: 'dialog[cancel]', image: 'cancel'} - ], - //BUTTONS_YES_NO: 2, - [ - {"button_id": 2,"text": 'yes', id: 'dialog[yes]', image: 'check', "default":true}, - {"button_id": 3,"text": 'no', id: 'dialog[no]', image: 'cancelled'} - ], - //BUTTONS_YES_NO_CANCEL: 3, - [ - {"button_id": 2,"text": 'yes', id: 'dialog[yes]', image: 'check', "default":true}, - {"button_id": 3,"text": 'no', id: 'dialog[no]', image: 'cancelled'}, - {"button_id": 0,"text": 'cancel', id: 'dialog[cancel]', image: 'cancel'} - ] - ], - - // Define this as null to avoid breaking any hierarchies (eg: destroy()) - _parent: null, - - /** - * Constructor - * - * @memberOf et2_dialog - */ - init: function() { - // Call the inherited constructor - this._super.apply(this, arguments); - - // Button callbacks need a reference to this - var self = this; - for(var i = 0; i < this._buttons.length; i++) - { - for(var j = 0; j < this._buttons[i].length; j++) - { - this._buttons[i][j].click = (function(id) { - return function(event) { - self.click(event.target,id); - }; - })(this._buttons[i][j].button_id); - // translate button texts, as translations are not available before - this._buttons[i][j].text = egw.lang(this._buttons[i][j].text); - } - } - - this.div = jQuery(document.createElement("div")); - - this._createDialog(); - }, - - /** - * Clean up dialog - */ - destroy: function() { - if(this.div != null) - { - // Un-dialog the dialog - this.div.dialog("destroy"); - - if(this.template) - { - this.template.clear(); - this.template = null; - } - - this.div = null; - } - - // Call the inherited constructor - this._super.apply(this, arguments); - }, - - /** - * Internal callback registered on all standard buttons. - * The provided callback is called after the dialog is closed. - * - * @param target DOMNode The clicked button - * @param button_id integer The ID of the clicked button - */ - click: function(target, button_id) { - if(this.options.callback) - { - if (this.options.callback.call(this,button_id,this.get_value()) === false) return; - } - // Triggers destroy too - this.div.dialog("close"); - }, - - /** - * Returns the values of any widgets in the dialog. This does not include - * the buttons, which are only supplied for the callback. - */ - get_value: function() { - var value = this.options.value; - if(this.template) - { - value = this.template.getValues(this.template.widgetContainer); - } - return value; - }, - - /** - * Set the displayed prompt message - * - * @param {string} message New message for the dialog - */ - set_message: function(message) { - this.options.message = message; - - this.div.empty() - .append("") - .append(jQuery('
').text(message)); - }, - - /** - * Set the dialog type to a pre-defined type - * - * @param {integer} type constant from et2_dialog - */ - set_dialog_type: function(type) { - if(this.options.dialog_type != type && typeof this._dialog_types[type] == "string") - { - this.options.dialog_type = type; - } - this.set_icon(this._dialog_types[type] ? egw.image(this._dialog_types[type]) : ""); - }, - - /** - * Set the icon for the dialog - * - * @param {string} icon_url - */ - set_icon: function(icon_url) { - if(icon_url == "") - { - jQuery("img.dialog_icon",this.div).hide(); - } - else - { - jQuery("img.dialog_icon",this.div).show().attr("src", icon_url); - } - }, - - /** - * Set the dialog buttons - * - * Use either the pre-defined options in et2_dialog, or an array - * @see http://api.jqueryui.com/dialog/#option-buttons - * @param {array} buttons - */ - set_buttons: function(buttons) { - this.options.buttons = buttons; - if (buttons instanceof Array) - { - for (var i = 0; i < buttons.length; i++) - { - var button = buttons[i]; - if(!button.click) - { - button.click = jQuery.proxy(this.click,this,null,button.id); - } - // set a default background image and css class based on buttons id - if (button.id && typeof button.class == 'undefined') - { - for(var name in et2_button.default_classes) - { - if (button.id.match(et2_button.default_classes[name])) - { - button.class = (typeof button.class == 'undefined' ? '' : button.class+' ')+name; - break; - } - } - } - if (button.id && typeof button.image == 'undefined' && typeof button.style == 'undefined') - { - for(var name in et2_button.default_background_images) - { - if (button.id.match(et2_button.default_background_images[name])) - { - button.image = name; - break; - } - } - } - if (button.image) - { - button.style = 'background-image: url('+this.egw().image(button.image,'api')+')'; - delete button.image; - } - } - } - - // If dialog already created, update buttons - if(this.div.data('ui-dialog')) - { - this.div.dialog("option", "buttons", buttons); - - // Focus default button so enter works - jQuery('.ui-dialog-buttonpane button[default]',this.div.parent()).focus(); - } - }, - - /** - * Set the dialog title - * - * @param {string} title New title for the dialog - */ - set_title: function(title) { - this.options.title = title; - this.div.dialog("option","title",title); - }, - - /** - * Block interaction with the page behind the dialog - * - * @param {boolean} modal Block page behind dialog - */ - set_modal: function(modal) { - this.options.modal = modal; - this.div.dialog("option","modal",modal); - }, - - /** - * Load an etemplate into the dialog - * - * @param template String etemplate file name - */ - set_template: function(template) { - if(this.template && this.options.template != template) - { - this.template.clear(); - } - - this.template = new etemplate2(this.div[0], false); - if(template.indexOf('.xet') > 0) - { - // File name provided, fetch from server - this.template.load("",template,this.options.value||{content: {}}, jQuery.proxy(function() { - // Set focus to the first input - jQuery('input',this.div).first().focus(); - },this)); - } - else - { - // Just template name, it better be loaded already - this.template.load(template, '', this.options.value || {}, - // true: do NOT call et2_ready, as it would overwrite this.et2 in app.js - undefined, undefined, true); - } - // set template-name as id, to allow to style dialogs - this.div.children().attr('id', template.replace(/^(.*\/)?([^/]+)(\.xet)?$/, '$2').replace(/\./g, '-')); - }, - - /** - * Actually create and display the dialog - */ - _createDialog: function() { - if(this.options.template) - { - this.set_template(this.options.template); - } - else - { - this.set_message(this.options.message); - this.set_dialog_type(this.options.dialog_type); - } - this.set_buttons(typeof this.options.buttons == "number" ? this._buttons[this.options.buttons] : this.options.buttons); - var self = this; - - var options = { - // Pass the internal object, not the option - buttons: this.options.buttons, - modal: this.options.modal, - resizable: this.options.resizable, - minWidth: this.options.minWidth, - minHeight:this.options.minHeight, - maxWidth: 640, - height: this.options.height, - title: this.options.title, - open: function() { - // Focus default button so enter works - jQuery(this).parents('.ui-dialog-buttonpane button[default]').focus(); - window.setTimeout(function() { - jQuery(this).dialog('option','position',{ - my: "center", - at: "center", - of: window - });}.bind(this),0); - }, - close: jQuery.proxy(function() {this.destroy();},this), - beforeClose: this.options.beforeClose, - closeText: this.egw().lang('close'), - position: {my:this.options.position, at:this.options.position, of:window} - }; - // Leaving width unset lets it size itself according to contents - if(this.options.width ) - { - options.width = this.options.width; - } - - this.div.dialog(options); - - // Make sure dialog is wide enough for the title - // Arbitrary numbers that seem to work nicely. - var title_width = 20 + 10 * this.options.title.length; - if(this.div.width() < title_width && this.options.title.trim()) - { - // Auto-sizing chopped the title - this.div.dialog('option', 'width', title_width); - } - } -});}).call(this); -et2_register_widget(et2_dialog, ["dialog"]); - -// Static class stuff -jQuery.extend(et2_dialog, //(function(){ "use strict"; return -/** @lends et2_dialog */ -{ - // Some class constants // - - /** - * Types - * @constant - */ - PLAIN_MESSAGE: 0, - INFORMATION_MESSAGE: 1, - QUESTION_MESSAGE: 2, - WARNING_MESSAGE: 3, - ERROR_MESSAGE: 4, - - /* Pre-defined Button combos */ - BUTTONS_OK: 0, - BUTTONS_OK_CANCEL: 1, - BUTTONS_YES_NO: 2, - BUTTONS_YES_NO_CANCEL: 3, - - /* Button constants */ - CANCEL_BUTTON: 0, - OK_BUTTON: 1, - YES_BUTTON: 2, - NO_BUTTON: 3, - - - /** - * Create a parent to inject application specific egw object with loaded translations into et2_dialog - * - * @param {string|egw} _egw_or_appname egw object with already laoded translations or application name to load translations for - */ - _create_parent: function(_egw_or_appname) - { - if (typeof _egw_or_appname == 'undefined') - { - _egw_or_appname = egw_appName; - } - // create a dummy parent with a correct reference to an application specific egw object - var parent = new et2_widget(); - // if egw object is passed in because called from et2, just use it - if (typeof _egw_or_appname != 'string') - { - parent._egw = _egw_or_appname; - } - // otherwise use given appname to create app-specific egw instance and load default translations - else - { - parent._egw = egw(_egw_or_appname); - parent._egw.langRequireApp(parent._egw.window, _egw_or_appname); - } - return parent; - }, - - /** - * Show a confirmation dialog - * - * @param {function} _callback Function called when the user clicks a button. The context will be the et2_dialog widget, and the button constant is passed in. - * @param {string} _message Message to be place in the dialog. - * @param {string} _title Text in the top bar of the dialog. - * @param _value passed unchanged to callback as 2. parameter - * @param {integer|array} _buttons One of the BUTTONS_ constants defining the set of buttons at the bottom of the box - * @param {integer} _type One of the message constants. This defines the style of the message. - * @param {string} _icon URL of an icon to display. If not provided, a type-specific icon will be used. - * @param {string|egw} _egw_or_appname egw object with already laoded translations or application name to load translations for - */ - show_dialog: function(_callback, _message, _title, _value, _buttons, _type, _icon, _egw_or_appname) - { - var parent = et2_dialog._create_parent(_egw_or_appname); - - // Just pass them along, widget handles defaults & missing - return et2_createWidget("dialog", { - callback: _callback||function(){}, - message: _message, - title: _title||parent._egw.lang('Confirmation required'), - buttons: typeof _buttons != 'undefined' ? _buttons : et2_dialog.BUTTONS_YES_NO, - dialog_type: typeof _type != 'undefined' ? _type : et2_dialog.QUESTION_MESSAGE, - icon: _icon, - value: _value, - width: 'auto' - }, parent); - }, - - /** - * Show an alert message with OK button - * - * @param {string} _message Message to be place in the dialog. - * @param {string} _title Text in the top bar of the dialog. - * @param {integer} _type One of the message constants. This defines the style of the message. - */ - alert: function (_message, _title, _type) - { - var parent = et2_dialog._create_parent(et2_dialog._create_parent()._egw); - et2_createWidget("dialog", { - callback:function(){}, - message: _message, - title: _title, - buttons: et2_dialog.BUTTONS_OK, - dialog_type: _type || et2_dialog.INFORMATION_MESSAGE - }, parent); - }, - - /** - * Show a prompt dialog - * - * @param {function} _callback Function called when the user clicks a button. The context will be the et2_dialog widget, and the button constant is passed in. - * @param {string} _message Message to be place in the dialog. - * @param {string} _title Text in the top bar of the dialog. - * @param {string} _value for prompt, passed to callback as 2. parameter - * @param {integer|array} _buttons One of the BUTTONS_ constants defining the set of buttons at the bottom of the box - * @param {string|egw} _egw_or_appname egw object with already laoded translations or application name to load translations for - */ - show_prompt: function(_callback, _message, _title, _value, _buttons, _egw_or_appname) - { - var callback = _callback; - // Just pass them along, widget handles defaults & missing - return et2_createWidget("dialog", { - callback: function(_button_id, _value) { - if (typeof callback == "function") - { - callback.call(this, _button_id, _value.value); - } - }, - title: _title||egw.lang('Input required'), - buttons: _buttons||et2_dialog.BUTTONS_OK_CANCEL, - value: { - content: { - value: _value, - message: _message - } - }, - template: egw.webserverUrl+'/api/templates/default/prompt.xet', - class: "et2_prompt" - }, et2_dialog._create_parent(_egw_or_appname)); - }, - - /** - * Method to build a confirmation dialog only with - * YES OR NO buttons and submit content back to server - * - * @param {widget} _senders widget that has been clicked - * @param {String} _dialogMsg message shows in dialog box - * @param {String} _titleMsg message shows as a title of the dialog box - * @param {Bool} _postSubmit true: use postSubmit instead of submit - * - * @description submit the form contents including the button that has been pressed - */ - confirm: function(_senders,_dialogMsg, _titleMsg, _postSubmit) - { - var senders = _senders; - var buttonId = _senders.id; - var dialogMsg = (typeof _dialogMsg !="undefined") ? _dialogMsg : ''; - var titleMsg = (typeof _titleMsg !="undefined") ? _titleMsg : ''; - var egw = _senders instanceof et2_widget ? _senders.egw() : et2_dialog._create_parent()._egw; - var callbackDialog = function (button_id) - { - if (button_id == et2_dialog.YES_BUTTON ) - { - if (_postSubmit) - { - senders.getRoot().getInstanceManager().postSubmit(buttonId); - } - else - { - senders.getRoot().getInstanceManager().submit(buttonId); - } - } - }; - et2_dialog.show_dialog(callbackDialog, egw.lang(dialogMsg), egw.lang(titleMsg), {}, - et2_dialog.BUTTON_YES_NO, et2_dialog.WARNING_MESSAGE, undefined, egw); - }, - - - /** - * Show a dialog for a long-running, multi-part task - * - * Given a server url and a list of parameters, this will open a dialog with - * a progress bar, asynchronously call the url with each parameter, and update - * the progress bar. - * Any output from the server will be displayed in a box. - * - * When all tasks are done, the callback will be called with boolean true. It will - * also be called if the user clicks a button (OK or CANCEL), so be sure to - * check to avoid executing more than intended. - * - * @param {function} _callback Function called when the user clicks a button, - * or when the list is done processing. The context will be the et2_dialog - * widget, and the button constant is passed in. - * @param {string} _message Message to be place in the dialog. Usually just - * text, but DOM nodes will work too. - * @param {string} _title Text in the top bar of the dialog. - * @param {string} _menuaction the menuaction function which should be called and - * which handles the actual request. If the menuaction is a full featured - * url, this one will be used instead. - * @param {Array[]} _list - List of parameters, one for each call to the - * address. Multiple parameters are allowed, in an array. - * @param {string|egw} _egw_or_appname egw object with already laoded translations or application name to load translations for - * - * @return {et2_dialog} - */ - long_task: function(_callback, _message, _title, _menuaction, _list, _egw_or_appname) - { - var parent = et2_dialog._create_parent(_egw_or_appname); - var egw = parent._egw; - - // Special action for cancel - var buttons = [ - {"button_id": et2_dialog.OK_BUTTON,"text": egw.lang('ok'), "default":true, "disabled":true}, - {"button_id": et2_dialog.CANCEL_BUTTON,"text": egw.lang('cancel'), click: function() { - // Cancel run - cancel = true; - jQuery("button[button_id="+et2_dialog.CANCEL_BUTTON+"]", dialog.div.parent()).button("disable"); - update.call(_list.length,''); - }} - ]; - var dialog = et2_createWidget("dialog", { - template: egw.webserverUrl+'/api/templates/default/long_task.xet', - value: { - content: { - message: _message - } - }, - callback: function(_button_id, _value) { - if(_button_id == et2_dialog.CANCEL_BUTTON) - { - cancel = true; - } - if (typeof _callback == "function") - { - _callback.call(this, _button_id, _value.value); - } - }, - title: _title||egw.lang('please wait...'), - buttons: buttons - }, parent); - - // OK starts disabled - jQuery("button[button_id="+et2_dialog.OK_BUTTON+"]", dialog.div.parent()).button("disable"); - - var log = null; - var progressbar = null; - var cancel = false; - var totals = { - success: 0, - skipped: 0, - failed: 0, - widget: null - }; - - // Updates progressbar & log, calls next step - var update = function(response) { - // context is index - var index = this || 0; - - progressbar.set_value(100*(index/_list.length)); - progressbar.set_label(index +' / ' + _list.length); - - // Display response information - switch(response.type) - { - case 'error': - jQuery("
") - .text(response.data) - .appendTo(log); - - totals.failed++; - - // Ask to retry / ignore / abort - et2_createWidget("dialog", { - callback:function(button) { - switch(button) - { - case 'dialog[cancel]': - cancel = true; - return update.call(index,''); - case 'dialog[skip]': - // Continue with next index - totals.skipped++; - return update.call(index,''); - default: - // Try again with previous index - return update.call(index-1,''); - } - - }, - message: response.data, - title: '', - buttons: [ - // These ones will use the callback, just like normal - {text: egw.lang("Abort"),id:'dialog[cancel]'}, - {text: egw.lang("Retry"),id:'dialog[retry]'}, - {text: egw.lang("Skip"),id:'dialog[skip]', class:"ui-priority-primary", default: true} - ], - dialog_type: et2_dialog.ERROR_MESSAGE - }, parent); - // Early exit - return; - default: - if(response) - { - totals.success++; - jQuery("
") - .text(response) - .appendTo(log); - } - } - // Scroll to bottom - var height = log[0].scrollHeight; - log.scrollTop(height); - - // Update totals - totals.widget.set_value(egw.lang( - "Total: %1 Successful: %2 Failed: %3 Skipped: %4", - _list.length, totals.success, totals.failed, totals.skipped - )); - - // Fire next step - if(!cancel && index < _list.length) - { - var parameters = _list[index]; - if(typeof parameters != 'object') parameters = [parameters]; - - // Async request, we'll take the next step in the callback - // We can't pass index = 0, it looks like false and causes issues - egw.json(_menuaction, parameters, update, index+1,true,index+1).sendRequest(); - } - else - { - // All done - if(!cancel) progressbar.set_value(100); - jQuery("button[button_id="+et2_dialog.CANCEL_BUTTON+"]", dialog.div.parent()).button("disable"); - jQuery("button[button_id="+et2_dialog.OK_BUTTON+"]", dialog.div.parent()).button("enable"); - if (!cancel && typeof _callback == "function") - { - _callback.call(dialog, true, response); - } - } - }; - - jQuery(dialog.template.DOMContainer).on('load', function() { - // Get access to template widgets - log = jQuery(dialog.template.widgetContainer.getWidgetById('log').getDOMNode()); - progressbar = dialog.template.widgetContainer.getWidgetById('progressbar'); - progressbar.set_label('0 / ' + _list.length); - totals.widget = dialog.template.widgetContainer.getWidgetById('totals'); - - // Start - window.setTimeout(function() {update.call(0,'');},0); - }); - - return dialog; - } -}//;}).call(this) -); +var et2_dialog = /** @class */ (function (_super) { + __extends(et2_dialog, _super); + function et2_dialog(_parent, _attrs, _child) { + var _this = _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_dialog._attributes, _child || {})) || this; + /** + * Details for dialog type options + */ + _this._dialog_types = [ + //PLAIN_MESSAGE: 0 + "", + //INFORMATION_MESSAGE: 1, + "dialog_info", + //QUESTION_MESSAGE: 2, + "dialog_help", + //WARNING_MESSAGE: 3, + "dialog_warning", + //ERROR_MESSAGE: 4, + "dialog_error" + ]; + _this._buttons = [ + /* + Pre-defined Button combos + - button ids copied from et2_dialog static, since the constants are not defined yet + - image get replaced by 'style="background-image: url('+egw.image(image)+')' for an image prefixing text + */ + //BUTTONS_OK: 0, + [{ "button_id": 1, "text": 'ok', id: 'dialog[ok]', image: 'check', "default": true }], + //BUTTONS_OK_CANCEL: 1, + [ + { "button_id": 1, "text": 'ok', id: 'dialog[ok]', image: 'check', "default": true }, + { "button_id": 0, "text": 'cancel', id: 'dialog[cancel]', image: 'cancel' } + ], + //BUTTONS_YES_NO: 2, + [ + { "button_id": 2, "text": 'yes', id: 'dialog[yes]', image: 'check', "default": true }, + { "button_id": 3, "text": 'no', id: 'dialog[no]', image: 'cancelled' } + ], + //BUTTONS_YES_NO_CANCEL: 3, + [ + { "button_id": 2, "text": 'yes', id: 'dialog[yes]', image: 'check', "default": true }, + { "button_id": 3, "text": 'no', id: 'dialog[no]', image: 'cancelled' }, + { "button_id": 0, "text": 'cancel', id: 'dialog[cancel]', image: 'cancel' } + ] + ]; + _this.div = null; + _this.template = null; + // Define this as null to avoid breaking any hierarchies (eg: destroy()) + if (_this.getParent() != null) + _this.getParent().removeChild(_this); + // Button callbacks need a reference to this + var self = _this; + for (var i = 0; i < _this._buttons.length; i++) { + for (var j = 0; j < _this._buttons[i].length; j++) { + _this._buttons[i][j].click = (function (id) { + return function (event) { + self.click(event.target, id); + }; + })(_this._buttons[i][j].button_id); + // translate button texts, as translations are not available before + _this._buttons[i][j].text = egw.lang(_this._buttons[i][j].text); + } + } + _this.div = jQuery(document.createElement("div")); + _this._createDialog(); + return _this; + } + /** + * Clean up dialog + */ + et2_dialog.prototype.destroy = function () { + if (this.div != null) { + // Un-dialog the dialog + this.div.dialog("destroy"); + if (this.template) { + this.template.clear(); + this.template = null; + } + this.div = null; + } + // Call the inherited constructor + _super.prototype.destroy.call(this); + }; + /** + * Internal callback registered on all standard buttons. + * The provided callback is called after the dialog is closed. + * + * @param target DOMNode The clicked button + * @param button_id integer The ID of the clicked button + */ + et2_dialog.prototype.click = function (target, button_id) { + if (this.options.callback) { + if (this.options.callback.call(this, button_id, this.get_value()) === false) + return; + } + // Triggers destroy too + this.div.dialog("close"); + }; + /** + * Returns the values of any widgets in the dialog. This does not include + * the buttons, which are only supplied for the callback. + */ + et2_dialog.prototype.get_value = function () { + var value = this.options.value; + if (this.template) { + value = this.template.getValues(this.template.widgetContainer); + } + return value; + }; + /** + * Set the displayed prompt message + * + * @param {string} message New message for the dialog + */ + et2_dialog.prototype.set_message = function (message) { + this.options.message = message; + this.div.empty() + .append("") + .append(jQuery('
').text(message)); + }; + /** + * Set the dialog type to a pre-defined type + * + * @param {integer} type constant from et2_dialog + */ + et2_dialog.prototype.set_dialog_type = function (type) { + if (this.options.dialog_type != type && typeof this._dialog_types[type] == "string") { + this.options.dialog_type = type; + } + this.set_icon(this._dialog_types[type] ? egw.image(this._dialog_types[type]) : ""); + }; + /** + * Set the icon for the dialog + * + * @param {string} icon_url + */ + et2_dialog.prototype.set_icon = function (icon_url) { + if (icon_url == "") { + jQuery("img.dialog_icon", this.div).hide(); + } + else { + jQuery("img.dialog_icon", this.div).show().attr("src", icon_url); + } + }; + /** + * Set the dialog buttons + * + * Use either the pre-defined options in et2_dialog, or an array + * @see http://api.jqueryui.com/dialog/#option-buttons + * @param {array} buttons + */ + et2_dialog.prototype.set_buttons = function (buttons) { + this.options.buttons = buttons; + if (buttons instanceof Array) { + for (var i = 0; i < buttons.length; i++) { + var button = buttons[i]; + if (!button.click) { + button.click = jQuery.proxy(this.click, this, null, button.id); + } + // set a default background image and css class based on buttons id + if (button.id && typeof button.class == 'undefined') { + for (var name in et2_widget_button_1.et2_button.default_classes) { + if (button.id.match(et2_widget_button_1.et2_button.default_classes[name])) { + button.class = (typeof button.class == 'undefined' ? '' : button.class + ' ') + name; + break; + } + } + } + if (button.id && typeof button.image == 'undefined' && typeof button.style == 'undefined') { + for (var name in et2_widget_button_1.et2_button.default_background_images) { + if (button.id.match(et2_widget_button_1.et2_button.default_background_images[name])) { + button.image = name; + break; + } + } + } + if (button.image) { + button.style = 'background-image: url(' + this.egw().image(button.image, 'api') + ')'; + delete button.image; + } + } + } + // If dialog already created, update buttons + if (this.div.data('ui-dialog')) { + this.div.dialog("option", "buttons", buttons); + // Focus default button so enter works + jQuery('.ui-dialog-buttonpane button[default]', this.div.parent()).focus(); + } + }; + /** + * Set the dialog title + * + * @param {string} title New title for the dialog + */ + et2_dialog.prototype.set_title = function (title) { + this.options.title = title; + this.div.dialog("option", "title", title); + }; + /** + * Block interaction with the page behind the dialog + * + * @param {boolean} modal Block page behind dialog + */ + et2_dialog.prototype.set_modal = function (modal) { + this.options.modal = modal; + this.div.dialog("option", "modal", modal); + }; + /** + * Load an etemplate into the dialog + * + * @param template String etemplate file name + */ + et2_dialog.prototype.set_template = function (template) { + if (this.template && this.options.template != template) { + this.template.clear(); + } + this.template = new etemplate2(this.div[0], false); + if (template.indexOf('.xet') > 0) { + // File name provided, fetch from server + this.template.load("", template, this.options.value || { content: {} }, jQuery.proxy(function () { + // Set focus to the first input + jQuery('input', this.div).first().focus(); + }, this)); + } + else { + // Just template name, it better be loaded already + this.template.load(template, '', this.options.value || {}, + // true: do NOT call et2_ready, as it would overwrite this.et2 in app.js + undefined, undefined, true); + } + // set template-name as id, to allow to style dialogs + this.div.children().attr('id', template.replace(/^(.*\/)?([^/]+)(\.xet)?$/, '$2').replace(/\./g, '-')); + }; + /** + * Actually create and display the dialog + */ + et2_dialog.prototype._createDialog = function () { + if (this.options.template) { + this.set_template(this.options.template); + } + else { + this.set_message(this.options.message); + this.set_dialog_type(this.options.dialog_type); + } + this.set_buttons(typeof this.options.buttons == "number" ? this._buttons[this.options.buttons] : this.options.buttons); + var options = { + // Pass the internal object, not the option + buttons: this.options.buttons, + modal: this.options.modal, + resizable: this.options.resizable, + minWidth: this.options.minWidth, + minHeight: this.options.minHeight, + maxWidth: 640, + height: this.options.height, + title: this.options.title, + open: function () { + // Focus default button so enter works + jQuery(this).parents('.ui-dialog-buttonpane button[default]').focus(); + window.setTimeout(function () { + jQuery(this).dialog('option', 'position', { + my: "center", + at: "center", + of: window + }); + }.bind(this), 0); + }, + close: jQuery.proxy(function () { + this.destroy(); + }, this), + beforeClose: this.options.beforeClose, + closeText: this.egw().lang('close'), + position: { my: this.options.position, at: this.options.position, of: window } + }; + // Leaving width unset lets it size itself according to contents + if (this.options.width) { + options['width'] = this.options.width; + } + this.div.dialog(options); + // Make sure dialog is wide enough for the title + // Arbitrary numbers that seem to work nicely. + var title_width = 20 + 10 * this.options.title.length; + if (this.div.width() < title_width && this.options.title.trim()) { + // Auto-sizing chopped the title + this.div.dialog('option', 'width', title_width); + } + }; + /** + * Create a parent to inject application specific egw object with loaded translations into et2_dialog + * + * @param {string|egw} _egw_or_appname egw object with already loaded translations or application name to load translations for + */ + et2_dialog._create_parent = function (_egw_or_appname) { + if (typeof _egw_or_appname == 'undefined') { + // @ts-ignore + _egw_or_appname = egw_appName; + } + // create a dummy parent with a correct reference to an application specific egw object + var parent = new et2_core_widget_2.et2_widget(); + // if egw object is passed in because called from et2, just use it + if (typeof _egw_or_appname != 'string') { + parent.setApiInstance(_egw_or_appname); + } + // otherwise use given appname to create app-specific egw instance and load default translations + else { + parent.setApiInstance(egw(_egw_or_appname)); + parent.egw().langRequireApp(parent.egw().window, _egw_or_appname); + } + return parent; + }; + /** + * Show a confirmation dialog + * + * @param {function} _callback Function called when the user clicks a button. The context will be the et2_dialog widget, and the button constant is passed in. + * @param {string} _message Message to be place in the dialog. + * @param {string} _title Text in the top bar of the dialog. + * @param _value passed unchanged to callback as 2. parameter + * @param {integer|array} _buttons One of the BUTTONS_ constants defining the set of buttons at the bottom of the box + * @param {integer} _type One of the message constants. This defines the style of the message. + * @param {string} _icon URL of an icon to display. If not provided, a type-specific icon will be used. + * @param {string|egw} _egw_or_appname egw object with already laoded translations or application name to load translations for + */ + et2_dialog.show_dialog = function (_callback, _message, _title, _value, _buttons, _type, _icon, _egw_or_appname) { + var parent = et2_dialog._create_parent(_egw_or_appname); + // Just pass them along, widget handles defaults & missing + return et2_createWidget("dialog", { + callback: _callback || function () { + }, + message: _message, + title: _title || parent.egw().lang('Confirmation required'), + buttons: typeof _buttons != 'undefined' ? _buttons : et2_dialog.BUTTONS_YES_NO, + dialog_type: typeof _type != 'undefined' ? _type : et2_dialog.QUESTION_MESSAGE, + icon: _icon, + value: _value, + width: 'auto' + }, parent); + }; + ; + /** + * Show an alert message with OK button + * + * @param {string} _message Message to be place in the dialog. + * @param {string} _title Text in the top bar of the dialog. + * @param {integer} _type One of the message constants. This defines the style of the message. + */ + et2_dialog.alert = function (_message, _title, _type) { + var parent = et2_dialog._create_parent(et2_dialog._create_parent().egw()); + et2_createWidget("dialog", { + callback: function () { + }, + message: _message, + title: _title, + buttons: et2_dialog.BUTTONS_OK, + dialog_type: _type || et2_dialog.INFORMATION_MESSAGE + }, parent); + }; + /** + * Show a prompt dialog + * + * @param {function} _callback Function called when the user clicks a button. The context will be the et2_dialog widget, and the button constant is passed in. + * @param {string} _message Message to be place in the dialog. + * @param {string} _title Text in the top bar of the dialog. + * @param {string} _value for prompt, passed to callback as 2. parameter + * @param {integer|array} _buttons One of the BUTTONS_ constants defining the set of buttons at the bottom of the box + * @param {string|egw} _egw_or_appname egw object with already laoded translations or application name to load translations for + */ + et2_dialog.show_prompt = function (_callback, _message, _title, _value, _buttons, _egw_or_appname) { + var callback = _callback; + // Just pass them along, widget handles defaults & missing + return et2_createWidget("dialog", { + callback: function (_button_id, _value) { + if (typeof callback == "function") { + callback.call(this, _button_id, _value.value); + } + }, + title: _title || egw.lang('Input required'), + buttons: _buttons || et2_dialog.BUTTONS_OK_CANCEL, + value: { + content: { + value: _value, + message: _message + } + }, + template: egw.webserverUrl + '/api/templates/default/prompt.xet', + class: "et2_prompt" + }, et2_dialog._create_parent(_egw_or_appname)); + }; + /** + * Method to build a confirmation dialog only with + * YES OR NO buttons and submit content back to server + * + * @param {widget} _senders widget that has been clicked + * @param {String} _dialogMsg message shows in dialog box + * @param {String} _titleMsg message shows as a title of the dialog box + * @param {Bool} _postSubmit true: use postSubmit instead of submit + * + * @description submit the form contents including the button that has been pressed + */ + et2_dialog.confirm = function (_senders, _dialogMsg, _titleMsg, _postSubmit) { + var senders = _senders; + var buttonId = _senders.id; + var dialogMsg = (typeof _dialogMsg != "undefined") ? _dialogMsg : ''; + var titleMsg = (typeof _titleMsg != "undefined") ? _titleMsg : ''; + var egw = _senders instanceof et2_core_widget_2.et2_widget ? _senders.egw() : et2_dialog._create_parent().egw(); + var callbackDialog = function (button_id) { + if (button_id == et2_dialog.YES_BUTTON) { + if (_postSubmit) { + senders.getRoot().getInstanceManager().postSubmit(buttonId); + } + else { + senders.getRoot().getInstanceManager().submit(buttonId); + } + } + }; + et2_dialog.show_dialog(callbackDialog, egw.lang(dialogMsg), egw.lang(titleMsg), {}, et2_dialog.BUTTONS_YES_NO, et2_dialog.WARNING_MESSAGE, undefined, egw); + }; + ; + /** + * Show a dialog for a long-running, multi-part task + * + * Given a server url and a list of parameters, this will open a dialog with + * a progress bar, asynchronously call the url with each parameter, and update + * the progress bar. + * Any output from the server will be displayed in a box. + * + * When all tasks are done, the callback will be called with boolean true. It will + * also be called if the user clicks a button (OK or CANCEL), so be sure to + * check to avoid executing more than intended. + * + * @param {function} _callback Function called when the user clicks a button, + * or when the list is done processing. The context will be the et2_dialog + * widget, and the button constant is passed in. + * @param {string} _message Message to be place in the dialog. Usually just + * text, but DOM nodes will work too. + * @param {string} _title Text in the top bar of the dialog. + * @param {string} _menuaction the menuaction function which should be called and + * which handles the actual request. If the menuaction is a full featured + * url, this one will be used instead. + * @param {Array[]} _list - List of parameters, one for each call to the + * address. Multiple parameters are allowed, in an array. + * @param {string|egw} _egw_or_appname egw object with already laoded translations or application name to load translations for + * + * @return {et2_dialog} + */ + et2_dialog.long_task = function (_callback, _message, _title, _menuaction, _list, _egw_or_appname) { + var parent = et2_dialog._create_parent(_egw_or_appname); + var egw = parent.egw(); + // Special action for cancel + var buttons = [ + { "button_id": et2_dialog.OK_BUTTON, "text": egw.lang('ok'), "default": true, "disabled": true }, + { + "button_id": et2_dialog.CANCEL_BUTTON, "text": egw.lang('cancel'), click: function () { + // Cancel run + cancel = true; + jQuery("button[button_id=" + et2_dialog.CANCEL_BUTTON + "]", dialog.div.parent()).button("disable"); + update.call(_list.length, ''); + } + } + ]; + var dialog = et2_createWidget("dialog", { + template: egw.webserverUrl + '/api/templates/default/long_task.xet', + value: { + content: { + message: _message + } + }, + callback: function (_button_id, _value) { + if (_button_id == et2_dialog.CANCEL_BUTTON) { + cancel = true; + } + if (typeof _callback == "function") { + _callback.call(this, _button_id, _value.value); + } + }, + title: _title || egw.lang('please wait...'), + buttons: buttons + }, parent); + // OK starts disabled + jQuery("button[button_id=" + et2_dialog.OK_BUTTON + "]", dialog.div.parent()).button("disable"); + var log = null; + var progressbar = null; + var cancel = false; + var totals = { + success: 0, + skipped: 0, + failed: 0, + widget: null + }; + // Updates progressbar & log, calls next step + var update = function (response) { + // context is index + var index = this || 0; + progressbar.set_value(100 * (index / _list.length)); + progressbar.set_label(index + ' / ' + _list.length); + // Display response information + switch (response.type) { + case 'error': + jQuery("
") + .text(response.data) + .appendTo(log); + totals.failed++; + // Ask to retry / ignore / abort + et2_createWidget("dialog", { + callback: function (button) { + switch (button) { + case 'dialog[cancel]': + cancel = true; + return update.call(index, ''); + case 'dialog[skip]': + // Continue with next index + totals.skipped++; + return update.call(index, ''); + default: + // Try again with previous index + return update.call(index - 1, ''); + } + }, + message: response.data, + title: '', + buttons: [ + // These ones will use the callback, just like normal + { text: egw.lang("Abort"), id: 'dialog[cancel]' }, + { text: egw.lang("Retry"), id: 'dialog[retry]' }, + { text: egw.lang("Skip"), id: 'dialog[skip]', class: "ui-priority-primary", default: true } + ], + dialog_type: et2_dialog.ERROR_MESSAGE + }, parent); + // Early exit + return; + default: + if (response) { + totals.success++; + jQuery("
") + .text(response) + .appendTo(log); + } + } + // Scroll to bottom + var height = log[0].scrollHeight; + log.scrollTop(height); + // Update totals + totals.widget.set_value(egw.lang("Total: %1 Successful: %2 Failed: %3 Skipped: %4", _list.length, totals.success, totals.failed, totals.skipped)); + // Fire next step + if (!cancel && index < _list.length) { + var parameters = _list[index]; + if (typeof parameters != 'object') + parameters = [parameters]; + // Async request, we'll take the next step in the callback + // We can't pass index = 0, it looks like false and causes issues + egw.json(_menuaction, parameters, update, index + 1, true, index + 1).sendRequest(); + } + else { + // All done + if (!cancel) + progressbar.set_value(100); + jQuery("button[button_id=" + et2_dialog.CANCEL_BUTTON + "]", dialog.div.parent()).button("disable"); + jQuery("button[button_id=" + et2_dialog.OK_BUTTON + "]", dialog.div.parent()).button("enable"); + if (!cancel && typeof _callback == "function") { + _callback.call(dialog, true, response); + } + } + }; + jQuery(dialog.template.DOMContainer).on('load', function () { + // Get access to template widgets + log = jQuery(dialog.template.widgetContainer.getWidgetById('log').getDOMNode()); + progressbar = dialog.template.widgetContainer.getWidgetById('progressbar'); + progressbar.set_label('0 / ' + _list.length); + totals.widget = dialog.template.widgetContainer.getWidgetById('totals'); + // Start + window.setTimeout(function () { + update.call(0, ''); + }, 0); + }); + return dialog; + }; + et2_dialog._attributes = { + callback: { + name: "Callback", + type: "js", + description: "Callback function is called with the value when the dialog is closed", + "default": function (button_id) { + egw.debug("log", "Button ID: %d", button_id); + } + }, + beforeClose: { + name: "before close callback", + type: "js", + description: "Callback function before dialog is closed, return false to prevent that", + "default": function () { + } + }, + message: { + name: "Message", + type: "string", + description: "Dialog message (plain text, no html)", + "default": "Somebody forgot to set this..." + }, + dialog_type: { + name: "Dialog type", + type: "integer", + description: "To use a pre-defined dialog style, use et2_dialog.ERROR_MESSAGE, INFORMATION_MESSAGE,WARNING_MESSAGE,QUESTION_MESSAGE,PLAIN_MESSAGE constants. Default is et2_dialog.PLAIN_MESSAGE", + "default": 0 //this.PLAIN_MESSAGE + }, + buttons: { + name: "Buttons", + type: "any", + "default": 0, + description: "Buttons that appear at the bottom of the dialog. You can use the constants et2_dialog.BUTTONS_OK, BUTTONS_YES_NO, BUTTONS_YES_NO_CANCEL, BUTTONS_OK_CANCEL, or pass in an array for full control" + }, + icon: { + name: "Icon", + type: "string", + description: "URL of an icon for the dialog. If omitted, an icon based on dialog_type will be used.", + "default": "" + }, + title: { + name: "Title", + type: "string", + description: "Title for the dialog box (plain text, no html)", + "default": "" + }, + modal: { + name: "Modal", + type: "boolean", + description: "Prevent the user from interacting with the page", + "default": true + }, + resizable: { + name: "Resizable", + type: "boolean", + description: "Allow the user to resize the dialog", + "default": true + }, + value: { + "name": "Value", + "description": "The (default) value of the dialog. Use with template.", + "type": "any", + "default": et2_no_init + }, + template: { + "name": "Template", + "description": "Instead of displaying a simple message, a full template can be loaded instead. Set defaults with value.", + "type": "string", + "default": et2_no_init + }, + minWidth: { + name: "minimum width", + type: "integer", + description: "Define minimum width of dialog", + "default": 0 + }, + minHeight: { + name: "minimum height", + type: "integer", + description: "Define minimum height of dialog", + "default": 0 + }, + width: { + name: "width", + type: "string", + description: "Define width of dialog, the default is auto", + "default": 'auto' + }, + height: { + name: "height", + type: "string", + description: "Define width of dialog, the default is auto", + "default": 'auto' + }, + position: { + name: "position", + type: "string", + description: "Define position of dialog in the main window", + default: "center" + } + }; + /** + * Types + * @constant + */ + et2_dialog.PLAIN_MESSAGE = 0; + et2_dialog.INFORMATION_MESSAGE = 1; + et2_dialog.QUESTION_MESSAGE = 2; + et2_dialog.WARNING_MESSAGE = 3; + et2_dialog.ERROR_MESSAGE = 4; + /* Pre-defined Button combos */ + et2_dialog.BUTTONS_OK = 0; + et2_dialog.BUTTONS_OK_CANCEL = 1; + et2_dialog.BUTTONS_YES_NO = 2; + et2_dialog.BUTTONS_YES_NO_CANCEL = 3; + /* Button constants */ + et2_dialog.CANCEL_BUTTON = 0; + et2_dialog.OK_BUTTON = 1; + et2_dialog.YES_BUTTON = 2; + et2_dialog.NO_BUTTON = 3; + return et2_dialog; +}(et2_core_widget_2.et2_widget)); +exports.et2_dialog = et2_dialog; +et2_core_widget_1.et2_register_widget(et2_dialog, ["dialog"]); +//# sourceMappingURL=et2_widget_dialog.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_dialog.ts b/api/js/etemplate/et2_widget_dialog.ts new file mode 100644 index 0000000000..7e32cd2a6c --- /dev/null +++ b/api/js/etemplate/et2_widget_dialog.ts @@ -0,0 +1,837 @@ +/** + * EGroupware eTemplate2 - JS Dialog Widget class + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @link http://www.egroupware.org + * @author Nathan Gray + * @copyright Nathan Gray 2013 + * @version $Id$ + */ + +/*egw:uses + et2_core_widget; + /vendor/bower-asset/jquery-ui/jquery-ui.js; +*/ + +import {et2_register_widget, WidgetConfig} from "./et2_core_widget"; +import {et2_widget} from "./et2_core_widget"; +import {et2_button} from "./et2_widget_button"; +import {ClassWithAttributes} from "./et2_core_inheritance"; +import {et2_DOMWidget} from "./et2_core_DOMWidget"; + +/** + * A common dialog widget that makes it easy to imform users or prompt for information. + * + * It is possible to have a custom dialog by using a template, but you can also use + * the static method et2_dialog.show_dialog(). At its simplest, you can just use: + * + * et2_dialog.show_dialog(false, "Operation completed"); + * + * Or a more complete example: + * + * var callback = function (button_id) + * { + * if(button_id == et2_dialog.YES_BUTTON) + * { + * // Do stuff + * } + * else if (button_id == et2_dialog.NO_BUTTON) + * { + * // Other stuff + * } + * else if (button_id == et2_dialog.CANCEL_BUTTON) + * { + * // Abort + * } + * }. + * var dialog = et2_dialog.show_dialog( + * callback, "Erase the entire database?","Break things", {} // value + * et2_dialog.BUTTONS_YES_NO_CANCEL, et2_dialog.WARNING_MESSAGE + * ); + * + * + * + * The parameters for the above are all optional, except callback and message: + * callback - function called when the dialog closes, or false/null. + * The ID of the button will be passed. Button ID will be one of the et2_dialog.*_BUTTON constants. + * The callback is _not_ called if the user closes the dialog with the X in the corner, or presses ESC. + * message - (plain) text to display + * title - Dialog title + * value (for prompt) + * buttons - et2_dialog BUTTONS_* constant, or an array of button settings + * dialog_type - et2_dialog *_MESSAGE constant + * icon - URL of icon + * + * Note that these methods will _not_ block program flow while waiting for user input. + * The user's input will be provided to the callback. + * + * You can also use the standard et2_createWidget() to create a custom dialog using an etemplate, even setting all + * the buttons yourself. + * + * var dialog = et2_createWidget("dialog",{ + * // If you use a template, the second parameter will be the value of the template, as if it were submitted. + * callback: function(button_id, value) {...}, // return false to prevent dialog closing + * buttons: [ + * // These ones will use the callback, just like normal + * {text: egw.lang("OK"),id:"OK", class="ui-priority-primary", default: true}, + * {text: egw.lang("Yes"),id:"Yes"}, + * {text: egw.lang("Sure"),id:"Sure"}, + * {text: egw.lang("Maybe"),click: function() { + * // If you override, 'this' will be the dialog DOMNode. + * // Things get more complicated. + * // Do what you like, but don't forget this line: + * jQuery(this).dialog("close") + * }, class="ui-state-error"}, + * + * ], + * title: 'Why would you want to do this?', + * template:"/egroupware/addressbook/templates/default/edit.xet", + * value: { content: {...default values}, sel_options: {...}...} + * }); + * + * @augments et2_widget + * @see http://api.jqueryui.com/dialog/ + */ +export class et2_dialog extends et2_widget { + static readonly _attributes: any = { + callback: { + name: "Callback", + type: "js", + description: "Callback function is called with the value when the dialog is closed", + "default": function (button_id) { + egw.debug("log", "Button ID: %d", button_id); + } + }, + beforeClose: { + name: "before close callback", + type: "js", + description: "Callback function before dialog is closed, return false to prevent that", + "default": function () { + } + }, + message: { + name: "Message", + type: "string", + description: "Dialog message (plain text, no html)", + "default": "Somebody forgot to set this..." + }, + dialog_type: { + name: "Dialog type", + type: "integer", + description: "To use a pre-defined dialog style, use et2_dialog.ERROR_MESSAGE, INFORMATION_MESSAGE,WARNING_MESSAGE,QUESTION_MESSAGE,PLAIN_MESSAGE constants. Default is et2_dialog.PLAIN_MESSAGE", + "default": 0 //this.PLAIN_MESSAGE + }, + buttons: { + name: "Buttons", + type: "any", + "default": 0, //this.BUTTONS_OK, + description: "Buttons that appear at the bottom of the dialog. You can use the constants et2_dialog.BUTTONS_OK, BUTTONS_YES_NO, BUTTONS_YES_NO_CANCEL, BUTTONS_OK_CANCEL, or pass in an array for full control" + }, + icon: { + name: "Icon", + type: "string", + description: "URL of an icon for the dialog. If omitted, an icon based on dialog_type will be used.", + "default": "" + }, + title: { + name: "Title", + type: "string", + description: "Title for the dialog box (plain text, no html)", + "default": "" + }, + modal: { + name: "Modal", + type: "boolean", + description: "Prevent the user from interacting with the page", + "default": true + }, + resizable: { + name: "Resizable", + type: "boolean", + description: "Allow the user to resize the dialog", + "default": true + }, + value: { + "name": "Value", + "description": "The (default) value of the dialog. Use with template.", + "type": "any", + "default": et2_no_init + }, + template: { + "name": "Template", + "description": "Instead of displaying a simple message, a full template can be loaded instead. Set defaults with value.", + "type": "string", + "default": et2_no_init + }, + minWidth: { + name: "minimum width", + type: "integer", + description: "Define minimum width of dialog", + "default": 0 + }, + minHeight: { + name: "minimum height", + type: "integer", + description: "Define minimum height of dialog", + "default": 0 + }, + width: { + name: "width", + type: "string", + description: "Define width of dialog, the default is auto", + "default": 'auto' + }, + height: { + name: "height", + type: "string", + description: "Define width of dialog, the default is auto", + "default": 'auto' + }, + position: { + name: "position", + type: "string", + description: "Define position of dialog in the main window", + default: "center" + } + }; + + /** + * Details for dialog type options + */ + private readonly _dialog_types: any = [ + //PLAIN_MESSAGE: 0 + "", + //INFORMATION_MESSAGE: 1, + "dialog_info", + //QUESTION_MESSAGE: 2, + "dialog_help", + //WARNING_MESSAGE: 3, + "dialog_warning", + //ERROR_MESSAGE: 4, + "dialog_error" + ]; + + private readonly _buttons: any = [ + /* + Pre-defined Button combos + - button ids copied from et2_dialog static, since the constants are not defined yet + - image get replaced by 'style="background-image: url('+egw.image(image)+')' for an image prefixing text + */ + //BUTTONS_OK: 0, + [{"button_id": 1, "text": 'ok', id: 'dialog[ok]', image: 'check', "default": true}], + //BUTTONS_OK_CANCEL: 1, + [ + {"button_id": 1, "text": 'ok', id: 'dialog[ok]', image: 'check', "default": true}, + {"button_id": 0, "text": 'cancel', id: 'dialog[cancel]', image: 'cancel'} + ], + //BUTTONS_YES_NO: 2, + [ + {"button_id": 2, "text": 'yes', id: 'dialog[yes]', image: 'check', "default": true}, + {"button_id": 3, "text": 'no', id: 'dialog[no]', image: 'cancelled'} + ], + //BUTTONS_YES_NO_CANCEL: 3, + [ + {"button_id": 2, "text": 'yes', id: 'dialog[yes]', image: 'check', "default": true}, + {"button_id": 3, "text": 'no', id: 'dialog[no]', image: 'cancelled'}, + {"button_id": 0, "text": 'cancel', id: 'dialog[cancel]', image: 'cancel'} + ] + ]; + + /** + * Types + * @constant + */ + public static PLAIN_MESSAGE: number = 0; + public static INFORMATION_MESSAGE: number = 1; + public static QUESTION_MESSAGE: number = 2; + public static WARNING_MESSAGE: number = 3; + public static ERROR_MESSAGE: number = 4; + + /* Pre-defined Button combos */ + public static BUTTONS_OK: number = 0; + public static BUTTONS_OK_CANCEL: number = 1; + public static BUTTONS_YES_NO: number = 2; + public static BUTTONS_YES_NO_CANCEL: number = 3; + + /* Button constants */ + public static CANCEL_BUTTON: number = 0; + public static OK_BUTTON: number = 1; + public static YES_BUTTON: number = 2; + public static NO_BUTTON: number = 3; + + div: JQuery = null; + template: any = null; + + constructor(_parent?, _attrs? : WidgetConfig, _child? : object) { + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_dialog._attributes, _child || {})); + + // Define this as null to avoid breaking any hierarchies (eg: destroy()) + if (this.getParent() != null) this.getParent().removeChild(this); + + // Button callbacks need a reference to this + let self = this; + for (let i = 0; i < this._buttons.length; i++) { + for (let j = 0; j < this._buttons[i].length; j++) { + this._buttons[i][j].click = (function (id) { + return function (event) { + self.click(event.target, id); + }; + })(this._buttons[i][j].button_id); + // translate button texts, as translations are not available before + this._buttons[i][j].text = egw.lang(this._buttons[i][j].text); + } + } + + this.div = jQuery(document.createElement("div")); + + this._createDialog(); + } + + /** + * Clean up dialog + */ + destroy() { + if (this.div != null) { + // Un-dialog the dialog + this.div.dialog("destroy"); + + if (this.template) { + this.template.clear(); + this.template = null; + } + + this.div = null; + } + + // Call the inherited constructor + super.destroy(); + } + + /** + * Internal callback registered on all standard buttons. + * The provided callback is called after the dialog is closed. + * + * @param target DOMNode The clicked button + * @param button_id integer The ID of the clicked button + */ + click(target: HTMLElement, button_id: number) { + if (this.options.callback) { + if (this.options.callback.call(this, button_id, this.get_value()) === false) return; + } + // Triggers destroy too + this.div.dialog("close"); + } + + /** + * Returns the values of any widgets in the dialog. This does not include + * the buttons, which are only supplied for the callback. + */ + get_value() { + var value = this.options.value; + if (this.template) { + value = this.template.getValues(this.template.widgetContainer); + } + return value; + } + + /** + * Set the displayed prompt message + * + * @param {string} message New message for the dialog + */ + set_message(message) { + this.options.message = message; + + this.div.empty() + .append("") + .append(jQuery('
').text(message)); + } + + /** + * Set the dialog type to a pre-defined type + * + * @param {integer} type constant from et2_dialog + */ + set_dialog_type(type) { + if (this.options.dialog_type != type && typeof this._dialog_types[type] == "string") { + this.options.dialog_type = type; + } + this.set_icon(this._dialog_types[type] ? egw.image(this._dialog_types[type]) : ""); + } + + /** + * Set the icon for the dialog + * + * @param {string} icon_url + */ + set_icon(icon_url) { + if (icon_url == "") { + jQuery("img.dialog_icon", this.div).hide(); + } else { + jQuery("img.dialog_icon", this.div).show().attr("src", icon_url); + } + } + + /** + * Set the dialog buttons + * + * Use either the pre-defined options in et2_dialog, or an array + * @see http://api.jqueryui.com/dialog/#option-buttons + * @param {array} buttons + */ + set_buttons(buttons) { + this.options.buttons = buttons; + if (buttons instanceof Array) { + for (var i = 0; i < buttons.length; i++) { + var button = buttons[i]; + if (!button.click) { + button.click = jQuery.proxy(this.click, this, null, button.id); + } + // set a default background image and css class based on buttons id + if (button.id && typeof button.class == 'undefined') { + for (var name in et2_button.default_classes) { + if (button.id.match(et2_button.default_classes[name])) { + button.class = (typeof button.class == 'undefined' ? '' : button.class + ' ') + name; + break; + } + } + } + if (button.id && typeof button.image == 'undefined' && typeof button.style == 'undefined') { + for (var name in et2_button.default_background_images) { + if (button.id.match(et2_button.default_background_images[name])) { + button.image = name; + break; + } + } + } + if (button.image) { + button.style = 'background-image: url(' + this.egw().image(button.image, 'api') + ')'; + delete button.image; + } + } + } + + // If dialog already created, update buttons + if (this.div.data('ui-dialog')) { + this.div.dialog("option", "buttons", buttons); + + // Focus default button so enter works + jQuery('.ui-dialog-buttonpane button[default]', this.div.parent()).focus(); + } + } + + /** + * Set the dialog title + * + * @param {string} title New title for the dialog + */ + set_title(title) { + this.options.title = title; + this.div.dialog("option", "title", title); + } + + /** + * Block interaction with the page behind the dialog + * + * @param {boolean} modal Block page behind dialog + */ + set_modal(modal) { + this.options.modal = modal; + this.div.dialog("option", "modal", modal); + } + + /** + * Load an etemplate into the dialog + * + * @param template String etemplate file name + */ + set_template(template) { + if (this.template && this.options.template != template) { + this.template.clear(); + } + + this.template = new etemplate2(this.div[0], false); + if (template.indexOf('.xet') > 0) { + // File name provided, fetch from server + this.template.load("", template, this.options.value || {content: {}}, jQuery.proxy(function () { + // Set focus to the first input + jQuery('input', this.div).first().focus(); + }, this)); + } else { + // Just template name, it better be loaded already + this.template.load(template, '', this.options.value || {}, + // true: do NOT call et2_ready, as it would overwrite this.et2 in app.js + undefined, undefined, true); + } + // set template-name as id, to allow to style dialogs + this.div.children().attr('id', template.replace(/^(.*\/)?([^/]+)(\.xet)?$/, '$2').replace(/\./g, '-')); + } + + /** + * Actually create and display the dialog + */ + _createDialog() { + if (this.options.template) { + this.set_template(this.options.template); + } else { + this.set_message(this.options.message); + this.set_dialog_type(this.options.dialog_type); + } + this.set_buttons(typeof this.options.buttons == "number" ? this._buttons[this.options.buttons] : this.options.buttons); + + let options = { + // Pass the internal object, not the option + buttons: this.options.buttons, + modal: this.options.modal, + resizable: this.options.resizable, + minWidth: this.options.minWidth, + minHeight: this.options.minHeight, + maxWidth: 640, + height: this.options.height, + title: this.options.title, + open: function () { + // Focus default button so enter works + jQuery(this).parents('.ui-dialog-buttonpane button[default]').focus(); + window.setTimeout(function () { + jQuery(this).dialog('option', 'position', { + my: "center", + at: "center", + of: window + }); + }.bind(this), 0); + }, + close: jQuery.proxy(function () { + this.destroy(); + }, this), + beforeClose: this.options.beforeClose, + closeText: this.egw().lang('close'), + position: {my: this.options.position, at: this.options.position, of: window} + }; + // Leaving width unset lets it size itself according to contents + if (this.options.width) { + options['width'] = this.options.width; + } + + this.div.dialog(options); + + // Make sure dialog is wide enough for the title + // Arbitrary numbers that seem to work nicely. + let title_width = 20 + 10 * this.options.title.length; + if (this.div.width() < title_width && this.options.title.trim()) { + // Auto-sizing chopped the title + this.div.dialog('option', 'width', title_width); + } + } + + /** + * Create a parent to inject application specific egw object with loaded translations into et2_dialog + * + * @param {string|egw} _egw_or_appname egw object with already loaded translations or application name to load translations for + */ + static _create_parent(_egw_or_appname? : string | IegwAppLocal) { + if (typeof _egw_or_appname == 'undefined') { + // @ts-ignore + _egw_or_appname = egw_appName; + } + // create a dummy parent with a correct reference to an application specific egw object + let parent = new et2_widget(); + // if egw object is passed in because called from et2, just use it + if (typeof _egw_or_appname != 'string') { + parent.setApiInstance(_egw_or_appname); + } + // otherwise use given appname to create app-specific egw instance and load default translations + else { + parent.setApiInstance(egw(_egw_or_appname)); + parent.egw().langRequireApp(parent.egw().window, _egw_or_appname); + } + return parent; + } + + /** + * Show a confirmation dialog + * + * @param {function} _callback Function called when the user clicks a button. The context will be the et2_dialog widget, and the button constant is passed in. + * @param {string} _message Message to be place in the dialog. + * @param {string} _title Text in the top bar of the dialog. + * @param _value passed unchanged to callback as 2. parameter + * @param {integer|array} _buttons One of the BUTTONS_ constants defining the set of buttons at the bottom of the box + * @param {integer} _type One of the message constants. This defines the style of the message. + * @param {string} _icon URL of an icon to display. If not provided, a type-specific icon will be used. + * @param {string|egw} _egw_or_appname egw object with already laoded translations or application name to load translations for + */ + static show_dialog(_callback? : Function, _message? : string, _title? : string, _value? : object, _buttons?, _type? : number, _icon? : string, _egw_or_appname? : string | IegwAppLocal) { + let parent = et2_dialog._create_parent(_egw_or_appname); + + // Just pass them along, widget handles defaults & missing + return et2_createWidget("dialog", { + callback: _callback || function () { + }, + message: _message, + title: _title || parent.egw().lang('Confirmation required'), + buttons: typeof _buttons != 'undefined' ? _buttons : et2_dialog.BUTTONS_YES_NO, + dialog_type: typeof _type != 'undefined' ? _type : et2_dialog.QUESTION_MESSAGE, + icon: _icon, + value: _value, + width: 'auto' + }, parent); + }; + + /** + * Show an alert message with OK button + * + * @param {string} _message Message to be place in the dialog. + * @param {string} _title Text in the top bar of the dialog. + * @param {integer} _type One of the message constants. This defines the style of the message. + */ + static alert(_message? : string, _title? : string, _type?) { + let parent = et2_dialog._create_parent(et2_dialog._create_parent().egw()); + et2_createWidget("dialog", { + callback: function () { + }, + message: _message, + title: _title, + buttons: et2_dialog.BUTTONS_OK, + dialog_type: _type || et2_dialog.INFORMATION_MESSAGE + }, parent); + } + + /** + * Show a prompt dialog + * + * @param {function} _callback Function called when the user clicks a button. The context will be the et2_dialog widget, and the button constant is passed in. + * @param {string} _message Message to be place in the dialog. + * @param {string} _title Text in the top bar of the dialog. + * @param {string} _value for prompt, passed to callback as 2. parameter + * @param {integer|array} _buttons One of the BUTTONS_ constants defining the set of buttons at the bottom of the box + * @param {string|egw} _egw_or_appname egw object with already laoded translations or application name to load translations for + */ + static show_prompt(_callback, _message, _title?, _value?, _buttons?, _egw_or_appname?) { + var callback = _callback; + // Just pass them along, widget handles defaults & missing + return et2_createWidget("dialog", { + callback: function (_button_id, _value) { + if (typeof callback == "function") { + callback.call(this, _button_id, _value.value); + } + }, + title: _title || egw.lang('Input required'), + buttons: _buttons || et2_dialog.BUTTONS_OK_CANCEL, + value: { + content: { + value: _value, + message: _message + } + }, + template: egw.webserverUrl + '/api/templates/default/prompt.xet', + class: "et2_prompt" + }, et2_dialog._create_parent(_egw_or_appname)); + } + + /** + * Method to build a confirmation dialog only with + * YES OR NO buttons and submit content back to server + * + * @param {widget} _senders widget that has been clicked + * @param {String} _dialogMsg message shows in dialog box + * @param {String} _titleMsg message shows as a title of the dialog box + * @param {Bool} _postSubmit true: use postSubmit instead of submit + * + * @description submit the form contents including the button that has been pressed + */ + static confirm(_senders, _dialogMsg, _titleMsg, _postSubmit) { + var senders = _senders; + var buttonId = _senders.id; + var dialogMsg = (typeof _dialogMsg != "undefined") ? _dialogMsg : ''; + var titleMsg = (typeof _titleMsg != "undefined") ? _titleMsg : ''; + var egw = _senders instanceof et2_widget ? _senders.egw() : et2_dialog._create_parent().egw(); + var callbackDialog = function (button_id) { + if (button_id == et2_dialog.YES_BUTTON) { + if (_postSubmit) { + senders.getRoot().getInstanceManager().postSubmit(buttonId); + } else { + senders.getRoot().getInstanceManager().submit(buttonId); + } + } + }; + et2_dialog.show_dialog(callbackDialog, egw.lang(dialogMsg), egw.lang(titleMsg), {}, + et2_dialog.BUTTONS_YES_NO, et2_dialog.WARNING_MESSAGE, undefined, egw); + }; + + + /** + * Show a dialog for a long-running, multi-part task + * + * Given a server url and a list of parameters, this will open a dialog with + * a progress bar, asynchronously call the url with each parameter, and update + * the progress bar. + * Any output from the server will be displayed in a box. + * + * When all tasks are done, the callback will be called with boolean true. It will + * also be called if the user clicks a button (OK or CANCEL), so be sure to + * check to avoid executing more than intended. + * + * @param {function} _callback Function called when the user clicks a button, + * or when the list is done processing. The context will be the et2_dialog + * widget, and the button constant is passed in. + * @param {string} _message Message to be place in the dialog. Usually just + * text, but DOM nodes will work too. + * @param {string} _title Text in the top bar of the dialog. + * @param {string} _menuaction the menuaction function which should be called and + * which handles the actual request. If the menuaction is a full featured + * url, this one will be used instead. + * @param {Array[]} _list - List of parameters, one for each call to the + * address. Multiple parameters are allowed, in an array. + * @param {string|egw} _egw_or_appname egw object with already laoded translations or application name to load translations for + * + * @return {et2_dialog} + */ + static long_task(_callback, _message, _title, _menuaction, _list, _egw_or_appname) + { + let parent = et2_dialog._create_parent(_egw_or_appname); + let egw = parent.egw(); + + // Special action for cancel + let buttons = [ + {"button_id": et2_dialog.OK_BUTTON, "text": egw.lang('ok'), "default": true, "disabled": true}, + { + "button_id": et2_dialog.CANCEL_BUTTON, "text": egw.lang('cancel'), click: function () { + // Cancel run + cancel = true; + jQuery("button[button_id=" + et2_dialog.CANCEL_BUTTON + "]", dialog.div.parent()).button("disable"); + update.call(_list.length, ''); + } + } + ]; + let dialog = et2_createWidget("dialog", { + template: egw.webserverUrl + '/api/templates/default/long_task.xet', + value: { + content: { + message: _message + } + }, + callback: function (_button_id, _value) { + if (_button_id == et2_dialog.CANCEL_BUTTON) { + cancel = true; + } + if (typeof _callback == "function") { + _callback.call(this, _button_id, _value.value); + } + }, + title: _title || egw.lang('please wait...'), + buttons: buttons + }, parent); + + // OK starts disabled + jQuery("button[button_id=" + et2_dialog.OK_BUTTON + "]", dialog.div.parent()).button("disable"); + + let log = null; + let progressbar = null; + let cancel = false; + let totals = { + success: 0, + skipped: 0, + failed: 0, + widget: null + }; + + // Updates progressbar & log, calls next step + let update = function (response) { + // context is index + let index = this || 0; + + progressbar.set_value(100 * (index / _list.length)); + progressbar.set_label(index + ' / ' + _list.length); + + // Display response information + switch (response.type) { + case 'error': + jQuery("
") + .text(response.data) + .appendTo(log); + + totals.failed++; + + // Ask to retry / ignore / abort + et2_createWidget("dialog", { + callback: function (button) { + switch (button) { + case 'dialog[cancel]': + cancel = true; + return update.call(index, ''); + case 'dialog[skip]': + // Continue with next index + totals.skipped++; + return update.call(index, ''); + default: + // Try again with previous index + return update.call(index - 1, ''); + } + + }, + message: response.data, + title: '', + buttons: [ + // These ones will use the callback, just like normal + {text: egw.lang("Abort"), id: 'dialog[cancel]'}, + {text: egw.lang("Retry"), id: 'dialog[retry]'}, + {text: egw.lang("Skip"), id: 'dialog[skip]', class: "ui-priority-primary", default: true} + ], + dialog_type: et2_dialog.ERROR_MESSAGE + }, parent); + // Early exit + return; + default: + if (response) { + totals.success++; + jQuery("
") + .text(response) + .appendTo(log); + } + } + // Scroll to bottom + let height = log[0].scrollHeight; + log.scrollTop(height); + + // Update totals + totals.widget.set_value(egw.lang( + "Total: %1 Successful: %2 Failed: %3 Skipped: %4", + _list.length, totals.success, totals.failed, totals.skipped + )); + + // Fire next step + if (!cancel && index < _list.length) { + var parameters = _list[index]; + if (typeof parameters != 'object') parameters = [parameters]; + + // Async request, we'll take the next step in the callback + // We can't pass index = 0, it looks like false and causes issues + egw.json(_menuaction, parameters, update, index + 1, true, index + 1).sendRequest(); + } else { + // All done + if (!cancel) progressbar.set_value(100); + jQuery("button[button_id=" + et2_dialog.CANCEL_BUTTON + "]", dialog.div.parent()).button("disable"); + jQuery("button[button_id=" + et2_dialog.OK_BUTTON + "]", dialog.div.parent()).button("enable"); + if (!cancel && typeof _callback == "function") { + _callback.call(dialog, true, response); + } + } + }; + + jQuery(dialog.template.DOMContainer).on('load', function () { + // Get access to template widgets + log = jQuery(dialog.template.widgetContainer.getWidgetById('log').getDOMNode()); + progressbar = dialog.template.widgetContainer.getWidgetById('progressbar'); + progressbar.set_label('0 / ' + _list.length); + totals.widget = dialog.template.widgetContainer.getWidgetById('totals'); + + // Start + window.setTimeout(function () { + update.call(0, ''); + }, 0); + }); + + return dialog; + } +} +et2_register_widget(et2_dialog, ["dialog"]); \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_diff.js b/api/js/etemplate/et2_widget_diff.js index 24ab728b27..575f18c5bb 100644 --- a/api/js/etemplate/et2_widget_diff.js +++ b/api/js/etemplate/et2_widget_diff.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - JS Diff object * @@ -9,186 +10,180 @@ * @copyright Nathan Gray 2012 * @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 - /vendor/bower-asset/jquery/dist/jquery.js; - /vendor/bower-asset/jquery-ui/jquery-ui.js; - /vendor/bower-asset/diff2html/dist/diff2html.min.js; - et2_core_valueWidget; + /vendor/bower-asset/jquery/dist/jquery.js; + /vendor/bower-asset/jquery-ui/jquery-ui.js; + /vendor/bower-asset/diff2html/dist/diff2html.min.js; + et2_core_valueWidget; */ - +var et2_core_widget_1 = require("./et2_core_widget"); +var et2_core_inheritance_1 = require("./et2_core_inheritance"); +var et2_core_valueWidget_1 = require("./et2_core_valueWidget"); /** * Class that displays the diff between two [text] values * * @augments et2_valueWidget */ -var et2_diff = (function(){ "use strict"; return et2_valueWidget.extend([et2_IDetachedDOM], -{ - attributes: { - "value": { - "type": "any" - } - }, - - diff_options: { - "inputFormat":"diff", - "matching": "words" - }, - - /** - * Constructor - * - * @memberOf et2_diff - */ - init: function() { - this._super.apply(this, arguments); - this.mini = true; - - // included via etemplate2.css - //this.egw().includeCSS('../../../vendor/bower-asset/dist/dist2html.css'); - this.div = document.createElement("div"); - jQuery(this.div).addClass('et2_diff'); - }, - - set_value: function(value) { - jQuery(this.div).empty(); - if(typeof value == 'string') { - - // Diff2Html likes to have files, we don't have them - if(value.indexOf('---') !== 0) - { - value = "--- diff\n+++ diff\n"+value; - } - var diff = Diff2Html.getPrettyHtml(value, this.diff_options); - // var ui = new Diff2HtmlUI({diff: diff}); - // ui.draw(jQuery(this.div), this.diff_options); - jQuery(this.div).append(diff); - } - else if(typeof value != 'object') - { - jQuery(this.div).append(value); - } - this.check_mini(); - }, - - check_mini: function() { - if(!this.mini) - { - return false; - } - var view = jQuery(this.div).children(); - this.minify(view); - var self = this; - jQuery(' ') - .appendTo(self.div) - .css("cursor", "pointer") - .click({diff: view, div: self.div, label: self.options.label}, function(e) { - var diff = e.data.diff; - var div = e.data.div; - self.un_minify(diff); - var dialog_div = jQuery('
') - .append(diff); - - dialog_div.dialog({ - title: e.data.label, - width: 'auto', - autoResize: true, - modal: true, - buttons: [{text: self.egw().lang('ok'), click: function() {jQuery(this).dialog("close");}}], - open: function() { - if(jQuery(this).parent().height() > jQuery(window).height()) - { - jQuery(this).height(jQuery(window).height() *0.7); - } - jQuery(this).addClass('et2_diff').dialog({position: "center"}); - }, - close: function(event, ui) { - // Need to destroy the dialog, etemplate widget needs divs back where they were - dialog_div.dialog("destroy"); - self.minify(this); - - // Put it back where it came from, or et2 will error when clear() is called - diff.prependTo(div); - } - }); - }); - }, - set_label: function(_label) { - this.options.label = _label; - - }, - - /** - * Make the diff into a mini-diff - * - * @param {DOMNode|String} view - */ - minify: function(view) { - view = jQuery(view) - .addClass('mini') - // Dialog changes these, if resized - .width('100%').css('height', 'inherit') - .show(); - jQuery('th', view).hide(); - jQuery('td.equal',view).hide() - .prevAll().hide(); - }, - - /** - * Expand mini-diff - * - * @param {DOMNode|String} view - */ - un_minify: function(view) { - jQuery(view).removeClass('mini').show(); - jQuery('th',view).show(); - jQuery('td.equal',view).show(); - }, - - /** - * Code for implementing et2_IDetachedDOM - * Fast-clonable read-only widget that only deals with DOM nodes, not the widget tree - */ - - /** - * Build a list of attributes which can be set when working in the - * "detached" mode in the _attrs array which is provided - * by the calling code. - * - * @param {object} _attrs - */ - getDetachedAttributes: function(_attrs) { - _attrs.push("value", "label"); - }, - - /** - * Returns an array of DOM nodes. The (relativly) same DOM-Nodes have to be - * passed to the "setDetachedAttributes" function in the same order. - */ - getDetachedNodes: function() { - return [this.div]; - }, - - /** - * Sets the given associative attribute->value array and applies the - * attributes to the given DOM-Node. - * - * @param _nodes is an array of nodes which has 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) { - this.div = _nodes[0]; - if(typeof _values['label'] != 'undefined') - { - this.set_label(_values['label']); - } - if(typeof _values['value'] != 'undefined') - { - this.set_value(_values['value']); - } - } -});}).call(this); -et2_register_widget(et2_diff, ["diff"]); +var et2_diff = /** @class */ (function (_super) { + __extends(et2_diff, _super); + /** + * Constructor + */ + function et2_diff(_parent, _attrs, _child) { + var _this = + // Call the inherited constructor + _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_diff._attributes, _child || {})) || this; + _this.mini = true; + // included via etemplate2.css + //this.egw().includeCSS('../../../vendor/bower-asset/dist/dist2html.css'); + _this.div = document.createElement("div"); + jQuery(_this.div).addClass('et2_diff'); + return _this; + } + et2_diff.prototype.set_value = function (value) { + jQuery(this.div).empty(); + if (typeof value == 'string') { + // Diff2Html likes to have files, we don't have them + if (value.indexOf('---') !== 0) { + value = "--- diff\n+++ diff\n" + value; + } + // @ts-ignore + var diff = Diff2Html.getPrettyHtml(value, this.diff_options); + // var ui = new Diff2HtmlUI({diff: diff}); + // ui.draw(jQuery(this.div), this.diff_options); + jQuery(this.div).append(diff); + } + else if (typeof value != 'object') { + jQuery(this.div).append(value); + } + this.check_mini(); + }; + et2_diff.prototype.check_mini = function () { + if (!this.mini) { + return false; + } + var view = jQuery(this.div).children(); + this.minify(view); + var self = this; + jQuery(' ') + .appendTo(self.div) + .css("cursor", "pointer") + .click({ diff: view, div: self.div, label: self.options.label }, function (e) { + var diff = e.data.diff; + var div = e.data.div; + self.un_minify(diff); + var dialog_div = jQuery('
') + .append(diff); + dialog_div.dialog({ + title: e.data.label, + width: 'auto', + modal: true, + buttons: [{ text: self.egw().lang('ok'), click: function () { jQuery(this).dialog("close"); } }], + open: function () { + if (jQuery(this).parent().height() > jQuery(window).height()) { + jQuery(this).height(jQuery(window).height() * 0.7); + } + jQuery(this).addClass('et2_diff').dialog({ position: "center" }); + }, + close: function (event, ui) { + // Need to destroy the dialog, etemplate widget needs divs back where they were + dialog_div.dialog("destroy"); + self.minify(this); + // Put it back where it came from, or et2 will error when clear() is called + diff.prependTo(div); + } + }); + }); + }; + et2_diff.prototype.set_label = function (_label) { + this.options.label = _label; + }; + /** + * Make the diff into a mini-diff + * + * @param {DOMNode|String} view + */ + et2_diff.prototype.minify = function (view) { + view = jQuery(view) + .addClass('mini') + // Dialog changes these, if resized + .width('100%').css('height', 'inherit') + .show(); + jQuery('th', view).hide(); + jQuery('td.equal', view).hide() + .prevAll().hide(); + }; + /** + * Expand mini-diff + * + * @param {DOMNode|String} view + */ + et2_diff.prototype.un_minify = function (view) { + jQuery(view).removeClass('mini').show(); + jQuery('th', view).show(); + jQuery('td.equal', view).show(); + }; + /** + * Code for implementing et2_IDetachedDOM + * Fast-clonable read-only widget that only deals with DOM nodes, not the widget tree + */ + /** + * Build a list of attributes which can be set when working in the + * "detached" mode in the _attrs array which is provided + * by the calling code. + * + * @param {object} _attrs + */ + et2_diff.prototype.getDetachedAttributes = function (_attrs) { + _attrs.push("value", "label"); + }; + /** + * Returns an array of DOM nodes. The (relativly) same DOM-Nodes have to be + * passed to the "setDetachedAttributes" function in the same order. + */ + et2_diff.prototype.getDetachedNodes = function () { + return [this.div]; + }; + /** + * Sets the given associative attribute->value array and applies the + * attributes to the given DOM-Node. + * + * @param _nodes is an array of nodes which has 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. + */ + et2_diff.prototype.setDetachedAttributes = function (_nodes, _values) { + this.div = _nodes[0]; + if (typeof _values['label'] != 'undefined') { + this.set_label(_values['label']); + } + if (typeof _values['value'] != 'undefined') { + this.set_value(_values['value']); + } + }; + et2_diff._attributes = { + "value": { + "type": "any" + } + }; + return et2_diff; +}(et2_core_valueWidget_1.et2_valueWidget)); +exports.et2_diff = et2_diff; +et2_core_widget_1.et2_register_widget(et2_diff, ["diff"]); +//# sourceMappingURL=et2_widget_diff.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_diff.ts b/api/js/etemplate/et2_widget_diff.ts new file mode 100644 index 0000000000..b703637a17 --- /dev/null +++ b/api/js/etemplate/et2_widget_diff.ts @@ -0,0 +1,209 @@ +/** + * EGroupware eTemplate2 - JS Diff object + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage api + * @link http://www.egroupware.org + * @author Nathan Gray + * @copyright Nathan Gray 2012 + * @version $Id$ + */ + +/*egw:uses + /vendor/bower-asset/jquery/dist/jquery.js; + /vendor/bower-asset/jquery-ui/jquery-ui.js; + /vendor/bower-asset/diff2html/dist/diff2html.min.js; + et2_core_valueWidget; +*/ + +import {et2_register_widget, WidgetConfig} from "./et2_core_widget"; +import {ClassWithAttributes} from "./et2_core_inheritance"; +import {et2_valueWidget} from "./et2_core_valueWidget"; + +/** + * Class that displays the diff between two [text] values + * + * @augments et2_valueWidget + */ +export class et2_diff extends et2_valueWidget implements et2_IDetachedDOM +{ + static readonly _attributes = { + "value": { + "type": "any" + } + }; + + private readonly diff_options: { + "inputFormat":"diff", + "matching": "words" + }; + private div: HTMLDivElement; + private mini: boolean = true; + + /** + * Constructor + */ + constructor(_parent, _attrs? : WidgetConfig, _child? : object) + { + // Call the inherited constructor + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_diff._attributes, _child || {})); + + // included via etemplate2.css + //this.egw().includeCSS('../../../vendor/bower-asset/dist/dist2html.css'); + this.div = document.createElement("div"); + jQuery(this.div).addClass('et2_diff'); + } + + set_value( value) + { + jQuery(this.div).empty(); + if(typeof value == 'string') { + + // Diff2Html likes to have files, we don't have them + if(value.indexOf('---') !== 0) + { + value = "--- diff\n+++ diff\n"+value; + } + // @ts-ignore + var diff = Diff2Html.getPrettyHtml(value, this.diff_options); + // var ui = new Diff2HtmlUI({diff: diff}); + // ui.draw(jQuery(this.div), this.diff_options); + jQuery(this.div).append(diff); + } + else if(typeof value != 'object') + { + jQuery(this.div).append(value); + } + this.check_mini(); + } + + check_mini( ) + { + if(!this.mini) + { + return false; + } + var view = jQuery(this.div).children(); + this.minify(view); + var self = this; + jQuery(' ') + .appendTo(self.div) + .css("cursor", "pointer") + .click({diff: view, div: self.div, label: self.options.label}, function(e) { + var diff = e.data.diff; + var div = e.data.div; + self.un_minify(diff); + var dialog_div = jQuery('
') + .append(diff); + + dialog_div.dialog({ + title: e.data.label, + width: 'auto', + modal: true, + buttons: [{text: self.egw().lang('ok'), click: function() {jQuery(this).dialog("close");}}], + open( ) + { + if(jQuery(this).parent().height() > jQuery(window).height()) + { + jQuery(this).height(jQuery(window).height() *0.7); + } + jQuery(this).addClass('et2_diff').dialog({position: "center"}); + }, + close( event, ui) + { + // Need to destroy the dialog, etemplate widget needs divs back where they were + dialog_div.dialog("destroy"); + self.minify(this); + + // Put it back where it came from, or et2 will error when clear() is called + diff.prependTo(div); + } + }); + }); + } + set_label( _label) + { + this.options.label = _label; + + } + + /** + * Make the diff into a mini-diff + * + * @param {DOMNode|String} view + */ + minify( view) + { + view = jQuery(view) + .addClass('mini') + // Dialog changes these, if resized + .width('100%').css('height', 'inherit') + .show(); + jQuery('th', view).hide(); + jQuery('td.equal',view).hide() + .prevAll().hide(); + } + + /** + * Expand mini-diff + * + * @param {DOMNode|String} view + */ + un_minify( view) + { + jQuery(view).removeClass('mini').show(); + jQuery('th',view).show(); + jQuery('td.equal',view).show(); + } + + /** + * Code for implementing et2_IDetachedDOM + * Fast-clonable read-only widget that only deals with DOM nodes, not the widget tree + */ + + /** + * Build a list of attributes which can be set when working in the + * "detached" mode in the _attrs array which is provided + * by the calling code. + * + * @param {object} _attrs + */ + getDetachedAttributes(_attrs) + { + _attrs.push("value", "label"); + } + + /** + * Returns an array of DOM nodes. The (relativly) same DOM-Nodes have to be + * passed to the "setDetachedAttributes" function in the same order. + */ + getDetachedNodes() + { + return [this.div]; + } + + /** + * Sets the given associative attribute->value array and applies the + * attributes to the given DOM-Node. + * + * @param _nodes is an array of nodes which has 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, _values) + { + this.div = _nodes[0]; + if(typeof _values['label'] != 'undefined') + { + this.set_label(_values['label']); + } + if(typeof _values['value'] != 'undefined') + { + this.set_value(_values['value']); + } + } +} +et2_register_widget(et2_diff, ["diff"]); diff --git a/api/js/etemplate/et2_widget_dropdown_button.js b/api/js/etemplate/et2_widget_dropdown_button.js index 164e7c7b9c..f08c16eec3 100644 --- a/api/js/etemplate/et2_widget_dropdown_button.js +++ b/api/js/etemplate/et2_widget_dropdown_button.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - JS Dropdown Button object * @@ -9,13 +10,28 @@ * @copyright Nathan Gray 2013 * @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 - /vendor/bower-asset/jquery/dist/jquery.js; - /vendor/bower-asset/jquery-ui/jquery-ui.js; - et2_baseWidget; + /vendor/bower-asset/jquery/dist/jquery.js; + /vendor/bower-asset/jquery-ui/jquery-ui.js; + et2_baseWidget; */ - +var et2_core_inputWidget_1 = require("./et2_core_inputWidget"); +var et2_core_widget_1 = require("./et2_core_widget"); +var et2_core_inheritance_1 = require("./et2_core_inheritance"); /** * A split button - a button with a dropdown list * @@ -30,393 +46,337 @@ * * @augments et2_inputWidget */ -var et2_dropdown_button = (function(){ "use strict"; return et2_inputWidget.extend( -{ - attributes: { - "label": { - "name": "caption", - "type": "string", - "description": "Label of the button", - "translate": true, - "default": "Select..." - }, - "label_updates": { - "name": "Label updates", - "type": "boolean", - "description": "Button label updates when an option is selected from the menu", - "default": true - }, - "image": { - "name": "Icon", - "type": "string", - "description": "Add an icon" - }, - "ro_image": { - "name": "Read-only Icon", - "type": "string", - "description": "Use this icon instead of hiding for read-only" - }, - "onclick": { - "description": "JS code which gets executed when the button is clicked" - }, - "select_options": { - "type": "any", - "name": "Select options", - "default": {}, - "description": "Select options for dropdown. Can be a simple key => value list, or value can be full HTML", - // Skip normal initialization for this one - "ignore": true - }, - "accesskey": { - "name": "Access Key", - "type": "string", - "default": et2_no_init, - "description": "Alt + activates widget" - }, - "tabindex": { - "name": "Tab index", - "type": "integer", - "default": et2_no_init, - "description": "Specifies the tab order of a widget when the 'tab' button is used for navigating." - }, - // No such thing as a required button - "required": { - "ignore": true - } - }, - - internal_ids: { - div: "", - button: "", - menu: "" - }, - - div: null, - buttons: null, - button: null, - menu: null, - - /** - * Default menu, so there is something for the widget browser / editor to show - */ - default_menu: '', - - /** - * Constructor - * - * @memberOf et2_dropdown_button - */ - init: function() { - this._super.apply(this, arguments); - - this.clicked = false; - - var self = this; - - // Create the individual UI elements - - // Menu is a UL - this.menu = jQuery(this.default_menu).attr("id",this.internal_ids.menu) - .hide() - .menu({ - select: function(event,ui) { - self.onselect.call(self,event,ui.item); - } - }); - - this.buttons = jQuery(document.createElement("div")) - .addClass("et2_dropdown"); - - // Main "wrapper" div - this.div = jQuery(document.createElement("div")) - .attr("id", this.internal_ids.div) - .append(this.buttons) - .append(this.menu); - - // Left side - activates click action - this.button = jQuery(document.createElement("button")) - .attr("id", this.internal_ids.button) - .attr("type", "button") - .addClass("ui-widget ui-corner-left").removeClass("ui-corner-all") - .appendTo(this.buttons); - - // Right side - shows dropdown - this.arrow = jQuery(document.createElement("button")) - .addClass("ui-widget ui-corner-right").removeClass("ui-corner-all") - .attr("type", "button") - .click(function() { - // ignore click on readonly button - if (self.options.readonly) return false; - // Clicking it again hides menu - if(self.menu.is(":visible")) - { - self.menu.hide(); - return false; - } - // Show menu dropdown - var menu = self.menu.show().position({ - my: "left top", - at: "left bottom", - of: self.buttons - }); - // Hide menu if clicked elsewhere - jQuery( document ).one( "click", function() { - menu.hide(); - }); - return false; - }) - // This is the actual down arrow icon - .append("
") - .appendTo(this.buttons); - - // Common button UI - this.buttons.children("button") - .addClass("ui-state-default") - .hover( - function() {jQuery(this).addClass("ui-state-hover");}, - function() {jQuery(this).removeClass("ui-state-hover");} - ); - - // Icon - this.image = jQuery(document.createElement("img")); - - this.setDOMNode(this.div[0]); - }, - - destroy: function() { - // Destroy widget - if(this.menu && this.menu.data('ui-menu')) this.menu.menu("destroy"); - - // Null children - this.image = null; - this.button = null; - this.arrow = null; - this.buttons = null; - this.menu = null; - - // Remove - this.div.empty().remove(); - }, - - set_id: function(_id) { - this._super.apply(this, arguments); - - // Update internal IDs - not really needed since we refer by internal - // javascript reference, but good to keep up to date - this.internal_ids = { - div: this.dom_id + "_wrapper", - button: this.dom_id, - menu: this.dom_id + "_menu" - }; - for(var key in this.internal_ids) - { - if(this[key] == null) continue; - this[key].attr("id", this.internal_ids[key]); - } - }, - - /** - * Set if the button label changes to match the selected option - * - * @param updates boolean Turn updating on or off - */ - set_label_updates: function(updates) { - this.label_updates = updates; - }, - - set_accesskey: function(key) { - jQuery(this.node).attr("accesskey", key); - }, - set_ro_image: function(_image) { - if(this.options.readonly) - { - this.set_image(_image); - } - }, - - set_image: function(_image) { - if(!this.isInTree() || this.image == null) return; - var found_image = false; - if(!_image.trim()) - { - this.image.hide(); - } - else - { - this.image.show(); - } - - var src = this.egw().image(_image); - if(src) - { - this.image.attr("src", src); - found_image = true; - } - // allow url's too - else if (_image[0] == '/' || _image.substr(0,4) == 'http') - { - this.image.attr('src', _image); - found_image = true; - } - else - { - this.image.hide(); - } - }, - - /** - * Overwritten to maintain an internal clicked attribute - * - * @param _ev - * @returns {Boolean} - */ - click: function(_ev) { - // ignore click on readonly button - if (this.options.readonly) return false; - - this.clicked = true; - - if (!this._super.apply(this, arguments)) - { - this.clicked = false; - return false; - } - this.clicked = false; - return true; - }, - - onselect: function(event, selected_node) { - this.set_value(selected_node.attr("data-id")); - this.change(selected_node); - }, - - attachToDOM: function() { - this._super.apply(this, arguments); - - // Move the parent's handler to the button, or we can't tell the difference between the clicks - jQuery(this.node).unbind("click.et2_baseWidget"); - this.button.off().bind("click.et2_baseWidget", this, function(e) { - return e.data.click.call(e.data, this); - }); - }, - - set_label: function(_value) { - if (this.button) - { - this.label = _value; - - this.button.text(_value) - .prepend(this.image); - } - }, - - /** - * Set the options for the dropdown - * - * @param options Object ID => Label pairs - */ - set_select_options: function(options) { - this.menu.first().empty(); - - // Allow more complicated content, if passed - if(typeof options == "string") - { - this.menu.append(options); - } - else - { - var add_complex = function(node, options) - { - for(var key in options) - { - var item; - if(typeof options[key] == "string") - { - item = jQuery("
  • "+options[key]+"
  • "); - } - else if (options[key]["label"]) - { - item =jQuery("
  • "+options[key]["label"]+"
  • "); - } - // Optgroup - else - { - item = jQuery("
  • "+key+"
  • "); - add_complex(node.append("
      "), options[key]); - } - node.append(item); - if(item && options[key].icon) - { - // we supply a applicable class for item images - jQuery('a',item).prepend(''); - } - } - } - add_complex(this.menu.first(), options); - } - this.menu.menu("refresh"); - }, - - /** - * Set tab index - */ - set_tabindex: function(index) { - jQuery(this.button).attr("tabindex", index); - }, - - set_value: function(new_value) { - var menu_item = jQuery("[data-id='"+new_value+"']",this.menu); - if(menu_item.length) - { - this.value = new_value; - if(this.label_updates) - { - this.set_label(menu_item.text()); - } - } - else - { - this.value = null; - if(this.label_updates) - { - this.set_label(this.options.label); - } - } - }, - - getValue: function() { - return this.value; - }, - - /** - * Set options.readonly - * - * @param {boolean} _ro - */ - set_readonly: function(_ro) - { - if (_ro != this.options.readonly) - { - this.options.readonly = _ro; - - // don't make readonly dropdown buttons clickable - if (this.buttons) - { - this.buttons.find('button') - .toggleClass('et2_clickable', !_ro) - .toggleClass('et2_button_ro', _ro) - .css('cursor', _ro ? 'default' : 'pointer'); - } - } - } -});}).call(this); -et2_register_widget(et2_dropdown_button, ["dropdown_button"]); - +var et2_dropdown_button = /** @class */ (function (_super) { + __extends(et2_dropdown_button, _super); + /** + * Constructor + * + * @memberOf et2_dropdown_button + */ + function et2_dropdown_button(_parent, _attrs, _child) { + var _this = _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_dropdown_button._attributes, _child || {})) || this; + _this.internal_ids = { + div: "", + button: "", + menu: "" + }; + _this.div = null; + _this.buttons = null; + _this.button = null; + _this.arrow = null; + _this.menu = null; + _this.image = null; + _this.clicked = false; + _this.label_updates = true; + _this.value = null; + /** + * Default menu, so there is something for the widget browser / editor to show + */ + _this.default_menu = ''; + _this.clicked = false; + var self = _this; + // Create the individual UI elements + // Menu is a UL + _this.menu = jQuery(_this.default_menu).attr("id", _this.internal_ids.menu) + .hide() + .menu({ + select: function (event, ui) { + self.onselect.call(self, event, ui.item); + } + }); + _this.buttons = jQuery(document.createElement("div")) + .addClass("et2_dropdown"); + // Main "wrapper" div + _this.div = jQuery(document.createElement("div")) + .attr("id", _this.internal_ids.div) + .append(_this.buttons) + .append(_this.menu); + // Left side - activates click action + _this.button = jQuery(document.createElement("button")) + .attr("id", _this.internal_ids.button) + .attr("type", "button") + .addClass("ui-widget ui-corner-left").removeClass("ui-corner-all") + .appendTo(_this.buttons); + // Right side - shows dropdown + _this.arrow = jQuery(document.createElement("button")) + .addClass("ui-widget ui-corner-right").removeClass("ui-corner-all") + .attr("type", "button") + .click(function () { + // ignore click on readonly button + if (self.options.readonly) + return false; + // Clicking it again hides menu + if (self.menu.is(":visible")) { + self.menu.hide(); + return false; + } + // Show menu dropdown + var menu = self.menu.show().position({ + my: "left top", + at: "left bottom", + of: self.buttons + }); + // Hide menu if clicked elsewhere + jQuery(document).one("click", function () { + menu.hide(); + }); + return false; + }) + // This is the actual down arrow icon + .append("
      ") + .appendTo(_this.buttons); + // Common button UI + _this.buttons.children("button") + .addClass("ui-state-default") + .hover(function () { jQuery(this).addClass("ui-state-hover"); }, function () { jQuery(this).removeClass("ui-state-hover"); }); + // Icon + _this.image = jQuery(document.createElement("img")); + _this.setDOMNode(_this.div[0]); + return _this; + } + et2_dropdown_button.prototype.destroy = function () { + // Destroy widget + if (this.menu && this.menu.data('ui-menu')) + this.menu.menu("destroy"); + // Null children + this.image = null; + this.button = null; + this.arrow = null; + this.buttons = null; + this.menu = null; + // Remove + this.div.empty().remove(); + }; + et2_dropdown_button.prototype.set_id = function (_id) { + _super.prototype.set_id.call(this, _id); + // Update internal IDs - not really needed since we refer by internal + // javascript reference, but good to keep up to date + this.internal_ids = { + div: this.dom_id + "_wrapper", + button: this.dom_id, + menu: this.dom_id + "_menu" + }; + for (var key in this.internal_ids) { + if (this[key] == null) + continue; + this[key].attr("id", this.internal_ids[key]); + } + }; + /** + * Set if the button label changes to match the selected option + * + * @param updates boolean Turn updating on or off + */ + et2_dropdown_button.prototype.set_label_updates = function (updates) { + this.label_updates = updates; + }; + et2_dropdown_button.prototype.set_accesskey = function (key) { + jQuery(this.node).attr("accesskey", key); + }; + et2_dropdown_button.prototype.set_ro_image = function (_image) { + if (this.options.readonly) { + this.set_image(_image); + } + }; + et2_dropdown_button.prototype.set_image = function (_image) { + if (!this.isInTree() || this.image == null) + return; + if (!_image.trim()) { + this.image.hide(); + } + else { + this.image.show(); + } + var src = this.egw().image(_image); + if (src) { + this.image.attr("src", src); + } + // allow url's too + else if (_image[0] == '/' || _image.substr(0, 4) == 'http') { + this.image.attr('src', _image); + } + else { + this.image.hide(); + } + }; + /** + * Overwritten to maintain an internal clicked attribute + * + * @param _ev + * @returns {Boolean} + */ + et2_dropdown_button.prototype.click = function (_ev) { + // ignore click on readonly button + if (this.options.readonly) + return false; + this.clicked = true; + if (!_super.prototype.click.call(this, _ev)) { + this.clicked = false; + return false; + } + this.clicked = false; + return true; + }; + et2_dropdown_button.prototype.onselect = function (event, selected_node) { + this.set_value(selected_node.attr("data-id")); + this.change(selected_node); + }; + et2_dropdown_button.prototype.attachToDOM = function () { + var res = _super.prototype.attachToDOM.call(this); + // Move the parent's handler to the button, or we can't tell the difference between the clicks + jQuery(this.node).unbind("click.et2_baseWidget"); + this.button.off().bind("click.et2_baseWidget", this, function (e) { + return e.data.click.call(e.data, this); + }); + return res; + }; + et2_dropdown_button.prototype.set_label = function (_value) { + if (this.button) { + this.label = _value; + this.button.text(_value) + .prepend(this.image); + } + }; + /** + * Set the options for the dropdown + * + * @param options Object ID => Label pairs + */ + et2_dropdown_button.prototype.set_select_options = function (options) { + this.menu.first().empty(); + // Allow more complicated content, if passed + if (typeof options == "string") { + this.menu.append(options); + } + else { + var add_complex_1 = function (node, options) { + for (var key in options) { + var item = void 0; + if (typeof options[key] == "string") { + item = jQuery("
    • " + options[key] + "
    • "); + } + else if (options[key]["label"]) { + item = jQuery("
    • " + options[key]["label"] + "
    • "); + } + // Optgroup + else { + item = jQuery("
    • " + key + "
    • "); + add_complex_1(node.append("
        "), options[key]); + } + node.append(item); + if (item && options[key].icon) { + // we supply a applicable class for item images + jQuery('a', item).prepend(''); + } + } + }; + add_complex_1(this.menu.first(), options); + } + this.menu.menu("refresh"); + }; + /** + * Set tab index + */ + et2_dropdown_button.prototype.set_tabindex = function (index) { + jQuery(this.button).attr("tabindex", index); + }; + et2_dropdown_button.prototype.set_value = function (new_value) { + var menu_item = jQuery("[data-id='" + new_value + "']", this.menu); + if (menu_item.length) { + this.value = new_value; + if (this.label_updates) { + this.set_label(menu_item.text()); + } + } + else { + this.value = null; + if (this.label_updates) { + this.set_label(this.options.label); + } + } + }; + et2_dropdown_button.prototype.getValue = function () { + return this.value; + }; + /** + * Set options.readonly + * + * @param {boolean} _ro + */ + et2_dropdown_button.prototype.set_readonly = function (_ro) { + if (_ro != this.options.readonly) { + this.options.readonly = _ro; + // don't make readonly dropdown buttons clickable + if (this.buttons) { + this.buttons.find('button') + .toggleClass('et2_clickable', !_ro) + .toggleClass('et2_button_ro', _ro) + .css('cursor', _ro ? 'default' : 'pointer'); + } + } + }; + et2_dropdown_button.attributes = { + "label": { + "name": "caption", + "type": "string", + "description": "Label of the button", + "translate": true, + "default": "Select..." + }, + "label_updates": { + "name": "Label updates", + "type": "boolean", + "description": "Button label updates when an option is selected from the menu", + "default": true + }, + "image": { + "name": "Icon", + "type": "string", + "description": "Add an icon" + }, + "ro_image": { + "name": "Read-only Icon", + "type": "string", + "description": "Use this icon instead of hiding for read-only" + }, + "onclick": { + "description": "JS code which gets executed when the button is clicked" + }, + "select_options": { + "type": "any", + "name": "Select options", + "default": {}, + "description": "Select options for dropdown. Can be a simple key => value list, or value can be full HTML", + // Skip normal initialization for this one + "ignore": true + }, + "accesskey": { + "name": "Access Key", + "type": "string", + "default": et2_no_init, + "description": "Alt + activates widget" + }, + "tabindex": { + "name": "Tab index", + "type": "integer", + "default": et2_no_init, + "description": "Specifies the tab order of a widget when the 'tab' button is used for navigating." + }, + // No such thing as a required button + "required": { + "ignore": true + } + }; + return et2_dropdown_button; +}(et2_core_inputWidget_1.et2_inputWidget)); +exports.et2_dropdown_button = et2_dropdown_button; +et2_core_widget_1.et2_register_widget(et2_dropdown_button, ["dropdown_button"]); +//# sourceMappingURL=et2_widget_dropdown_button.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_dropdown_button.ts b/api/js/etemplate/et2_widget_dropdown_button.ts new file mode 100644 index 0000000000..150851f387 --- /dev/null +++ b/api/js/etemplate/et2_widget_dropdown_button.ts @@ -0,0 +1,441 @@ +/** + * EGroupware eTemplate2 - JS Dropdown Button object + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage api + * @link http://www.egroupware.org + * @author Nathan Gray + * @copyright Nathan Gray 2013 + * @version $Id$ + */ + +/*egw:uses + /vendor/bower-asset/jquery/dist/jquery.js; + /vendor/bower-asset/jquery-ui/jquery-ui.js; + et2_baseWidget; +*/ + +import {et2_inputWidget} from './et2_core_inputWidget'; +import {WidgetConfig, et2_register_widget} from "./et2_core_widget"; +import {ClassWithAttributes} from "./et2_core_inheritance"; + +/** + * A split button - a button with a dropdown list + * + * There are several parts to the button UI: + * - Container: This is what is percieved as the dropdown button, the whole package together + * - Button: The part on the left that can be clicked + * - Arrow: The button to display the choices + * - Menu: The list of choices + * + * Menu options are passed via the select_options. They are normally ID => Title pairs, + * as for a select box, but the title can also be full HTML if needed. + * + * @augments et2_inputWidget + */ +export class et2_dropdown_button extends et2_inputWidget +{ + static readonly attributes : any = { + "label": { + "name": "caption", + "type": "string", + "description": "Label of the button", + "translate": true, + "default": "Select..." + }, + "label_updates": { + "name": "Label updates", + "type": "boolean", + "description": "Button label updates when an option is selected from the menu", + "default": true + }, + "image": { + "name": "Icon", + "type": "string", + "description": "Add an icon" + }, + "ro_image": { + "name": "Read-only Icon", + "type": "string", + "description": "Use this icon instead of hiding for read-only" + }, + "onclick": { + "description": "JS code which gets executed when the button is clicked" + }, + "select_options": { + "type": "any", + "name": "Select options", + "default": {}, + "description": "Select options for dropdown. Can be a simple key => value list, or value can be full HTML", + // Skip normal initialization for this one + "ignore": true + }, + "accesskey": { + "name": "Access Key", + "type": "string", + "default": et2_no_init, + "description": "Alt + activates widget" + }, + "tabindex": { + "name": "Tab index", + "type": "integer", + "default": et2_no_init, + "description": "Specifies the tab order of a widget when the 'tab' button is used for navigating." + }, + // No such thing as a required button + "required": { + "ignore": true + } + }; + + internal_ids : any = { + div: "", + button: "", + menu: "" + }; + + div : JQuery = null; + buttons : JQuery = null; + button : JQuery = null; + arrow : JQuery = null; + menu : JQuery = null; + image : JQuery = null; + clicked : boolean = false; + label_updates : boolean = true; + value : any = null; + /** + * Default menu, so there is something for the widget browser / editor to show + */ + readonly default_menu : string = ''; + + /** + * Constructor + * + * @memberOf et2_dropdown_button + */ + constructor(_parent?, _attrs? : WidgetConfig, _child? : object) { + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_dropdown_button._attributes, _child || {})); + + this.clicked = false; + + let self = this; + + // Create the individual UI elements + + // Menu is a UL + this.menu = jQuery(this.default_menu).attr("id",this.internal_ids.menu) + .hide() + .menu({ + select: function(event,ui) { + self.onselect.call(self,event,ui.item); + } + }); + + this.buttons = jQuery(document.createElement("div")) + .addClass("et2_dropdown"); + + // Main "wrapper" div + this.div = jQuery(document.createElement("div")) + .attr("id", this.internal_ids.div) + .append(this.buttons) + .append(this.menu); + + // Left side - activates click action + this.button = jQuery(document.createElement("button")) + .attr("id", this.internal_ids.button) + .attr("type", "button") + .addClass("ui-widget ui-corner-left").removeClass("ui-corner-all") + .appendTo(this.buttons); + + // Right side - shows dropdown + this.arrow = jQuery(document.createElement("button")) + .addClass("ui-widget ui-corner-right").removeClass("ui-corner-all") + .attr("type", "button") + .click(function() { + // ignore click on readonly button + if (self.options.readonly) return false; + // Clicking it again hides menu + if(self.menu.is(":visible")) + { + self.menu.hide(); + return false; + } + // Show menu dropdown + var menu = self.menu.show().position({ + my: "left top", + at: "left bottom", + of: self.buttons + }); + // Hide menu if clicked elsewhere + jQuery( document ).one( "click", function() { + menu.hide(); + }); + return false; + }) + // This is the actual down arrow icon + .append("
        ") + .appendTo(this.buttons); + + // Common button UI + this.buttons.children("button") + .addClass("ui-state-default") + .hover( + function() {jQuery(this).addClass("ui-state-hover");}, + function() {jQuery(this).removeClass("ui-state-hover");} + ); + + // Icon + this.image = jQuery(document.createElement("img")); + + this.setDOMNode(this.div[0]); + } + + destroy() { + // Destroy widget + if(this.menu && this.menu.data('ui-menu')) this.menu.menu("destroy"); + + // Null children + this.image = null; + this.button = null; + this.arrow = null; + this.buttons = null; + this.menu = null; + + // Remove + this.div.empty().remove(); + } + + set_id(_id) { + super.set_id(_id); + + // Update internal IDs - not really needed since we refer by internal + // javascript reference, but good to keep up to date + this.internal_ids = { + div: this.dom_id + "_wrapper", + button: this.dom_id, + menu: this.dom_id + "_menu" + }; + for(let key in this.internal_ids) + { + if(this[key] == null) continue; + this[key].attr("id", this.internal_ids[key]); + } + } + + /** + * Set if the button label changes to match the selected option + * + * @param updates boolean Turn updating on or off + */ + set_label_updates(updates) + { + this.label_updates = updates; + } + + set_accesskey(key) + { + jQuery(this.node).attr("accesskey", key); + } + + set_ro_image(_image) + { + if(this.options.readonly) + { + this.set_image(_image); + } + } + + set_image(_image) + { + if(!this.isInTree() || this.image == null) return; + if(!_image.trim()) + { + this.image.hide(); + } + else + { + this.image.show(); + } + + let src = this.egw().image(_image); + if(src) + { + this.image.attr("src", src); + } + // allow url's too + else if (_image[0] == '/' || _image.substr(0,4) == 'http') + { + this.image.attr('src', _image); + } + else + { + this.image.hide(); + } + } + + /** + * Overwritten to maintain an internal clicked attribute + * + * @param _ev + * @returns {Boolean} + */ + click(_ev) + { + // ignore click on readonly button + if (this.options.readonly) return false; + + this.clicked = true; + + if (!super.click(_ev)) + { + this.clicked = false; + return false; + } + this.clicked = false; + return true; + } + + onselect(event, selected_node) + { + this.set_value(selected_node.attr("data-id")); + this.change(selected_node); + } + + attachToDOM() + { + let res = super.attachToDOM(); + + // Move the parent's handler to the button, or we can't tell the difference between the clicks + jQuery(this.node).unbind("click.et2_baseWidget"); + this.button.off().bind("click.et2_baseWidget", this, function(e) { + return e.data.click.call(e.data, this); + }); + return res; + } + + set_label(_value) + { + if (this.button) + { + this.label = _value; + + this.button.text(_value) + .prepend(this.image); + } + } + + /** + * Set the options for the dropdown + * + * @param options Object ID => Label pairs + */ + set_select_options(options) { + this.menu.first().empty(); + + // Allow more complicated content, if passed + if(typeof options == "string") + { + this.menu.append(options); + } + else + { + let add_complex = function(node, options) + { + for(let key in options) + { + let item; + if(typeof options[key] == "string") + { + item = jQuery("
      • "+options[key]+"
      • "); + } + else if (options[key]["label"]) + { + item =jQuery("
      • "+options[key]["label"]+"
      • "); + } + // Optgroup + else + { + item = jQuery("
      • "+key+"
      • "); + add_complex(node.append("
          "), options[key]); + } + node.append(item); + if(item && options[key].icon) + { + // we supply a applicable class for item images + jQuery('a',item).prepend(''); + } + } + } + add_complex(this.menu.first(), options); + } + this.menu.menu("refresh"); + } + + /** + * Set tab index + */ + set_tabindex(index) + { + jQuery(this.button).attr("tabindex", index); + } + + set_value(new_value) + { + let menu_item = jQuery("[data-id='"+new_value+"']",this.menu); + if(menu_item.length) + { + this.value = new_value; + if(this.label_updates) + { + this.set_label(menu_item.text()); + } + } + else + { + this.value = null; + if(this.label_updates) + { + this.set_label(this.options.label); + } + } + } + + getValue() + { + return this.value; + } + + /** + * Set options.readonly + * + * @param {boolean} _ro + */ + set_readonly(_ro : boolean) + { + if (_ro != this.options.readonly) + { + this.options.readonly = _ro; + + // don't make readonly dropdown buttons clickable + if (this.buttons) + { + this.buttons.find('button') + .toggleClass('et2_clickable', !_ro) + .toggleClass('et2_button_ro', _ro) + .css('cursor', _ro ? 'default' : 'pointer'); + } + } + } +} +et2_register_widget(et2_dropdown_button, ["dropdown_button"]); + + diff --git a/api/js/etemplate/et2_widget_dynheight.js b/api/js/etemplate/et2_widget_dynheight.js new file mode 100644 index 0000000000..e9edccb598 --- /dev/null +++ b/api/js/etemplate/et2_widget_dynheight.js @@ -0,0 +1,157 @@ +"use strict"; +/** + * EGroupware eTemplate2 - JS Dynheight object + * + * @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 + * @version $Id$ + */ +Object.defineProperty(exports, "__esModule", { value: true }); +/*egw:use + /vendor/bower-asset/jquery/dist/jquery.js; + et2_core_inheritance; +*/ +/** + * Object which resizes an inner node to the maximum extend of an outer node + * (without creating a scrollbar) - it achieves that by performing some very + * nasty and time consuming calculations. + */ +var et2_dynheight = /** @class */ (function () { + function et2_dynheight(_outerNode, _innerNode, _minHeight) { + this.initialized = false; + this.minHeight = 0; + this.bottomNodes = []; + this.innerMargin = 0; + this.outerMargin = 0; + this.outerNode = jQuery(_outerNode); + this.innerNode = jQuery(_innerNode); + this.minHeight = _minHeight; + } + et2_dynheight.prototype.destroy = function () { + this.outerNode = null; + this.innerNode = null; + this.bottomNodes = []; + }; + /** + * Resizes the inner node. When this is done, the callback function is + * called. + * + * @param {function} _callback + * @param {object} _context + */ + et2_dynheight.prototype.update = function (_callback, _context) { + // Check whether the inner node is actually visible - if not, don't + // trigger the callback function + if (this.innerNode.is(":visible")) { + // Initialize the height calculation + this._initialize(); + // Get the outer container height and offset, if available + var oh = this.outerNode.height(); + var ot = this.outerNode.offset() ? this.outerNode.offset().top : 0; + // Get top and height of the inner node + var it = this.innerNode.offset().top; + // Calculate the height of the "bottomNodes" + var bminTop = this.bottomNodes.length ? Infinity : 0; + var bmaxBot = 0; + for (var i = 0; i < this.bottomNodes.length; i++) { + // Ignore hidden popups + if (this.bottomNodes[i].find('.action_popup').length) { + egw.debug('warn', "Had to skip a hidden popup - it should be removed", this.bottomNodes[i].find('.action_popup')); + continue; + } + // Ignore other hidden nodes + if (!this.bottomNodes[i].is(':visible')) + continue; + // Get height, top and bottom and calculate the maximum/minimum + var bh_1 = this.bottomNodes[i].outerHeight(true); + var bt = this.bottomNodes[i].offset().top; + var bb = bh_1 + bt; + if (i == 0 || bminTop > bt) { + bminTop = bt; + } + if (i == 0 || bmaxBot < bb) { + bmaxBot = bb; + } + } + // Get the height of the bottom container + var bh = Math.max(0, bmaxBot - bminTop); + // Calculate the new height of the inner container + var h = Math.max(this.minHeight, oh + ot - it - bh - + this.innerMargin - this.outerMargin); + this.innerNode.height(h); + // Update the width + // Some checking to make sure it doesn't overflow the width when user + // resizes the window + var w = this.outerNode.width(); + if (w > jQuery(window).width()) { + // 50px border, totally arbitrary, but we just need to make sure it's inside + w = jQuery(window).width() - 50; + } + if (w != this.innerNode.outerWidth()) { + this.innerNode.width(w); + } + // Call the callback function + if (typeof _callback != "undefined") { + _callback.call(_context, w, h); + } + } + }; + /** + * Function used internally which collects all DOM-Nodes which are located + * below this element. + * + * @param {HTMLElement} _node + * @param {number} _bottom + */ + et2_dynheight.prototype._collectBottomNodes = function (_node, _bottom) { + // Calculate the bottom position of the inner node + if (typeof _bottom == "undefined") { + _bottom = this.innerNode.offset().top + this.innerNode.height(); + } + if (_node) { + // Accumulate the outer margin of the parent elements + var node = jQuery(_node); + var ooh = node.outerHeight(true); + var oh = node.height(); + this.outerMargin += (ooh - oh) / 2; // Divide by 2 as the value contains margin-top and -bottom + // Iterate over the children of the given node and do the same + // recursively to the parent nodes until the _outerNode or body is + // reached. + var self_1 = this; + jQuery(_node).children().each(function () { + var $this = jQuery(this); + var top = $this.offset().top; + if (this != self_1.innerNode[0] && top >= _bottom) { + self_1.bottomNodes.push($this); + } + }); + if (_node != this.outerNode[0] && _node != jQuery("body")[0]) { + this._collectBottomNodes(_node.parentNode, _bottom); + } + } + }; + /** + * Used internally to calculate some information which will not change over + * the time. + */ + et2_dynheight.prototype._initialize = function () { + if (!this.initialized) { + // Collect all bottomNodes and calculates the outer margin + this.bottomNodes = []; + this.outerMargin = 0; + this._collectBottomNodes(this.innerNode[0].parentNode); + // Calculate the inner margin + var ioh = this.innerNode.outerHeight(true); + var ih = this.innerNode.height(); + this.innerMargin = ioh - ih; + this.initialized = true; + } + }; + return et2_dynheight; +}()); +exports.et2_dynheight = et2_dynheight; +//# sourceMappingURL=et2_widget_dynheight.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_extension_nextmatch_dynheight.js b/api/js/etemplate/et2_widget_dynheight.ts similarity index 69% rename from api/js/etemplate/et2_extension_nextmatch_dynheight.js rename to api/js/etemplate/et2_widget_dynheight.ts index 725e6e6b0b..a056666153 100644 --- a/api/js/etemplate/et2_extension_nextmatch_dynheight.js +++ b/api/js/etemplate/et2_widget_dynheight.ts @@ -19,32 +19,32 @@ * Object which resizes an inner node to the maximum extend of an outer node * (without creating a scrollbar) - it achieves that by performing some very * nasty and time consuming calculations. - * - * @augments Class */ -var et2_dynheight = (function(){ "use strict"; return Class.extend( +export class et2_dynheight { - /** - * Constructor for the dynheight object - * - * @param _outerNode is the node which surrounds the _innerNode and to - * which extend the innerNode should be expanded without creating a - * scrollbar. Note: The outer node must be a parent of the inner node. - * @param _innerNode is the node which should be scaled. Call update to - * scale the node. - * @param _minHeight is the minimum height the inner node should have - * @memberOf et2_dynheight - */ - init: function(_outerNode, _innerNode, _minHeight) { + private initialized: boolean = false; + + private outerNode: JQuery; + private innerNode: JQuery; + private minHeight: number = 0; + + private bottomNodes: any[] = []; + private innerMargin: number = 0; + private outerMargin: number = 0; + + constructor(_outerNode, _innerNode, _minHeight) + { this.outerNode = jQuery(_outerNode); this.innerNode = jQuery(_innerNode); this.minHeight = _minHeight; + } + destroy() + { + this.outerNode = null; + this.innerNode = null; this.bottomNodes = []; - this.initialized = false; - this.innerMargin = 0; - this.outerMargin = 0; - }, + } /** * Resizes the inner node. When this is done, the callback function is @@ -53,7 +53,8 @@ var et2_dynheight = (function(){ "use strict"; return Class.extend( * @param {function} _callback * @param {object} _context */ - update: function(_callback, _context) { + update( _callback, _context) + { // Check whether the inner node is actually visible - if not, don't // trigger the callback function if (this.innerNode.is(":visible")) @@ -62,16 +63,16 @@ var et2_dynheight = (function(){ "use strict"; return Class.extend( this._initialize(); // Get the outer container height and offset, if available - var oh = this.outerNode.height(); - var ot = this.outerNode.offset() ? this.outerNode.offset().top : 0; + const oh = this.outerNode.height(); + const ot = this.outerNode.offset() ? this.outerNode.offset().top : 0; // Get top and height of the inner node - var it = this.innerNode.offset().top; + const it = this.innerNode.offset().top; // Calculate the height of the "bottomNodes" - var bminTop = this.bottomNodes.length ? Infinity : 0; - var bmaxBot = 0; - for (var i = 0; i < this.bottomNodes.length; i++) + let bminTop = this.bottomNodes.length ? Infinity : 0; + let bmaxBot = 0; + for (let i = 0; i < this.bottomNodes.length; i++) { // Ignore hidden popups if(this.bottomNodes[i].find('.action_popup').length) @@ -85,9 +86,9 @@ var et2_dynheight = (function(){ "use strict"; return Class.extend( if(!this.bottomNodes[i].is(':visible')) continue; // Get height, top and bottom and calculate the maximum/minimum - var bh = this.bottomNodes[i].outerHeight(true); - var bt = this.bottomNodes[i].offset().top; - var bb = bh + bt; + let bh = this.bottomNodes[i].outerHeight(true); + let bt = this.bottomNodes[i].offset().top; + const bb = bh + bt; if (i == 0 || bminTop > bt) { @@ -101,17 +102,17 @@ var et2_dynheight = (function(){ "use strict"; return Class.extend( } // Get the height of the bottom container - var bh = Math.max(0,bmaxBot - bminTop); + const bh = Math.max(0, bmaxBot - bminTop); // Calculate the new height of the inner container - var h = Math.max(this.minHeight, oh + ot - it - bh - + const h = Math.max(this.minHeight, oh + ot - it - bh - this.innerMargin - this.outerMargin); this.innerNode.height(h); // Update the width // Some checking to make sure it doesn't overflow the width when user // resizes the window - var w = this.outerNode.width(); + let w = this.outerNode.width(); if (w > jQuery(window).width()) { // 50px border, totally arbitrary, but we just need to make sure it's inside @@ -128,16 +129,17 @@ var et2_dynheight = (function(){ "use strict"; return Class.extend( _callback.call(_context, w, h); } } - }, + } /** * Function used internally which collects all DOM-Nodes which are located * below this element. * - * @param {DOMElement} _node + * @param {HTMLElement} _node * @param {number} _bottom */ - _collectBottomNodes: function(_node, _bottom) { + _collectBottomNodes( _node : any, _bottom? : number) + { // Calculate the bottom position of the inner node if (typeof _bottom == "undefined") { @@ -147,18 +149,18 @@ var et2_dynheight = (function(){ "use strict"; return Class.extend( if (_node) { // Accumulate the outer margin of the parent elements - var node = jQuery(_node); - var ooh = node.outerHeight(true); - var oh = node.height(); + const node = jQuery(_node); + const ooh = node.outerHeight(true); + const oh = node.height(); this.outerMargin += (ooh - oh) / 2; // Divide by 2 as the value contains margin-top and -bottom // Iterate over the children of the given node and do the same // recursively to the parent nodes until the _outerNode or body is // reached. - var self = this; + const self = this; jQuery(_node).children().each(function() { - var $this = jQuery(this); - var top = $this.offset().top; + const $this = jQuery(this); + const top = $this.offset().top; if (this != self.innerNode[0] && top >= _bottom) { self.bottomNodes.push($this); @@ -170,13 +172,14 @@ var et2_dynheight = (function(){ "use strict"; return Class.extend( this._collectBottomNodes(_node.parentNode, _bottom); } } - }, + } /** * Used internally to calculate some information which will not change over * the time. */ - _initialize: function() { + _initialize( ) + { if (!this.initialized) { // Collect all bottomNodes and calculates the outer margin @@ -185,13 +188,12 @@ var et2_dynheight = (function(){ "use strict"; return Class.extend( this._collectBottomNodes(this.innerNode[0].parentNode); // Calculate the inner margin - var ioh = this.innerNode.outerHeight(true); - var ih = this.innerNode.height(); + const ioh = this.innerNode.outerHeight(true); + const ih = this.innerNode.height(); this.innerMargin = ioh - ih; this.initialized = true; } } -});}).call(this); - +} \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_entry.js b/api/js/etemplate/et2_widget_entry.js index 928b220fa9..095999402d 100644 --- a/api/js/etemplate/et2_widget_entry.js +++ b/api/js/etemplate/et2_widget_entry.js @@ -1,3 +1,4 @@ +"use strict"; /* * Egroupware etemplate2 JS Entry widget * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License @@ -5,14 +6,27 @@ * @subpackage api * @link http://www.egroupware.org * @author Nathan Gray - * @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_valueWidget; + et2_core_valueWidget; */ - +var et2_core_widget_1 = require("./et2_core_widget"); +var et2_core_valueWidget_1 = require("./et2_core_valueWidget"); +var et2_core_inheritance_1 = require("./et2_core_inheritance"); /** * A widget to display a value from an entry * @@ -27,151 +41,129 @@ * * @augments et2_valueWidget */ -var et2_entry = (function(){ "use strict"; return et2_valueWidget.extend( -{ - attributes: { - field: { - 'name': 'Fields', - 'description': 'Which entry field to display, or "sum" to add up the alternate_fields', - 'type': 'string' - }, - compare: { - name: 'Compare', - description: 'if given, the selected field is compared with its value and an X is printed on equality, nothing otherwise', - default: et2_no_init, - type: 'string' - }, - alternate_fields: { - name: 'Alternate fields', - description: 'colon (:) separated list of alternative fields. The first non-empty one is used if the selected field is empty, (-) used for subtraction', - type: 'string', - default: et2_no_init - }, - precision: { - name: 'Decimals to be shown', - description: 'Specifies the number of decimals for sum of alternates, the default is 2', - type: 'string', - default: '2' - }, - regex: { - name: 'Regular expression pattern', - description: 'Only used server-side in a preg_replace with regex_replace to modify the value', - default: et2_no_init, - type: 'string' - }, - regex_replace: { - name: 'Regular expression replacement pattern', - description: 'Only used server-side in a preg_replace with regex to modify the value', - default: et2_no_init, - type: 'string' - }, - value: { - type: 'any' - }, - readonly: { - default: true - } - }, - - legacyOptions: ["field","compare","alternate_fields"], - - prefix: '~', - - /** - * Constructor - * - * @memberOf et2_customfields_list - */ - init: function(parent, attrs) { - // Often the ID conflicts, so check prefix - if(attrs.id && attrs.id.indexOf(this.prefix) < 0) - { - attrs.id = this.prefix + attrs.id; - } - var value = attrs.value; - - this._super.apply(this, arguments); - - // Save value from parsing, but only if set - if(value) - { - this.options.value = value; - } - - this.widget = null; - this.setDOMNode(document.createElement('span')); - }, - - loadFromXML: function(_node) { - // Load the nodes as usual - this._super.apply(this, arguments); - - // Do the magic - this.loadField(); - }, - - /** - * Initialize widget for entry field - */ - loadField: function() { - // Create widget of correct type - var attrs = { - id: this.id + (this.options.field ? '[' +this.options.field+']' : ''), - type: 'label', - readonly: this.options.readonly - }; - var modifications = this.getArrayMgr("modifications"); - if(modifications && this.options.field) - { - jQuery.extend(attrs, modifications.getEntry(attrs.id)); - } - - // Supress labels on templates - if(attrs.type == 'template' && this.options.label) - { - this.egw().debug('log', "Surpressed label on <" + this._type + ' label="' + this.options.label + '" id="' + this.id + '"...>'); - this.options.label = ''; - } - var widget = et2_createWidget(attrs.type, attrs, this); - - // If value is not set, etemplate takes care of everything - // If value was set, find the record explicitly. - if(typeof this.options.value == 'string') - { - widget.options.value = this.getArrayMgr('content').getEntry(this.id+'['+this.options.field+']') || - this.getRoot().getArrayMgr('content').getEntry(this.prefix+this.options.value + '['+this.options.field+']'); - } - else if (this.options.field && this.options.value && this.options.value[this.options.field]) - { - widget.options.value = this.options.value[this.options.field]; - } - if(this.options.compare) - { - widget.options.value = widget.options.value == this.options.compare ? 'X' : ''; - } - if(this.options.alternate_fields) - { - var sum = 0; - var fields = this.options.alternate_fields.split(':'); - for(var i = 0; i < fields.length; i++) - { - var negate = (fields[i][0] == "-"); - var value = this.getArrayMgr('content').getEntry(fields[i].replace('-','')) - sum += typeof value === 'undefined' ? 0 : (parseFloat(value) * (negate ? -1 : 1)); - if(value && this.options.field !== 'sum') - { - widget.options.value = value; - break; - } - } - if(this.options.field == 'sum') - { - if (this.options.precision && jQuery.isNumeric(sum)) sum = parseFloat(sum).toFixed(this.options.precision); - widget.options.value = sum; - } - } - - } -});}).call(this); - -et2_register_widget(et2_entry, ["entry", 'contact-value', 'contact-account', 'contact-template', 'infolog-value','tracker-value','records-value']); \ No newline at end of file +var et2_entry = /** @class */ (function (_super) { + __extends(et2_entry, _super); + function et2_entry(_parent, _attrs, _child) { + var _this = + // Call the inherited constructor + _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_entry._attributes, _child || {})) || this; + _this.legacyOptions = ["field", "compare", "alternate_fields"]; + _this.widget = null; + // Often the ID conflicts, so check prefix + if (_attrs.id && _attrs.id.indexOf(et2_entry.prefix) < 0) { + _attrs.id = et2_entry.prefix + _attrs.id; + } + var value = _attrs.value; + _this = _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_entry._attributes, _child || {})) || this; + // Save value from parsing, but only if set + if (value) { + _this.options.value = value; + } + _this.widget = null; + _this.setDOMNode(document.createElement('span')); + return _this; + } + et2_entry.prototype.loadFromXML = function (_node) { + // Load the nodes as usual + _super.prototype.loadFromXML.call(this, _node); + // Do the magic + this.loadField(); + }; + /** + * Initialize widget for entry field + */ + et2_entry.prototype.loadField = function () { + // Create widget of correct type + var attrs = { + id: this.id + (this.options.field ? '[' + this.options.field + ']' : ''), + type: 'label', + readonly: this.options.readonly + }; + var modifications = this.getArrayMgr("modifications"); + if (modifications && this.options.field) { + jQuery.extend(attrs, modifications.getEntry(attrs.id)); + } + // Supress labels on templates + if (attrs.type == 'template' && this.options.label) { + this.egw().debug('log', "Surpressed label on <" + this.getType() + ' label="' + this.options.label + '" id="' + this.id + '"...>'); + this.options.label = ''; + } + var widget = et2_createWidget(attrs.type, attrs, this); + // If value is not set, etemplate takes care of everything + // If value was set, find the record explicitly. + if (typeof this.options.value == 'string') { + widget.options.value = this.getArrayMgr('content').getEntry(this.id + '[' + this.options.field + ']') || + this.getRoot().getArrayMgr('content').getEntry(et2_entry.prefix + this.options.value + '[' + this.options.field + ']'); + } + else if (this.options.field && this.options.value && this.options.value[this.options.field]) { + widget.options.value = this.options.value[this.options.field]; + } + if (this.options.compare) { + widget.options.value = widget.options.value == this.options.compare ? 'X' : ''; + } + if (this.options.alternate_fields) { + var sum = 0; + var fields = this.options.alternate_fields.split(':'); + for (var i = 0; i < fields.length; i++) { + var negate = (fields[i][0] == "-"); + var value = this.getArrayMgr('content').getEntry(fields[i].replace('-', '')); + sum += typeof value === 'undefined' ? 0 : (parseFloat(value) * (negate ? -1 : 1)); + if (value && this.options.field !== 'sum') { + widget.options.value = value; + break; + } + } + if (this.options.field == 'sum') { + if (this.options.precision && jQuery.isNumeric(sum)) + sum = parseFloat(sum).toFixed(this.options.precision); + widget.options.value = sum; + } + } + }; + et2_entry._attributes = { + field: { + 'name': 'Fields', + 'description': 'Which entry field to display, or "sum" to add up the alternate_fields', + 'type': 'string' + }, + compare: { + name: 'Compare', + description: 'if given, the selected field is compared with its value and an X is printed on equality, nothing otherwise', + default: et2_no_init, + type: 'string' + }, + alternate_fields: { + name: 'Alternate fields', + description: 'colon (:) separated list of alternative fields. The first non-empty one is used if the selected field is empty, (-) used for subtraction', + type: 'string', + default: et2_no_init + }, + precision: { + name: 'Decimals to be shown', + description: 'Specifies the number of decimals for sum of alternates, the default is 2', + type: 'string', + default: '2' + }, + regex: { + name: 'Regular expression pattern', + description: 'Only used server-side in a preg_replace with regex_replace to modify the value', + default: et2_no_init, + type: 'string' + }, + regex_replace: { + name: 'Regular expression replacement pattern', + description: 'Only used server-side in a preg_replace with regex to modify the value', + default: et2_no_init, + type: 'string' + }, + value: { + type: 'any' + }, + readonly: { + default: true + } + }; + return et2_entry; +}(et2_core_valueWidget_1.et2_valueWidget)); +et2_core_widget_1.et2_register_widget(et2_entry, ["entry", 'contact-value', 'contact-account', 'contact-template', 'infolog-value', 'tracker-value', 'records-value']); +//# sourceMappingURL=et2_widget_entry.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_entry.ts b/api/js/etemplate/et2_widget_entry.ts new file mode 100644 index 0000000000..6b8cec7c5b --- /dev/null +++ b/api/js/etemplate/et2_widget_entry.ts @@ -0,0 +1,179 @@ +/* + * Egroupware etemplate2 JS Entry widget + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage api + * @link http://www.egroupware.org + * @author Nathan Gray + */ + + +/*egw:uses + et2_core_valueWidget; +*/ + +import {et2_register_widget, WidgetConfig} from "./et2_core_widget"; +import {et2_valueWidget} from "./et2_core_valueWidget"; +import {ClassWithAttributes} from "./et2_core_inheritance"; + +/** + * A widget to display a value from an entry + * + * Since we have Etemplate\Widget\Transformer, this client side widget exists + * mostly to resolve the problem where the ID for the entry widget is the same + * as the widget where you actually set the value, which prevents transformer + * from working. + * + * Server side will find the associated entry, and load it into ~ to + * avoid overwriting the widget with id="entry_id". This widget will reverse + * that, and the modifications from transformer will be applied. + * + * @augments et2_valueWidget + */ +class et2_entry extends et2_valueWidget +{ + static readonly _attributes : any = { + field: { + 'name': 'Fields', + 'description': 'Which entry field to display, or "sum" to add up the alternate_fields', + 'type': 'string' + }, + compare: { + name: 'Compare', + description: 'if given, the selected field is compared with its value and an X is printed on equality, nothing otherwise', + default: et2_no_init, + type: 'string' + }, + alternate_fields: { + name: 'Alternate fields', + description: 'colon (:) separated list of alternative fields. The first non-empty one is used if the selected field is empty, (-) used for subtraction', + type: 'string', + default: et2_no_init + }, + precision: { + name: 'Decimals to be shown', + description: 'Specifies the number of decimals for sum of alternates, the default is 2', + type: 'string', + default: '2' + }, + regex: { + name: 'Regular expression pattern', + description: 'Only used server-side in a preg_replace with regex_replace to modify the value', + default: et2_no_init, + type: 'string' + }, + regex_replace: { + name: 'Regular expression replacement pattern', + description: 'Only used server-side in a preg_replace with regex to modify the value', + default: et2_no_init, + type: 'string' + }, + value: { + type: 'any' + }, + readonly: { + default: true + } + }; + + legacyOptions : string[] = ["field","compare","alternate_fields"]; + + static readonly prefix: '~'; + protected widget = null; + + constructor(_parent, _attrs? : WidgetConfig, _child? : object) + { + // Call the inherited constructor + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_entry._attributes, _child || {})); + + // Often the ID conflicts, so check prefix + if(_attrs.id && _attrs.id.indexOf(et2_entry.prefix) < 0) + { + _attrs.id = et2_entry.prefix + _attrs.id; + } + let value = _attrs.value; + + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_entry._attributes, _child || {})); + + // Save value from parsing, but only if set + if(value) + { + this.options.value = value; + } + + this.widget = null; + this.setDOMNode(document.createElement('span')); + } + + loadFromXML(_node) + { + // Load the nodes as usual + super.loadFromXML(_node); + // Do the magic + this.loadField(); + } + + /** + * Initialize widget for entry field + */ + loadField() + { + // Create widget of correct type + let attrs = { + id: this.id + (this.options.field ? '[' +this.options.field+']' : ''), + type: 'label', + readonly: this.options.readonly + }; + let modifications = this.getArrayMgr("modifications"); + if(modifications && this.options.field) + { + jQuery.extend(attrs, modifications.getEntry(attrs.id)); + } + + // Supress labels on templates + if(attrs.type == 'template' && this.options.label) + { + this.egw().debug('log', "Surpressed label on <" + this.getType() + ' label="' + this.options.label + '" id="' + this.id + '"...>'); + this.options.label = ''; + } + let widget = et2_createWidget(attrs.type, attrs, this); + + // If value is not set, etemplate takes care of everything + // If value was set, find the record explicitly. + if(typeof this.options.value == 'string') + { + widget.options.value = this.getArrayMgr('content').getEntry(this.id+'['+this.options.field+']') || + this.getRoot().getArrayMgr('content').getEntry(et2_entry.prefix+this.options.value + '['+this.options.field+']'); + } + else if (this.options.field && this.options.value && this.options.value[this.options.field]) + { + widget.options.value = this.options.value[this.options.field]; + } + if(this.options.compare) + { + widget.options.value = widget.options.value == this.options.compare ? 'X' : ''; + } + if(this.options.alternate_fields) + { + let sum : number | string = 0; + let fields = this.options.alternate_fields.split(':'); + for(let i = 0; i < fields.length; i++) + { + let negate = (fields[i][0] == "-"); + let value = this.getArrayMgr('content').getEntry(fields[i].replace('-','')); + sum += typeof value === 'undefined' ? 0 : (parseFloat(value) * (negate ? -1 : 1)); + if(value && this.options.field !== 'sum') + { + widget.options.value = value; + break; + } + } + if(this.options.field == 'sum') + { + if (this.options.precision && jQuery.isNumeric(sum)) sum = parseFloat(sum).toFixed(this.options.precision); + widget.options.value = sum; + } + } + } +} +et2_register_widget(et2_entry, ["entry", 'contact-value', 'contact-account', 'contact-template', 'infolog-value','tracker-value','records-value']); diff --git a/api/js/etemplate/et2_widget_favorites.js b/api/js/etemplate/et2_widget_favorites.js index c455e1d847..962fbfe0a3 100644 --- a/api/js/etemplate/et2_widget_favorites.js +++ b/api/js/etemplate/et2_widget_favorites.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - JS Favorite widget * @@ -7,14 +8,28 @@ * @link http://www.egroupware.org * @author Nathan Gray * @copyright Nathan Gray 2013 - * @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_dropdown_button; - et2_extension_nextmatch; + et2_dropdown_button; + et2_extension_nextmatch; */ - +var et2_core_widget_1 = require("./et2_core_widget"); +var et2_widget_dropdown_button_1 = require("./et2_widget_dropdown_button"); +var et2_core_inheritance_1 = require("./et2_core_inheritance"); /** * Favorites widget, designed for use with a nextmatch widget * @@ -42,369 +57,287 @@ * * @augments et2_dropdown_button */ -var et2_favorites = (function(){ "use strict"; return et2_dropdown_button.extend([et2_INextmatchHeader], -{ - attributes: { - "default_pref": { - "name": "Default preference key", - "type": "string", - "description": "The preference key where default favorite is stored (not the value)" - }, - "sidebox_target": { - "name": "Sidebox target", - "type": "string", - "description": "ID of element to insert favorite list into", - "default": "favorite_sidebox" - }, - "app": { - "name": "Application", - "type": "string", - "description": "Application to show favorites for" - }, - "filters": { - "name": "Extra filters", - "type": "any", - "description": "Array of extra filters to include in the saved favorite" - }, - - // These are particular to favorites - id: {"default": "favorite"}, - label: {"default": ""}, - label_updates: { "default": false}, - image: {"default": this.egw().image('fav_filter')}, - statustext: {"default": "Favorite queries", "type": "string"} - }, - - // Some convenient variables, used in closures / event handlers - header: null, - nextmatch: null, - favorite_prefix: "favorite_", - stored_filters: {}, - - // If filter was set server side, we need to remember it until nm is created - nm_filter: false, - - /** - * Constructor - * - * @memberOf et2_favorites - */ - init: function() { - this._super.apply(this, arguments); - this.sidebox_target = jQuery("#"+this.options.sidebox_target); - if(this.sidebox_target.length == 0 && egw_getFramework() != null) - { - var egw_fw = egw_getFramework(); - this.sidebox_target = jQuery("#"+this.options.sidebox_target,egw_fw.sidemenuDiv); - } - // Store array of sorted items - this.favSortedList = ['blank']; - - var apps = egw().user('apps'); - this.is_admin = (typeof apps['admin'] != "undefined"); - - this.stored_filters = this.load_favorites(this.options.app); - - this.preferred = egw.preference(this.options.default_pref,this.options.app); - if(!this.preferred || typeof this.stored_filters[this.preferred] == "undefined") - { - this.preferred = "blank"; - } - - // It helps to have the ID properly set before we get too far - this.set_id(this.id); - - this.init_filters(this); - - this.menu.addClass("favorites"); - - // Set the default (button) value - this.set_value(this.preferred,true); - - var self = this; - - // Add a listener on the radio buttons to set default filter - jQuery(this.menu).on("click","input:radio", function(event){ - // Don't do the menu - event.stopImmediatePropagation(); - - // Save as default favorite - used when you click the button - self.egw().set_preference(self.options.app,self.options.default_pref,jQuery(this).val()); - self.preferred = jQuery(this).val(); - - // Update sidebox, if there - if(self.sidebox_target.length) - { - self.sidebox_target.find("div.ui-icon-heart") - .replaceWith("
          "); - jQuery("li[data-id='"+self.preferred+"'] div.sideboxstar",self.sidebox_target) - .replaceWith("
          "); - } - - // Close the menu - self.menu.hide(); - - // Some user feedback - self.button.addClass("ui-state-active", 500,"swing",function(){ - self.button.removeClass("ui-state-active",2000); - }); - }); - - //Sort DomNodes of sidebox fav. menu - var sideBoxDOMNodeSort = function (_favSList) { - var favS = jQuery.isArray(_favSList)?_favSList.slice(0).reverse():[]; - - for (var i=0; i < favS.length;i++) - { - self.sidebox_target.children().find('[data-id$="' + favS[i] + '"]').prependTo(self.sidebox_target.children()); - } - }; - - //Add Sortable handler to nm fav. menu - jQuery(this.menu).sortable({ - - items:'li:not([data-id$="add"])', - placeholder:'ui-fav-sortable-placeholder', - delay: 250, //(millisecond) delay before the sorting should start - update: function (event, ui) - { - self.favSortedList = jQuery(this).sortable('toArray', {attribute:'data-id'}); - - self.egw().set_preference(self.options.app,'fav_sort_pref',self.favSortedList); - - sideBoxDOMNodeSort(self.favSortedList); - } - }); - - // Add a listener on the delete to remove - this.menu.on("click","div.ui-icon-trash", app[self.options.app], function() { - // App instance might not be ready yet, so don't bind directly - app[self.options.app].delete_favorite.apply(this,arguments); - }) - // Wrap and unwrap because jQueryUI styles use a parent, and we don't want to change the state of the menu item - // Wrap in a span instead of a div because div gets a border - .on("mouseenter","div.ui-icon-trash", function() {jQuery(this).wrap("");}) - .on("mouseleave","div.ui-icon-trash", function() {jQuery(this).unwrap();}); - - // Trigger refresh of menu options now that events are registered - // to update sidebox - if(this.sidebox_target.length > 0) - { - this.init_filters(this); - } - }, - - /** - * Load favorites from preferences - * - * @param app String Load favorites from this application - */ - load_favorites: function(app) { - - // Default blank filter - var stored_filters = { - 'blank': { - name: this.egw().lang("No filters"), - state: {} - } - }; - - // Load saved favorites - var preferences = egw.preference("*",app); - for(var pref_name in preferences) - { - if(pref_name.indexOf(this.favorite_prefix) == 0 && typeof preferences[pref_name] == 'object') - { - var name = pref_name.substr(this.favorite_prefix.length); - stored_filters[name] = preferences[pref_name]; - // Keep older favorites working - they used to store nm filters in 'filters',not state - if(preferences[pref_name].filters) - { - stored_filters[pref_name].state = preferences[pref_name].filters; - } - } - if (pref_name == 'fav_sort_pref') - { - this.favSortedList = preferences[pref_name]; - //Make sure sorted list is always an array, seems some old fav are not array - if (!jQuery.isArray(this.favSortedList)) this.favSortedList = this.favSortedList.split(','); - } - } - if(typeof stored_filters == "undefined" || !stored_filters) - { - stored_filters = {}; - } - else - { - for(var name in stored_filters) - { - if (this.favSortedList.indexOf(name) < 0) - { - this.favSortedList.push(name); - } - } - this.egw().set_preference (this.options.app,'fav_sort_pref',this.favSortedList); - if (this.favSortedList.length > 0) - { - var sortedListObj = {}; - - for (var i=0;i < this.favSortedList.length;i++) - { - if (typeof stored_filters[this.favSortedList[i]] != 'undefined') - { - sortedListObj[this.favSortedList[i]] = stored_filters[this.favSortedList[i]]; - } - else - { - this.favSortedList.splice(i,1); - this.egw().set_preference (this.options.app,'fav_sort_pref',this.favSortedList); - } - } - stored_filters = jQuery.extend(sortedListObj,stored_filters); - } - } - return stored_filters; - }, - - // Create & set filter options for dropdown menu - init_filters: function(widget, filters) - { - if(typeof filters == "undefined") - { - filters = this.stored_filters; - } - - var options = {}; - for(var name in filters) - { - options[name] = ""+ - (filters[name].name != undefined ? filters[name].name : name) + - (filters[name].group != false && !this.is_admin || name == 'blank' ? "" : - "
          "); - } - - // Only add 'Add current' if we have a nextmatch - if(this.nextmatch) - { - options.add = ""+this.egw().lang('Add current'); - } - widget.set_select_options.call(widget,options); - - // Set radio to current value - jQuery("input[value='"+ this.preferred +"']:radio", this.menu).attr("checked",true); - }, - - set_nm_filters: function(filters) - { - if(this.nextmatch) - { - this.nextmatch.applyFilters(filters); - } - else - { - console.log(filters); - } - }, - - onclick: function(node) { - // Apply preferred filter - make sure it's an object, and not a reference - if(this.preferred && this.stored_filters[this.preferred]) - { - // use app[appname].setState if available to allow app to overwrite it (eg. change to non-listview in calendar) - if (typeof app[this.options.app] != 'undefined') - { - app[this.options.app].setState(this.stored_filters[this.preferred]); - } - else - { - this.set_nm_filters(jQuery.extend({},this.stored_filters[this.preferred].state)); - } - } - else - { - alert(this.egw().lang("No default set")); - } - }, - - // Apply the favorite when you pick from the list - change: function(selected_node) { - this.value = jQuery(selected_node).attr("data-id"); - if(this.value == "add" && this.nextmatch) - { - // Get current filters - var current_filters = jQuery.extend({},this.nextmatch.activeFilters); - - // Add in extras - for(var extra in this.options.filters) - { - // Don't overwrite what nm has, chances are nm has more up-to-date value - if(typeof current_filters == 'undefined') - { - current_filters[extra] = this.nextmatch.options.settings[extra]; - } - } - - // Skip columns for now - delete current_filters.selcolumns; - - // Add in application's settings - if(this.filters != true) - { - for(var i = 0; i < this.filters.length; i++) - { - current_filters[this.options.filters[i]] = this.nextmatch.options.settings[this.options.filters[i]]; - } - } - - // Call framework - app[this.options.app].add_favorite(current_filters); - - // Reset value - this.set_value(this.preferred,true); - } - else if (this.value == 'blank') - { - // Reset filters when select no filters - this.set_nm_filters({}); - } - }, - - set_value: function(filter_name, parent) { - if(parent) - { - return this._super.call(this, filter_name); - } - - if(filter_name == 'add') return false; - - app[this.options.app].setState(this.stored_filters[filter_name]); - return false; - }, - - getValue: function() - { - return null; - }, - - /** - * Set the nextmatch to filter - * From et2_INextmatchHeader interface - * - * @param {et2_nextmatch} nextmatch - */ - setNextmatch: function(nextmatch) - { - this.nextmatch = nextmatch; - - if(this.nm_filter) - { - this.set_value(this.nm_filter); - this.nm_filter = false; - } - - // Re-generate filter list so we can add 'Add current' - this.init_filters(this); - } -});}).call(this); -et2_register_widget(et2_favorites, ["favorites"]); +var et2_favorites = /** @class */ (function (_super) { + __extends(et2_favorites, _super); + /** + * Constructor + * + * @memberOf et2_favorites + */ + function et2_favorites(_parent, _attrs, _child) { + var _this = _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_favorites._attributes, _child || {})) || this; + // Some convenient variables, used in closures / event handlers + _this.header = null; + _this.nextmatch = null; + _this.favSortedList = null; + _this.sidebox_target = null; + // If filter was set server side, we need to remember it until nm is created + _this.nm_filter = false; + _this.sidebox_target = jQuery("#" + _this.options.sidebox_target); + if (_this.sidebox_target.length == 0 && egw_getFramework() != null) { + var egw_fw = egw_getFramework(); + _this.sidebox_target = jQuery("#" + _this.options.sidebox_target, egw_fw.sidemenuDiv); + } + // Store array of sorted items + _this.favSortedList = ['blank']; + var apps = egw().user('apps'); + et2_favorites.is_admin = (typeof apps['admin'] != "undefined"); + _this.stored_filters = _this.load_favorites(_this.options.app); + _this.preferred = egw.preference(_this.options.default_pref, _this.options.app); + if (!_this.preferred || typeof _this.stored_filters[_this.preferred] == "undefined") { + _this.preferred = "blank"; + } + // It helps to have the ID properly set before we get too far + _this.set_id(_this.id); + _this.init_filters(_this); + _this.menu.addClass("favorites"); + // Set the default (button) value + _this.set_value(_this.preferred, true); + var self = _this; + // Add a listener on the radio buttons to set default filter + jQuery(_this.menu).on("click", "input:radio", function (event) { + // Don't do the menu + event.stopImmediatePropagation(); + // Save as default favorite - used when you click the button + self.egw().set_preference(self.options.app, self.options.default_pref, jQuery(this).val()); + self.preferred = jQuery(this).val(); + // Update sidebox, if there + if (self.sidebox_target.length) { + self.sidebox_target.find("div.ui-icon-heart") + .replaceWith("
          "); + jQuery("li[data-id='" + self.preferred + "'] div.sideboxstar", self.sidebox_target) + .replaceWith("
          "); + } + // Close the menu + self.menu.hide(); + // Some user feedback + self.button.addClass("ui-state-active", 500, "swing", function () { + self.button.removeClass("ui-state-active", 2000); + }); + }); + //Sort DomNodes of sidebox fav. menu + var sideBoxDOMNodeSort = function (_favSList) { + var favS = jQuery.isArray(_favSList) ? _favSList.slice(0).reverse() : []; + for (var i = 0; i < favS.length; i++) { + self.sidebox_target.children().find('[data-id$="' + favS[i] + '"]').prependTo(self.sidebox_target.children()); + } + }; + //Add Sortable handler to nm fav. menu + jQuery(_this.menu).sortable({ + items: 'li:not([data-id$="add"])', + placeholder: 'ui-fav-sortable-placeholder', + delay: 250, + update: function () { + self.favSortedList = jQuery(this).sortable('toArray', { attribute: 'data-id' }); + self.egw().set_preference(self.options.app, 'fav_sort_pref', self.favSortedList); + sideBoxDOMNodeSort(self.favSortedList); + } + }); + // Add a listener on the delete to remove + _this.menu.on("click", "div.ui-icon-trash", app[self.options.app], function () { + // App instance might not be ready yet, so don't bind directly + app[self.options.app].delete_favorite.apply(this, arguments); + }) + // Wrap and unwrap because jQueryUI styles use a parent, and we don't want to change the state of the menu item + // Wrap in a span instead of a div because div gets a border + .on("mouseenter", "div.ui-icon-trash", function () { jQuery(this).wrap(""); }) + .on("mouseleave", "div.ui-icon-trash", function () { jQuery(this).unwrap(); }); + // Trigger refresh of menu options now that events are registered + // to update sidebox + if (_this.sidebox_target.length > 0) { + _this.init_filters(_this); + } + return _this; + } + /** + * Load favorites from preferences + * + * @param app String Load favorites from this application + */ + et2_favorites.prototype.load_favorites = function (app) { + // Default blank filter + var stored_filters = { + 'blank': { + name: this.egw().lang("No filters"), + state: {} + } + }; + // Load saved favorites + var preferences = egw.preference("*", app); + for (var pref_name in preferences) { + if (pref_name.indexOf(this.favorite_prefix) == 0 && typeof preferences[pref_name] == 'object') { + var name_1 = pref_name.substr(this.favorite_prefix.length); + stored_filters[name_1] = preferences[pref_name]; + // Keep older favorites working - they used to store nm filters in 'filters',not state + if (preferences[pref_name]["filters"]) { + stored_filters[pref_name]["state"] = preferences[pref_name]["filters"]; + } + } + if (pref_name == 'fav_sort_pref') { + this.favSortedList = preferences[pref_name]; + //Make sure sorted list is always an array, seems some old fav are not array + if (!jQuery.isArray(this.favSortedList)) + this.favSortedList = this.favSortedList.split(','); + } + } + if (typeof stored_filters == "undefined" || !stored_filters) { + stored_filters = {}; + } + else { + for (var name_2 in stored_filters) { + if (this.favSortedList.indexOf(name_2) < 0) { + this.favSortedList.push(name_2); + } + } + this.egw().set_preference(this.options.app, 'fav_sort_pref', this.favSortedList); + if (this.favSortedList.length > 0) { + var sortedListObj = {}; + for (var i = 0; i < this.favSortedList.length; i++) { + if (typeof stored_filters[this.favSortedList[i]] != 'undefined') { + sortedListObj[this.favSortedList[i]] = stored_filters[this.favSortedList[i]]; + } + else { + this.favSortedList.splice(i, 1); + this.egw().set_preference(this.options.app, 'fav_sort_pref', this.favSortedList); + } + } + stored_filters = jQuery.extend(sortedListObj, stored_filters); + } + } + return stored_filters; + }; + // Create & set filter options for dropdown menu + et2_favorites.prototype.init_filters = function (widget, filters) { + if (typeof filters == "undefined") { + filters = this.stored_filters; + } + var options = {}; + for (var name_3 in filters) { + options[name_3] = "" + + (filters[name_3].name != undefined ? filters[name_3].name : name_3) + + (filters[name_3].group != false && !et2_favorites.is_admin || name_3 == 'blank' ? "" : + "
          "); + } + // Only add 'Add current' if we have a nextmatch + if (this.nextmatch) { + options["add"] = "" + this.egw().lang('Add current'); + } + widget.set_select_options.call(widget, options); + // Set radio to current value + jQuery("input[value='" + this.preferred + "']:radio", this.menu).attr("checked", 1); + }; + et2_favorites.prototype.set_nm_filters = function (filters) { + if (this.nextmatch) { + this.nextmatch.applyFilters(filters); + } + else { + console.log(filters); + } + }; + et2_favorites.prototype.onclick = function (node) { + // Apply preferred filter - make sure it's an object, and not a reference + if (this.preferred && this.stored_filters[this.preferred]) { + // use app[appname].setState if available to allow app to overwrite it (eg. change to non-listview in calendar) + if (typeof app[this.options.app] != 'undefined') { + app[this.options.app].setState(this.stored_filters[this.preferred]); + } + else { + this.set_nm_filters(jQuery.extend({}, this.stored_filters[this.preferred].state)); + } + } + else { + alert(this.egw().lang("No default set")); + } + }; + // Apply the favorite when you pick from the list + et2_favorites.prototype.change = function (selected_node) { + this.value = jQuery(selected_node).attr("data-id"); + if (this.value == "add" && this.nextmatch) { + // Get current filters + var current_filters = jQuery.extend({}, this.nextmatch.activeFilters); + // Add in extras + for (var extra in this.options.filters) { + // Don't overwrite what nm has, chances are nm has more up-to-date value + if (typeof current_filters == 'undefined') { + current_filters[extra] = this.nextmatch.options.settings[extra]; + } + } + // Skip columns for now + delete current_filters.selcolumns; + // Add in application's settings + if (this.filters != true) { + for (var i = 0; i < this.filters.length; i++) { + current_filters[this.options.filters[i]] = this.nextmatch.options.settings[this.options.filters[i]]; + } + } + // Call framework + app[this.options.app].add_favorite(current_filters); + // Reset value + this.set_value(this.preferred, true); + } + else if (this.value == 'blank') { + // Reset filters when select no filters + this.set_nm_filters({}); + } + }; + et2_favorites.prototype.set_value = function (filter_name, parent) { + if (parent) { + return _super.prototype.set_value.call(this, filter_name); + } + if (filter_name == 'add') + return false; + app[this.options.app].setState(this.stored_filters[filter_name]); + return false; + }; + et2_favorites.prototype.getValue = function () { + return null; + }; + /** + * Set the nextmatch to filter + * From et2_INextmatchHeader interface + * + * @param {et2_nextmatch} nextmatch + */ + et2_favorites.prototype.setNextmatch = function (nextmatch) { + this.nextmatch = nextmatch; + if (this.nm_filter) { + this.set_value(this.nm_filter); + this.nm_filter = false; + } + // Re-generate filter list so we can add 'Add current' + this.init_filters(this); + }; + et2_favorites._attributes = { + "default_pref": { + "name": "Default preference key", + "type": "string", + "description": "The preference key where default favorite is stored (not the value)" + }, + "sidebox_target": { + "name": "Sidebox target", + "type": "string", + "description": "ID of element to insert favorite list into", + "default": "favorite_sidebox" + }, + "app": { + "name": "Application", + "type": "string", + "description": "Application to show favorites for" + }, + "filters": { + "name": "Extra filters", + "type": "any", + "description": "Array of extra filters to include in the saved favorite" + }, + // These are particular to favorites + id: { "default": "favorite" }, + label: { "default": "" }, + label_updates: { "default": false }, + image: { "default": egw().image('fav_filter') }, + statustext: { "default": "Favorite queries", "type": "string" } + }; + return et2_favorites; +}(et2_widget_dropdown_button_1.et2_dropdown_button)); +et2_core_widget_1.et2_register_widget(et2_favorites, ["favorites"]); +//# sourceMappingURL=et2_widget_favorites.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_favorites.ts b/api/js/etemplate/et2_widget_favorites.ts new file mode 100644 index 0000000000..0144e52f7f --- /dev/null +++ b/api/js/etemplate/et2_widget_favorites.ts @@ -0,0 +1,425 @@ +/** + * EGroupware eTemplate2 - JS Favorite widget + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage api + * @link http://www.egroupware.org + * @author Nathan Gray + * @copyright Nathan Gray 2013 + */ + +/*egw:uses + et2_dropdown_button; + et2_extension_nextmatch; +*/ + +import {et2_register_widget, WidgetConfig} from "./et2_core_widget"; +import {et2_INextmatchHeader} from "./et2_extension_nextmatch"; +import {et2_dropdown_button} from "./et2_widget_dropdown_button"; +import {ClassWithAttributes} from "./et2_core_inheritance"; + +/** + * Favorites widget, designed for use with a nextmatch widget + * + * The primary control is a split/dropdown button. Clicking on the left side of the button filters the + * nextmatch list by the user's default filter. The right side of the button gives a list of + * saved filters, pulled from preferences. Clicking a filter from the dropdown list sets the + * filters as saved. + * + * Favorites can also automatically be shown in the sidebox, using the special ID favorite_sidebox. + * Use the following code to generate the sidebox section: + * display_sidebox($appname,lang('Favorites'),array( + * array( + * 'no_lang' => true, + * 'text'=>'', + * 'link'=>false, + * 'icon' => false + * ) + * )); + * This sidebox list will be automatically generated and kept up to date. + * + * + * Favorites are implemented by saving the values for [column] filters. Filters are stored + * in preferences, with the name favorite_. The favorite favorite used for clicking on + * the filter button is stored in nextmatch--favorite. + * + * @augments et2_dropdown_button + */ +class et2_favorites extends et2_dropdown_button implements et2_INextmatchHeader +{ + static readonly _attributes : any = { + "default_pref": { + "name": "Default preference key", + "type": "string", + "description": "The preference key where default favorite is stored (not the value)" + }, + "sidebox_target": { + "name": "Sidebox target", + "type": "string", + "description": "ID of element to insert favorite list into", + "default": "favorite_sidebox" + }, + "app": { + "name": "Application", + "type": "string", + "description": "Application to show favorites for" + }, + "filters": { + "name": "Extra filters", + "type": "any", + "description": "Array of extra filters to include in the saved favorite" + }, + + // These are particular to favorites + id: {"default": "favorite"}, + label: {"default": ""}, + label_updates: { "default": false}, + image: {"default": egw().image('fav_filter')}, + statustext: {"default": "Favorite queries", "type": "string"} + }; + + // Some convenient variables, used in closures / event handlers + header = null; + nextmatch = null; + favorite_prefix: "favorite_"; + private stored_filters: {}; + private favSortedList : any = null; + private sidebox_target : JQuery = null; + private preferred; + static is_admin : boolean; + private filters : any; + + // If filter was set server side, we need to remember it until nm is created + nm_filter = false; + + /** + * Constructor + * + * @memberOf et2_favorites + */ + constructor(_parent?, _attrs? : WidgetConfig, _child? : object) + { + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_favorites._attributes, _child || {})); + this.sidebox_target = jQuery("#"+this.options.sidebox_target); + if(this.sidebox_target.length == 0 && egw_getFramework() != null) + { + let egw_fw = egw_getFramework(); + this.sidebox_target = jQuery("#"+this.options.sidebox_target,egw_fw.sidemenuDiv); + } + // Store array of sorted items + this.favSortedList = ['blank']; + + let apps = egw().user('apps'); + et2_favorites.is_admin = (typeof apps['admin'] != "undefined"); + + this.stored_filters = this.load_favorites(this.options.app); + + this.preferred = egw.preference(this.options.default_pref,this.options.app); + if(!this.preferred || typeof this.stored_filters[this.preferred] == "undefined") + { + this.preferred = "blank"; + } + + // It helps to have the ID properly set before we get too far + this.set_id(this.id); + + this.init_filters(this); + + this.menu.addClass("favorites"); + + // Set the default (button) value + this.set_value(this.preferred,true); + + let self = this; + + // Add a listener on the radio buttons to set default filter + jQuery(this.menu).on("click","input:radio", function(event){ + // Don't do the menu + event.stopImmediatePropagation(); + + // Save as default favorite - used when you click the button + self.egw().set_preference(self.options.app,self.options.default_pref,jQuery(this).val()); + self.preferred = jQuery(this).val(); + + // Update sidebox, if there + if(self.sidebox_target.length) + { + self.sidebox_target.find("div.ui-icon-heart") + .replaceWith("
          "); + jQuery("li[data-id='"+self.preferred+"'] div.sideboxstar",self.sidebox_target) + .replaceWith("
          "); + } + + // Close the menu + self.menu.hide(); + + // Some user feedback + self.button.addClass("ui-state-active", 500,"swing",function(){ + self.button.removeClass("ui-state-active",2000); + }); + }); + + //Sort DomNodes of sidebox fav. menu + let sideBoxDOMNodeSort = function (_favSList) { + let favS = jQuery.isArray(_favSList)?_favSList.slice(0).reverse():[]; + + for (let i=0; i < favS.length;i++) + { + self.sidebox_target.children().find('[data-id$="' + favS[i] + '"]').prependTo(self.sidebox_target.children()); + } + }; + + //Add Sortable handler to nm fav. menu + jQuery(this.menu).sortable({ + + items:'li:not([data-id$="add"])', + placeholder:'ui-fav-sortable-placeholder', + delay: 250, //(millisecond) delay before the sorting should start + update: function () + { + self.favSortedList = jQuery(this).sortable('toArray', {attribute:'data-id'}); + + self.egw().set_preference(self.options.app,'fav_sort_pref',self.favSortedList); + + sideBoxDOMNodeSort(self.favSortedList); + } + }); + + // Add a listener on the delete to remove + this.menu.on("click","div.ui-icon-trash", app[self.options.app], function() { + // App instance might not be ready yet, so don't bind directly + app[self.options.app].delete_favorite.apply(this,arguments); + }) + // Wrap and unwrap because jQueryUI styles use a parent, and we don't want to change the state of the menu item + // Wrap in a span instead of a div because div gets a border + .on("mouseenter","div.ui-icon-trash", function() {jQuery(this).wrap("");}) + .on("mouseleave","div.ui-icon-trash", function() {jQuery(this).unwrap();}); + + // Trigger refresh of menu options now that events are registered + // to update sidebox + if(this.sidebox_target.length > 0) + { + this.init_filters(this); + } + } + + /** + * Load favorites from preferences + * + * @param app String Load favorites from this application + */ + load_favorites(app) + { + + // Default blank filter + let stored_filters : any = { + 'blank': { + name: this.egw().lang("No filters"), + state: {} + } + }; + + // Load saved favorites + let preferences : any = egw.preference("*",app); + for(let pref_name in preferences) + { + if(pref_name.indexOf(this.favorite_prefix) == 0 && typeof preferences[pref_name] == 'object') + { + let name = pref_name.substr(this.favorite_prefix.length); + stored_filters[name] = preferences[pref_name]; + // Keep older favorites working - they used to store nm filters in 'filters',not state + if(preferences[pref_name]["filters"]) + { + stored_filters[pref_name]["state"] = preferences[pref_name]["filters"]; + } + } + if (pref_name == 'fav_sort_pref') + { + this.favSortedList = preferences[pref_name]; + //Make sure sorted list is always an array, seems some old fav are not array + if (!jQuery.isArray(this.favSortedList)) this.favSortedList = this.favSortedList.split(','); + } + } + if(typeof stored_filters == "undefined" || !stored_filters) + { + stored_filters = {}; + } + else + { + for(let name in stored_filters) + { + if (this.favSortedList.indexOf(name) < 0) + { + this.favSortedList.push(name); + } + } + this.egw().set_preference (this.options.app,'fav_sort_pref',this.favSortedList); + if (this.favSortedList.length > 0) + { + let sortedListObj = {}; + + for (let i=0; i < this.favSortedList.length; i++) + { + if (typeof stored_filters[this.favSortedList[i]] != 'undefined') + { + sortedListObj[this.favSortedList[i]] = stored_filters[this.favSortedList[i]]; + } + else + { + this.favSortedList.splice(i,1); + this.egw().set_preference (this.options.app,'fav_sort_pref',this.favSortedList); + } + } + stored_filters = jQuery.extend(sortedListObj,stored_filters); + } + } + return stored_filters; + } + + // Create & set filter options for dropdown menu + init_filters(widget, filters?) + { + if(typeof filters == "undefined") + { + filters = this.stored_filters; + } + + let options = {}; + for(let name in filters) + { + options[name] = ""+ + (filters[name].name != undefined ? filters[name].name : name) + + (filters[name].group != false && !et2_favorites.is_admin || name == 'blank' ? "" : + "
          "); + } + + // Only add 'Add current' if we have a nextmatch + if(this.nextmatch) + { + options["add"] = ""+this.egw().lang('Add current'); + } + widget.set_select_options.call(widget,options); + + // Set radio to current value + jQuery("input[value='"+ this.preferred +"']:radio", this.menu).attr("checked",1); + } + + set_nm_filters(filters) + { + if(this.nextmatch) + { + this.nextmatch.applyFilters(filters); + } + else + { + console.log(filters); + } + } + + onclick(node) + { + // Apply preferred filter - make sure it's an object, and not a reference + if(this.preferred && this.stored_filters[this.preferred]) + { + // use app[appname].setState if available to allow app to overwrite it (eg. change to non-listview in calendar) + if (typeof app[this.options.app] != 'undefined') + { + app[this.options.app].setState(this.stored_filters[this.preferred]); + } + else + { + this.set_nm_filters(jQuery.extend({},this.stored_filters[this.preferred].state)); + } + } + else + { + alert(this.egw().lang("No default set")); + } + } + + // Apply the favorite when you pick from the list + change(selected_node) + { + this.value = jQuery(selected_node).attr("data-id"); + if(this.value == "add" && this.nextmatch) + { + // Get current filters + let current_filters = jQuery.extend({},this.nextmatch.activeFilters); + + // Add in extras + for(let extra in this.options.filters) + { + // Don't overwrite what nm has, chances are nm has more up-to-date value + if(typeof current_filters == 'undefined') + { + current_filters[extra] = this.nextmatch.options.settings[extra]; + } + } + + // Skip columns for now + delete current_filters.selcolumns; + + // Add in application's settings + if(this.filters != true) + { + for(let i = 0; i < this.filters.length; i++) + { + current_filters[this.options.filters[i]] = this.nextmatch.options.settings[this.options.filters[i]]; + } + } + + // Call framework + app[this.options.app].add_favorite(current_filters); + + // Reset value + this.set_value(this.preferred,true); + } + else if (this.value == 'blank') + { + // Reset filters when select no filters + this.set_nm_filters({}); + } + } + + set_value(filter_name, parent? : boolean) : void | boolean + { + if(parent) + { + return super.set_value(filter_name); + } + + if(filter_name == 'add') return false; + + app[this.options.app].setState(this.stored_filters[filter_name]); + return false; + } + + getValue() + { + return null; + } + + /** + * Set the nextmatch to filter + * From et2_INextmatchHeader interface + * + * @param {et2_nextmatch} nextmatch + */ + setNextmatch(nextmatch) + { + this.nextmatch = nextmatch; + + if(this.nm_filter) + { + this.set_value(this.nm_filter); + this.nm_filter = false; + } + + // Re-generate filter list so we can add 'Add current' + this.init_filters(this); + } +} +et2_register_widget(et2_favorites, ["favorites"]); + diff --git a/api/js/etemplate/et2_widget_file.js b/api/js/etemplate/et2_widget_file.js index b274069d28..78b5254108 100644 --- a/api/js/etemplate/et2_widget_file.js +++ b/api/js/etemplate/et2_widget_file.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - JS Number object * @@ -9,710 +10,611 @@ * @copyright Nathan Gray 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_inputWidget; - phpgwapi.Resumable.resumable; + et2_core_inputWidget; + phpgwapi.Resumable.resumable; */ - +var et2_core_inputWidget_1 = require("./et2_core_inputWidget"); +var et2_core_widget_1 = require("./et2_core_widget"); +var et2_core_inheritance_1 = require("./et2_core_inheritance"); /** * Class which implements file upload * * @augments et2_inputWidget */ -var et2_file = (function(){ "use strict"; return et2_inputWidget.extend( -{ - attributes: { - "multiple": { - "name": "Multiple files", - "type": "boolean", - "default": false, - "description": "Allow the user to select more than one file to upload at a time. Subject to browser support." - }, - "max_file_size": { - "name": "Maximum file size", - "type": "integer", - "default":0, - "description": "Largest file accepted, in bytes. Subject to server limitations. 8MB = 8388608" - }, - "mime": { - "name": "Allowed file types", - "type": "string", - "default": et2_no_init, - "description": "Mime type (eg: image/png) or regex (eg: /^text\//i) for allowed file types" - }, - "blur": { - "name": "Placeholder", - "type": "string", - "default": "", - "description": "This text get displayed if an input-field is empty and does not have the input-focus (blur). It can be used to show a default value or a kind of help-text." - }, - "progress": { - "name": "Progress node", - "type": "string", - "default": et2_no_init, - "description": "The ID of an alternate node (div) to display progress and results. The Node is fetched with et2 getWidgetById so you MUST use the id assigned in XET-File (it may not be available at creation time, so we (re)check on createStatus time)" - }, - "onStart": { - "name": "Start event handler", - "type": "any", - "default": et2_no_init, - "description": "A (js) function called when an upload starts. Return true to continue with upload, false to cancel." - }, - "onFinish": { - "name": "Finish event handler", - "type": "any", - "default": et2_no_init, - "description": "A (js) function called when all files to be uploaded are finished." - }, - drop_target: { - "name": "Optional, additional drop target for HTML5 uploads", - "type": "string", - "default": et2_no_init, - "description": "The ID of an additional drop target for HTML5 drag-n-drop file uploads" - }, - label: { - "name": "Label of file upload", - "type": "string", - "default": "Choose file...", - "description": "String caption to be displayed on file upload span" - }, - progress_dropdownlist: { - "name": "List on files in progress like dropdown", - "type": "boolean", - "default": false, - "description": "Style list of files in uploading progress like dropdown list with a total upload progress indicator" - }, - onFinishOne: { - "name": "Finish event handler for each one", - "type": "any", - "default": et2_no_init, - "description": "A (js) function called when a file to be uploaded is finished." - }, - accept: { - "name": "Acceptable extensions", - "type": "string", - "default": '', - "description": "Define types of files that the server accepts. Multiple types can be seperated by comma and the default is to accept everything." - }, - chunk_size: { - "name": "Chunk size", - "type": "integer", - "default": 1024*1024, - "description": "Max chunk size, gets set from server-side PHP (max_upload_size-1M)/2" // last chunk can be up to 2*chunk_size! - } - }, - - asyncOptions: {}, - - /** - * Constructor - * - * @memberOf et2_file - */ - init: function(_parent, attrs) { - this._super.apply(this, arguments); - - this.node = null; - this.input = null; - this.progress = null; - this.span = null; - // Contains all submit buttons need to be disabled during upload process - this.disabled_buttons = jQuery("input[type='submit'], button"); - if(!this.options.value) this.options.value = {}; - - if(!this.options.id) { - console.warn("File widget needs an ID. Used 'file_widget'."); - this.options.id = "file_widget"; - } - - // Legacy - id ending in [] means multiple - if(this.options.id.substr(-2) == "[]") - { - this.options.multiple = true; - } - // If ID ends in /, it's a directory - allow multiple - else if (this.options.id.substr(-1) === "/") - { - this.options.multiple = true; - attrs.multiple = true; - } - - // Set up the URL to have the request ID & the widget ID - var instance = this.getInstanceManager(); - - var self = this; - - this.asyncOptions = jQuery.extend({ - // Callbacks - onStart: function(event, file_count) { - return self.onStart(event, file_count); - }, - onFinish: function(event, file_count) { - self.onFinish.apply(self, [event, file_count]) - }, - onStartOne: function(event, file_name, index, file_count) { - - }, - onFinishOne: function(event, response, name, number, total) { return self.finishUpload(event,response,name,number,total);}, - onProgress: function(event, progress, name, number, total) { return self.onProgress(event,progress,name,number,total);}, - onError: function(event, name, error) { return self.onError(event,name,error);}, - beforeSend: function(form) { return self.beforeSend(form);}, - - chunkSize: this.options.chunk_size || 1024*1024, - - target: egw.ajaxUrl("EGroupware\\Api\\Etemplate\\Widget\\File::ajax_upload"), - query: function(file) {return self.beforeSend(file);}, - // Disable checking for already uploaded chunks - testChunks: false - },this.asyncOptions); - this.asyncOptions.fieldName = this.options.id; - this.createInputWidget(); - this.set_readonly(this.options.readonly); - }, - - destroy: function() { - this._super.apply(this, arguments); - this.set_drop_target(null); - this.node = null; - this.input = null; - this.span = null; - this.progress = null; - }, - - createInputWidget: function() { - this.node = jQuery(document.createElement("div")).addClass("et2_file"); - this.span = jQuery(document.createElement("span")) - .addClass('et2_file_span et2_button') - .appendTo (this.node); - if (this.options.label != '') this.span.addClass('et2_button_text'); - var span = this.span; - this.input = jQuery(document.createElement("input")) - .attr("type", "file").attr("placeholder", this.options.blur) - .addClass ("et2_file_upload") - .appendTo(this.node) - .hover(function(e){ - jQuery(span) - .toggleClass('et2_file_spanHover'); - }) - .on({ - mousedown:function (e){ - jQuery(span).addClass('et2_file_spanActive'); - }, - mouseup:function (e){ - jQuery(span).removeClass('et2_file_spanActive'); - } - }); - if (this.options.accept) this.input.attr('accept', this.options.accept); - var self = this; - // trigger native input upload file - if (!this.options.readonly) this.span.click(function(){self.input.click()}); - // Check for File interface, should fall back to normal form submit if missing - if(typeof File != "undefined" && typeof (new XMLHttpRequest()).upload != "undefined") - { - this.resumable = new Resumable(this.asyncOptions); - this.resumable.assignBrowse(this.input); - this.resumable.on('fileAdded', jQuery.proxy(this._fileAdded, this)); - this.resumable.on('fileProgress', jQuery.proxy(this._fileProgress, this)); - this.resumable.on('fileSuccess', jQuery.proxy(this.finishUpload, this)); - this.resumable.on('complete', jQuery.proxy(this.onFinish, this)); - } - else - { - // This may be a problem submitting via ajax - } - if(this.options.progress) - { - var widget = this.getRoot().getWidgetById(this.options.progress); - if(widget) - { - //may be not available at createInputWidget time - this.progress = jQuery(widget.getDOMNode()); - } - } - if(!this.progress) - { - this.progress = jQuery(document.createElement("div")).appendTo(this.node); - } - this.progress.addClass("progress"); - - if(this.options.multiple) - { - this.input.attr("multiple","multiple"); - } - - this.setDOMNode(this.node[0]); - }, - - /** - * Set a widget or DOM node as a HTML5 file drop target - * - * @param String new_target widget ID or DOM node ID to be used as a new target - */ - set_drop_target: function(new_target) - { - // Cancel old drop target - if(this.options.drop_target) - { - var widget = this.getRoot().getWidgetById(this.options.drop_target); - var drop_target = widget && widget.getDOMNode() || document.getElementById(this.options.drop_target); - if(drop_target) - { - this.resumable.unAssignDrop(drop_target); - } - } - - this.options.drop_target = new_target; - - if(!this.options.drop_target) return; - - // Set up new drop target - var widget = this.getRoot().getWidgetById(this.options.drop_target); - var drop_target = widget && widget.getDOMNode() || document.getElementById(this.options.drop_target); - if(drop_target) - { - this.resumable.assignDrop([drop_target]); - } - else - { - this.egw().debug("warn", "Did not find file drop target %s", this.options.drop_target); - } - - }, - attachToDOM: function() { - this._super.apply(this, arguments); - // Override parent's change, file widget will fire change when finished uploading - this.input.unbind("change.et2_inputWidget"); - }, - getValue: function() { - var value = this.options.value ? this.options.value : this.input.val(); - return value; - }, - - /** - * Set the value of the file widget. - * - * If you pass a FileList or list of files, it will trigger the async upload - * - * @param {FileList|File[]|false} value List of files to be uploaded, or false to reset. - * @param {Event} event Most browsers require the user to initiate file transfers in some way. - * Pass the event in, if you have it. - */ - set_value: function(value, event) { - if(!value || typeof value == "undefined") - { - value = {}; - } - if(jQuery.isEmptyObject(value)) - { - this.options.value = {}; - if (this.resumable.progress() == 1) this.progress.empty(); - - // Reset the HTML element - this.input.wrap('
          ').closest('form').get(0).reset(); - this.input.unwrap(); - - return; - } - - var addFile = jQuery.proxy(function(i, file) { - this.resumable.addFile(file,event); - }, this); - if(typeof value == 'object' && value.length && typeof value[0] == 'object' && value[0].name) - { - try - { - this.input[0].files = value; - - jQuery.each(value, addFile); - } - catch (e) - { - var self = this; - var args = arguments; - jQuery.each(value, addFile); - } - } - }, - - /** - * Set the value for label - * The label is used as caption for span tag which customize the HTML file upload styling - * - * @param {string} value text value of label - */ - set_label: function (value) - { - if (this.span != null && value != null) - { - this.span.text(value); - } - }, - - getInputNode: function() { - if (typeof this.input == 'undefined') return false; - return this.input[0]; - }, - - - set_mime: function(mime) { - if(!mime) - { - this.options.mime = null; - } - if(mime.indexOf("/") != 0) - { - // Lower case it now, if it's not a regex - this.options.mime = mime.toLowerCase(); - } - else - { - // Convert into a js regex - var parts = mime.substr(1).match(/(.*)\/([igm]?)$/); - this.options.mime = new RegExp(parts[1],parts.length > 2 ? parts[2] : ""); - } - }, - - set_multiple: function(_multiple) { - this.options.multiple = _multiple; - if(_multiple) - { - return this.input.attr("multiple", "multiple"); - } - return this.input.removeAttr("multiple"); - }, - /** - * Check to see if the provided file's mimetype matches - * - * @param f File object - * @return boolean - */ - checkMime: function(f) { - // If missing, let the server handle it - if(!this.options.mime || !f.type) return true; - - var is_preg = (typeof this.options.mime == "object"); - if(!is_preg && f.type.toLowerCase() == this.options.mime || is_preg && this.options.mime.test(f.type)) - { - return true; - } - - // Not right mime - return false; - }, - - _fileAdded: function(file,event) { - // Manual additions have no event - if(typeof event == 'undefined') - { - event = {}; - } - // Trigger start of uploading, calls callback - if(!this.resumable.isUploading()) - { - if (!(this.onStart(event,this.resumable.files.length))) return; - } - - // Here 'this' is the input - if(this.checkMime(file.file)) - { - if(this.createStatus(event,file)) - { - - // Disable buttons - this.disabled_buttons - .not("[disabled]") - .attr("disabled", true) - .addClass('et2_button_ro') - .removeClass('et2_clickable') - .css('cursor', 'default'); - - // Actually start uploading - this.resumable.upload(); - } - } - else - { - // Wrong mime type - show in the list of files - return this.createStatus( - this.egw().lang("File is of wrong type (%1 != %2)!", file.file.type, this.options.mime), - file - ); - } - - }, - - /** - * Add in the request id - */ - beforeSend: function(form) { - var instance = this.getInstanceManager(); - - return { - request_id: instance.etemplate_exec_id, - widget_id: this.id - }; - }, - - /** - * Disables submit buttons while uploading - */ - onStart: function(event, file_count) { - // Hide any previous errors - this.hideMessage(); - - - event.data = this; - - //Add dropdown_progress - if (this.options.progress_dropdownlist) - { - this._build_progressDropDownList(); - } - - // Callback - if(this.options.onStart) return et2_call(this.options.onStart, event, file_count); - return true; - }, - - /** - * Re-enables submit buttons when done - */ - onFinish: function() { - this.disabled_buttons.attr("disabled", false).css('cursor','pointer').removeClass('et2_button_ro'); - - var file_count = this.resumable.files.length; - - // Remove files from list - while(this.resumable.files.length > 0) - { - this.resumable.removeFile(this.resumable.files[this.resumable.files.length -1]); - } - - var event = jQuery.Event('upload'); - - event.data = this; - - var result = false; - - //Remove progress_dropDown_fileList class and unbind the click handler from body - if (this.options.progress_dropdownlist) - { - this.progress.removeClass("progress_dropDown_fileList"); - jQuery(this.node).find('span').removeClass('totalProgress_loader'); - jQuery('body').off('click'); - } - - if(this.options.onFinish && !jQuery.isEmptyObject(this.getValue())) - { - result = et2_call(this.options.onFinish, event, file_count); - } - else - { - result = (file_count == 0 || !jQuery.isEmptyObject(this.getValue())); - } - if(result) - { - // Fire legacy change action when done - this.change(this.input); - } - }, - - /** - * Build up dropdown progress with total count indicator - * - * @todo Implement totalProgress bar instead of ajax-loader, in order to show how much percent of uploading is completed - */ - _build_progressDropDownList: function () - { - this.progress.addClass("progress_dropDown_fileList"); - - //Add uploading indicator and bind hover handler on it - jQuery(this.node).find('span').addClass('totalProgress_loader'); - - jQuery(this.node).find('span.et2_file_span').hover(function(){ - jQuery('.progress_dropDown_fileList').show(); - }); - //Bind click handler to dismiss the dropdown while uploading - jQuery('body').on('click', function(event){ - if (event.target.className != 'remove') - { - jQuery('.progress_dropDown_fileList').hide(); - } - }); - - }, - - /** - * Creates the elements used for displaying the file, and it's upload status, and - * attaches them to the DOM - * - * @param _event Either the event, or an error message - */ - createStatus: function(_event, file) { - var error = (typeof _event == "object" ? "" : _event); - - if(this.options.max_file_size && file.size > this.options.max_file_size) { - error = this.egw().lang("File too large. Maximum %1", et2_vfsSize.prototype.human_size(this.options.max_file_size)); - } - - if(this.options.progress) - { - var widget = this.getRoot().getWidgetById(this.options.progress); - if(widget) - { - this.progress = jQuery(widget.getDOMNode()); - this.progress.addClass("progress"); - } - } - if(this.progress) - { - var fileName = file.fileName || 'file'; - var status = jQuery("
        • "+fileName - +"

        • ") - .appendTo(this.progress); - jQuery("div.remove",status).on('click', file, jQuery.proxy(this.cancel,this)); - if(error != "") - { - status.addClass("message ui-state-error"); - status.append("
          "+error+""); - jQuery(".progressBar",status).css("display", "none"); - } - } - return error == ""; - }, - - _fileProgress: function(file) { - if(this.progress) - { - jQuery("li[data-file='"+file.fileName.replace(/'/g, '"')+"'] > span.progressBar > p").css("width", Math.ceil(file.progress()*100)+"%"); - - } - return true; - }, - - onError: function(event, name, error) { - console.warn(event,name,error); - }, - - /** - * A file upload is finished, update the UI - */ - finishUpload: function(file, response) { - var name = file.fileName || 'file'; - - if(typeof response == 'string') response = jQuery.parseJSON(response); - if(response.response[0] && typeof response.response[0].data.length == 'undefined') { - if(typeof this.options.value !== 'object' || !this.options.multiple) - { - this.set_value({}); - } - for(var key in response.response[0].data) { - if(typeof response.response[0].data[key] == "string") - { - // Message from server - probably error - jQuery("[data-file='"+name.replace(/'/g, '"')+"']",this.progress) - .addClass("error") - .css("display", "block") - .text(response.response[0].data[key]); - } - else - { - this.options.value[key] = response.response[0].data[key]; - // If not multiple, we already destroyed the status, so re-create it - if(!this.options.multiple) - { - this.createStatus({}, file); - } - if(this.progress) - { - jQuery("[data-file='"+name.replace(/'/g, '"')+"']",this.progress).addClass("message success"); - } - } - } - } - else if (this.progress) - { - jQuery("[data-file='"+name.replace(/'/g, '"')+"']",this.progress) - .addClass("ui-state-error") - .css("display", "block") - .text(this.egw().lang("Server error")); - } - var event = jQuery.Event('upload'); - - event.data = this; - - // Callback - if(this.options.onFinishOne) - { - return et2_call(this.options.onFinishOne,event,response,name); - } - return true; - }, - - /** - * Remove a file from the list of values - * - * @param {File|string} File object, or file name, to remove - */ - remove_file: function(file) - { - //console.info(filename); - if(typeof file == 'string') - { - file = {fileName: file}; - } - for(var key in this.options.value) - { - if(this.options.value[key].name == file.fileName) - { - delete this.options.value[key]; - jQuery('[data-file="'+file.fileName.replace(/'/g, '"')+'"]',this.node).remove(); - return; - } - } - if(file.isComplete && !file.isComplete() && file.cancel) file.cancel(); - }, - - /** - * Cancel a file - event callback - */ - cancel: function(e) - { - e.preventDefault(); - // Look for file name in list - var target = jQuery(e.target).parents("li"); - - this.remove_file(e.data); - - // In case it didn't make it to the list (error) - target.remove(); - jQuery(e.target).remove(); - }, - - /** - * Set readonly - * - * @param {boolean} _ro boolean readonly state, true means readonly - */ - set_readonly: function(_ro) - { - if (typeof _ro != "undefined") - { - this.options.readonly = _ro; - this.span.toggleClass('et2_file_ro',_ro); - if (this.options.readonly) - { - this.span.unbind('click'); - } - else - { - var self = this; - this.span.off().bind('click',function(){self.input.click()}); - } - } - } -});}).call(this); - -et2_register_widget(et2_file, ["file"]); - +var et2_file = /** @class */ (function (_super) { + __extends(et2_file, _super); + /** + * Constructor + * + * @memberOf et2_file + */ + function et2_file(_parent, _attrs, _child) { + var _this = + // Call the inherited constructor + _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_file._attributes, _child || {})) || this; + _this.asyncOptions = {}; + _this.input = null; + _this.progress = null; + _this.span = null; + _this.node = null; + _this.input = null; + _this.progress = null; + _this.span = null; + // Contains all submit buttons need to be disabled during upload process + _this.disabled_buttons = jQuery("input[type='submit'], button"); + if (!_this.options.value) + _this.options.value = {}; + if (!_this.options.id) { + console.warn("File widget needs an ID. Used 'file_widget'."); + _this.options.id = "file_widget"; + } + // Legacy - id ending in [] means multiple + if (_this.options.id.substr(-2) == "[]") { + _this.options.multiple = true; + } + // If ID ends in /, it's a directory - allow multiple + else if (_this.options.id.substr(-1) === "/") { + _this.options.multiple = true; + _attrs.multiple = true; + } + // Set up the URL to have the request ID & the widget ID + var instance = _this.getInstanceManager(); + var self = _this; + _this.asyncOptions = jQuery.extend({ + // Callbacks + onStart: function (event, file_count) { + return self.onStart(event, file_count); + }, + onFinish: function (event, file_count) { + self.onFinish.apply(self, [event, file_count]); + }, + onStartOne: function (event, file_name, index, file_count) { + }, + onFinishOne: function (event, response, name, number, total) { return self.finishUpload(event, response, name, number, total); }, + onProgress: function (event, progress, name, number, total) { return self.onProgress(event, progress, name, number, total); }, + onError: function (event, name, error) { return self.onError(event, name, error); }, + beforeSend: function (form) { return self.beforeSend(form); }, + chunkSize: _this.options.chunk_size || 1024 * 1024, + target: egw.ajaxUrl("EGroupware\\Api\\Etemplate\\Widget\\File::ajax_upload"), + query: function (file) { return self.beforeSend(file); }, + // Disable checking for already uploaded chunks + testChunks: false + }, _this.asyncOptions); + _this.asyncOptions.fieldName = _this.options.id; + _this.createInputWidget(); + _this.set_readonly(_this.options.readonly); + return _this; + } + et2_file.prototype.destroy = function () { + _super.prototype.destroy.call(this); + this.set_drop_target(null); + this.node = null; + this.input = null; + this.span = null; + this.progress = null; + }; + et2_file.prototype.createInputWidget = function () { + this.node = jQuery(document.createElement("div")).addClass("et2_file"); + this.span = jQuery(document.createElement("span")) + .addClass('et2_file_span et2_button') + .appendTo(this.node); + if (this.options.label != '') + this.span.addClass('et2_button_text'); + var span = this.span; + this.input = jQuery(document.createElement("input")) + .attr("type", "file").attr("placeholder", this.options.blur) + .addClass("et2_file_upload") + .appendTo(this.node) + .hover(function () { + jQuery(span) + .toggleClass('et2_file_spanHover'); + }) + .on({ + mousedown: function () { + jQuery(span).addClass('et2_file_spanActive'); + }, + mouseup: function () { + jQuery(span).removeClass('et2_file_spanActive'); + } + }); + if (this.options.accept) + this.input.attr('accept', this.options.accept); + var self = this; + // trigger native input upload file + if (!this.options.readonly) + this.span.click(function () { self.input.click(); }); + // Check for File interface, should fall back to normal form submit if missing + if (typeof File != "undefined" && typeof (new XMLHttpRequest()).upload != "undefined") { + this.resumable = new Resumable(this.asyncOptions); + this.resumable.assignBrowse(this.input); + this.resumable.on('fileAdded', jQuery.proxy(this._fileAdded, this)); + this.resumable.on('fileProgress', jQuery.proxy(this._fileProgress, this)); + this.resumable.on('fileSuccess', jQuery.proxy(this.finishUpload, this)); + this.resumable.on('complete', jQuery.proxy(this.onFinish, this)); + } + else { + // This may be a problem submitting via ajax + } + if (this.options.progress) { + var widget = this.getRoot().getWidgetById(this.options.progress); + if (widget) { + //may be not available at createInputWidget time + this.progress = jQuery(widget.getDOMNode()); + } + } + if (!this.progress) { + this.progress = jQuery(document.createElement("div")).appendTo(this.node); + } + this.progress.addClass("progress"); + if (this.options.multiple) { + this.input.attr("multiple", "multiple"); + } + this.setDOMNode(this.node[0]); + }; + /** + * Set a widget or DOM node as a HTML5 file drop target + * + * @param {string} new_target widget ID or DOM node ID to be used as a new target + */ + et2_file.prototype.set_drop_target = function (new_target) { + // Cancel old drop target + if (this.options.drop_target) { + var widget_1 = this.getRoot().getWidgetById(this.options.drop_target); + var drop_target_1 = widget_1 && widget_1.getDOMNode() || document.getElementById(this.options.drop_target); + if (drop_target_1) { + this.resumable.unAssignDrop(drop_target_1); + } + } + this.options.drop_target = new_target; + if (!this.options.drop_target) + return; + // Set up new drop target + var widget = this.getRoot().getWidgetById(this.options.drop_target); + var drop_target = widget && widget.getDOMNode() || document.getElementById(this.options.drop_target); + if (drop_target) { + this.resumable.assignDrop([drop_target]); + } + else { + this.egw().debug("warn", "Did not find file drop target %s", this.options.drop_target); + } + }; + et2_file.prototype.attachToDOM = function () { + var res = _super.prototype.attachToDOM.call(this); + // Override parent's change, file widget will fire change when finished uploading + this.input.unbind("change.et2_inputWidget"); + return res; + }; + et2_file.prototype.getValue = function () { + return this.options.value ? this.options.value : this.input.val(); + }; + /** + * Set the value of the file widget. + * + * If you pass a FileList or list of files, it will trigger the async upload + * + * @param {FileList|File[]|false} value List of files to be uploaded, or false to reset. + * @param {Event} event Most browsers require the user to initiate file transfers in some way. + * Pass the event in, if you have it. + */ + et2_file.prototype.set_value = function (value, event) { + if (!value || typeof value == "undefined") { + value = {}; + } + if (jQuery.isEmptyObject(value)) { + this.options.value = {}; + if (this.resumable.progress() == 1) + this.progress.empty(); + // Reset the HTML element + this.input.wrap('').closest('form').get(0).reset(); + this.input.unwrap(); + return; + } + var addFile = jQuery.proxy(function (i, file) { + this.resumable.addFile(file, event); + }, this); + if (typeof value == 'object' && value.length && typeof value[0] == 'object' && value[0].name) { + try { + this.input[0].files = value; + jQuery.each(value, addFile); + } + catch (e) { + var self = this; + var args = arguments; + jQuery.each(value, addFile); + } + } + }; + /** + * Set the value for label + * The label is used as caption for span tag which customize the HTML file upload styling + * + * @param {string} value text value of label + */ + et2_file.prototype.set_label = function (value) { + if (this.span != null && value != null) { + this.span.text(value); + } + }; + et2_file.prototype.getInputNode = function () { + if (typeof this.input == 'undefined') + return false; + return this.input[0]; + }; + et2_file.prototype.set_mime = function (mime) { + if (!mime) { + this.options.mime = null; + } + if (mime.indexOf("/") != 0) { + // Lower case it now, if it's not a regex + this.options.mime = mime.toLowerCase(); + } + else { + // Convert into a js regex + var parts = mime.substr(1).match(/(.*)\/([igm]?)$/); + this.options.mime = new RegExp(parts[1], parts.length > 2 ? parts[2] : ""); + } + }; + et2_file.prototype.set_multiple = function (_multiple) { + this.options.multiple = _multiple; + if (_multiple) { + return this.input.attr("multiple", "multiple"); + } + return this.input.removeAttr("multiple"); + }; + /** + * Check to see if the provided file's mimetype matches + * + * @param f File object + * @return boolean + */ + et2_file.prototype.checkMime = function (f) { + // If missing, let the server handle it + if (!this.options.mime || !f.type) + return true; + var is_preg = (typeof this.options.mime == "object"); + if (!is_preg && f.type.toLowerCase() == this.options.mime || is_preg && this.options.mime.test(f.type)) { + return true; + } + // Not right mime + return false; + }; + et2_file.prototype._fileAdded = function (file, event) { + // Manual additions have no event + if (typeof event == 'undefined') { + event = {}; + } + // Trigger start of uploading, calls callback + if (!this.resumable.isUploading()) { + if (!(this.onStart(event, this.resumable.files.length))) + return; + } + // Here 'this' is the input + if (this.checkMime(file.file)) { + if (this.createStatus(event, file)) { + // Disable buttons + this.disabled_buttons + .not("[disabled]") + .attr("disabled", true) + .addClass('et2_button_ro') + .removeClass('et2_clickable') + .css('cursor', 'default'); + // Actually start uploading + this.resumable.upload(); + } + } + else { + // Wrong mime type - show in the list of files + return this.createStatus(this.egw().lang("File is of wrong type (%1 != %2)!", file.file.type, this.options.mime), file); + } + }; + /** + * Add in the request id + */ + et2_file.prototype.beforeSend = function (form) { + var instance = this.getInstanceManager(); + return { + request_id: instance.etemplate_exec_id, + widget_id: this.id + }; + }; + /** + * Disables submit buttons while uploading + */ + et2_file.prototype.onStart = function (event, file_count) { + // Hide any previous errors + this.hideMessage(); + event.data = this; + //Add dropdown_progress + if (this.options.progress_dropdownlist) { + this._build_progressDropDownList(); + } + // Callback + if (this.options.onStart) + return et2_call(this.options.onStart, event, file_count); + return true; + }; + /** + * Re-enables submit buttons when done + */ + et2_file.prototype.onFinish = function () { + this.disabled_buttons.attr("disabled", 0).css('cursor', 'pointer').removeClass('et2_button_ro'); + var file_count = this.resumable.files.length; + // Remove files from list + while (this.resumable.files.length > 0) { + this.resumable.removeFile(this.resumable.files[this.resumable.files.length - 1]); + } + var event = jQuery.Event('upload'); + event.data = this; + var result = false; + //Remove progress_dropDown_fileList class and unbind the click handler from body + if (this.options.progress_dropdownlist) { + this.progress.removeClass("progress_dropDown_fileList"); + jQuery(this.node).find('span').removeClass('totalProgress_loader'); + jQuery('body').off('click'); + } + if (this.options.onFinish && !jQuery.isEmptyObject(this.getValue())) { + result = et2_call(this.options.onFinish, event, file_count); + } + else { + result = (file_count == 0 || !jQuery.isEmptyObject(this.getValue())); + } + if (result) { + // Fire legacy change action when done + this.change(this.input); + } + }; + /** + * Build up dropdown progress with total count indicator + * + * @todo Implement totalProgress bar instead of ajax-loader, in order to show how much percent of uploading is completed + */ + et2_file.prototype._build_progressDropDownList = function () { + this.progress.addClass("progress_dropDown_fileList"); + //Add uploading indicator and bind hover handler on it + jQuery(this.node).find('span').addClass('totalProgress_loader'); + jQuery(this.node).find('span.et2_file_span').hover(function () { + jQuery('.progress_dropDown_fileList').show(); + }); + //Bind click handler to dismiss the dropdown while uploading + jQuery('body').on('click', function (event) { + if (event.target.className != 'remove') { + jQuery('.progress_dropDown_fileList').hide(); + } + }); + }; + /** + * Creates the elements used for displaying the file, and it's upload status, and + * attaches them to the DOM + * + * @param _event Either the event, or an error message + */ + et2_file.prototype.createStatus = function (_event, file) { + var error = (typeof _event == "object" ? "" : _event); + if (this.options.max_file_size && file.size > this.options.max_file_size) { + error = this.egw().lang("File too large. Maximum %1", et2_vfsSize.prototype.human_size(this.options.max_file_size)); + } + if (this.options.progress) { + var widget = this.getRoot().getWidgetById(this.options.progress); + if (widget) { + this.progress = jQuery(widget.getDOMNode()); + this.progress.addClass("progress"); + } + } + if (this.progress) { + var fileName = file.fileName || 'file'; + var status = jQuery("
        • " + fileName + + "

        • ") + .appendTo(this.progress); + jQuery("div.remove", status).on('click', file, jQuery.proxy(this.cancel, this)); + if (error != "") { + status.addClass("message ui-state-error"); + status.append("
          " + error + ""); + jQuery(".progressBar", status).css("display", "none"); + } + } + return error == ""; + }; + et2_file.prototype._fileProgress = function (file) { + if (this.progress) { + jQuery("li[data-file='" + file.fileName.replace(/'/g, '"') + "'] > span.progressBar > p").css("width", Math.ceil(file.progress() * 100) + "%"); + } + return true; + }; + et2_file.prototype.onError = function (event, name, error) { + console.warn(event, name, error); + }; + /** + * A file upload is finished, update the UI + */ + et2_file.prototype.finishUpload = function (file, response) { + var name = file.fileName || 'file'; + if (typeof response == 'string') + response = jQuery.parseJSON(response); + if (response.response[0] && typeof response.response[0].data.length == 'undefined') { + if (typeof this.options.value !== 'object' || !this.options.multiple) { + this.set_value({}); + } + for (var key in response.response[0].data) { + if (typeof response.response[0].data[key] == "string") { + // Message from server - probably error + jQuery("[data-file='" + name.replace(/'/g, '"') + "']", this.progress) + .addClass("error") + .css("display", "block") + .text(response.response[0].data[key]); + } + else { + this.options.value[key] = response.response[0].data[key]; + // If not multiple, we already destroyed the status, so re-create it + if (!this.options.multiple) { + this.createStatus({}, file); + } + if (this.progress) { + jQuery("[data-file='" + name.replace(/'/g, '"') + "']", this.progress).addClass("message success"); + } + } + } + } + else if (this.progress) { + jQuery("[data-file='" + name.replace(/'/g, '"') + "']", this.progress) + .addClass("ui-state-error") + .css("display", "block") + .text(this.egw().lang("Server error")); + } + var event = jQuery.Event('upload'); + event.data = this; + // Callback + if (this.options.onFinishOne) { + return et2_call(this.options.onFinishOne, event, response, name); + } + return true; + }; + /** + * Remove a file from the list of values + * + * @param {File|string} File object, or file name, to remove + */ + et2_file.prototype.remove_file = function (file) { + //console.info(filename); + if (typeof file == 'string') { + file = { fileName: file }; + } + for (var key in this.options.value) { + if (this.options.value[key].name == file.fileName) { + delete this.options.value[key]; + jQuery('[data-file="' + file.fileName.replace(/'/g, '"') + '"]', this.node).remove(); + return; + } + } + if (file.isComplete && !file.isComplete() && file.cancel) + file.cancel(); + }; + /** + * Cancel a file - event callback + */ + et2_file.prototype.cancel = function (e) { + e.preventDefault(); + // Look for file name in list + var target = jQuery(e.target).parents("li"); + this.remove_file(e.data); + // In case it didn't make it to the list (error) + target.remove(); + jQuery(e.target).remove(); + }; + /** + * Set readonly + * + * @param {boolean} _ro boolean readonly state, true means readonly + */ + et2_file.prototype.set_readonly = function (_ro) { + if (typeof _ro != "undefined") { + this.options.readonly = _ro; + this.span.toggleClass('et2_file_ro', _ro); + if (this.options.readonly) { + this.span.unbind('click'); + } + else { + var self = this; + this.span.off().bind('click', function () { self.input.click(); }); + } + } + }; + et2_file._attributes = { + "multiple": { + "name": "Multiple files", + "type": "boolean", + "default": false, + "description": "Allow the user to select more than one file to upload at a time. Subject to browser support." + }, + "max_file_size": { + "name": "Maximum file size", + "type": "integer", + "default": 0, + "description": "Largest file accepted, in bytes. Subject to server limitations. 8MB = 8388608" + }, + "mime": { + "name": "Allowed file types", + "type": "string", + "default": et2_no_init, + "description": "Mime type (eg: image/png) or regex (eg: /^text\//i) for allowed file types" + }, + "blur": { + "name": "Placeholder", + "type": "string", + "default": "", + "description": "This text get displayed if an input-field is empty and does not have the input-focus (blur). It can be used to show a default value or a kind of help-text." + }, + "progress": { + "name": "Progress node", + "type": "string", + "default": et2_no_init, + "description": "The ID of an alternate node (div) to display progress and results. The Node is fetched with et2 getWidgetById so you MUST use the id assigned in XET-File (it may not be available at creation time, so we (re)check on createStatus time)" + }, + "onStart": { + "name": "Start event handler", + "type": "any", + "default": et2_no_init, + "description": "A (js) function called when an upload starts. Return true to continue with upload, false to cancel." + }, + "onFinish": { + "name": "Finish event handler", + "type": "any", + "default": et2_no_init, + "description": "A (js) function called when all files to be uploaded are finished." + }, + drop_target: { + "name": "Optional, additional drop target for HTML5 uploads", + "type": "string", + "default": et2_no_init, + "description": "The ID of an additional drop target for HTML5 drag-n-drop file uploads" + }, + label: { + "name": "Label of file upload", + "type": "string", + "default": "Choose file...", + "description": "String caption to be displayed on file upload span" + }, + progress_dropdownlist: { + "name": "List on files in progress like dropdown", + "type": "boolean", + "default": false, + "description": "Style list of files in uploading progress like dropdown list with a total upload progress indicator" + }, + onFinishOne: { + "name": "Finish event handler for each one", + "type": "any", + "default": et2_no_init, + "description": "A (js) function called when a file to be uploaded is finished." + }, + accept: { + "name": "Acceptable extensions", + "type": "string", + "default": '', + "description": "Define types of files that the server accepts. Multiple types can be seperated by comma and the default is to accept everything." + }, + chunk_size: { + "name": "Chunk size", + "type": "integer", + "default": 1024 * 1024, + "description": "Max chunk size, gets set from server-side PHP (max_upload_size-1M)/2" // last chunk can be up to 2*chunk_size! + } + }; + return et2_file; +}(et2_core_inputWidget_1.et2_inputWidget)); +exports.et2_file = et2_file; +et2_core_widget_1.et2_register_widget(et2_file, ["file"]); +//# sourceMappingURL=et2_widget_file.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_file.ts b/api/js/etemplate/et2_widget_file.ts new file mode 100644 index 0000000000..29bd61fe31 --- /dev/null +++ b/api/js/etemplate/et2_widget_file.ts @@ -0,0 +1,740 @@ +/** + * EGroupware eTemplate2 - JS Number object + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage api + * @link http://www.egroupware.org + * @author Nathan Gray + * @copyright Nathan Gray 2011 + * @version $Id$ + */ + +/*egw:uses + et2_core_inputWidget; + phpgwapi.Resumable.resumable; +*/ + +import {et2_inputWidget} from "./et2_core_inputWidget"; +import {et2_register_widget, WidgetConfig} from "./et2_core_widget"; +import {ClassWithAttributes} from "./et2_core_inheritance"; + +/** + * Class which implements file upload + * + * @augments et2_inputWidget + */ +export class et2_file extends et2_inputWidget +{ + static readonly _attributes : any = { + "multiple": { + "name": "Multiple files", + "type": "boolean", + "default": false, + "description": "Allow the user to select more than one file to upload at a time. Subject to browser support." + }, + "max_file_size": { + "name": "Maximum file size", + "type": "integer", + "default":0, + "description": "Largest file accepted, in bytes. Subject to server limitations. 8MB = 8388608" + }, + "mime": { + "name": "Allowed file types", + "type": "string", + "default": et2_no_init, + "description": "Mime type (eg: image/png) or regex (eg: /^text\//i) for allowed file types" + }, + "blur": { + "name": "Placeholder", + "type": "string", + "default": "", + "description": "This text get displayed if an input-field is empty and does not have the input-focus (blur). It can be used to show a default value or a kind of help-text." + }, + "progress": { + "name": "Progress node", + "type": "string", + "default": et2_no_init, + "description": "The ID of an alternate node (div) to display progress and results. The Node is fetched with et2 getWidgetById so you MUST use the id assigned in XET-File (it may not be available at creation time, so we (re)check on createStatus time)" + }, + "onStart": { + "name": "Start event handler", + "type": "any", + "default": et2_no_init, + "description": "A (js) function called when an upload starts. Return true to continue with upload, false to cancel." + }, + "onFinish": { + "name": "Finish event handler", + "type": "any", + "default": et2_no_init, + "description": "A (js) function called when all files to be uploaded are finished." + }, + drop_target: { + "name": "Optional, additional drop target for HTML5 uploads", + "type": "string", + "default": et2_no_init, + "description": "The ID of an additional drop target for HTML5 drag-n-drop file uploads" + }, + label: { + "name": "Label of file upload", + "type": "string", + "default": "Choose file...", + "description": "String caption to be displayed on file upload span" + }, + progress_dropdownlist: { + "name": "List on files in progress like dropdown", + "type": "boolean", + "default": false, + "description": "Style list of files in uploading progress like dropdown list with a total upload progress indicator" + }, + onFinishOne: { + "name": "Finish event handler for each one", + "type": "any", + "default": et2_no_init, + "description": "A (js) function called when a file to be uploaded is finished." + }, + accept: { + "name": "Acceptable extensions", + "type": "string", + "default": '', + "description": "Define types of files that the server accepts. Multiple types can be seperated by comma and the default is to accept everything." + }, + chunk_size: { + "name": "Chunk size", + "type": "integer", + "default": 1024*1024, + "description": "Max chunk size, gets set from server-side PHP (max_upload_size-1M)/2" // last chunk can be up to 2*chunk_size! + } + }; + + asyncOptions : any = {}; + input : JQuery = null; + progress : JQuery = null; + span : JQuery = null; + disabled_buttons : JQuery; + resumable : any; + + /** + * Constructor + * + * @memberOf et2_file + */ + constructor(_parent, _attrs? : WidgetConfig, _child? : object) + { + // Call the inherited constructor + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_file._attributes, _child || {})); + + this.node = null; + this.input = null; + this.progress = null; + this.span = null; + // Contains all submit buttons need to be disabled during upload process + this.disabled_buttons = jQuery("input[type='submit'], button"); + if(!this.options.value) this.options.value = {}; + + if(!this.options.id) { + console.warn("File widget needs an ID. Used 'file_widget'."); + this.options.id = "file_widget"; + } + + // Legacy - id ending in [] means multiple + if(this.options.id.substr(-2) == "[]") + { + this.options.multiple = true; + } + // If ID ends in /, it's a directory - allow multiple + else if (this.options.id.substr(-1) === "/") + { + this.options.multiple = true; + _attrs.multiple = true; + } + + // Set up the URL to have the request ID & the widget ID + var instance = this.getInstanceManager(); + + let self = this; + + this.asyncOptions = jQuery.extend({ + // Callbacks + onStart: function(event, file_count) { + return self.onStart(event, file_count); + }, + onFinish: function(event, file_count) { + self.onFinish.apply(self, [event, file_count]) + }, + onStartOne: function(event, file_name, index, file_count) { + + }, + onFinishOne: function(event, response, name, number, total) { return self.finishUpload(event,response,name,number,total);}, + onProgress: function(event, progress, name, number, total) { return self.onProgress(event,progress,name,number,total);}, + onError: function(event, name, error) { return self.onError(event,name,error);}, + beforeSend: function(form) { return self.beforeSend(form);}, + + chunkSize: this.options.chunk_size || 1024*1024, + + target: egw.ajaxUrl("EGroupware\\Api\\Etemplate\\Widget\\File::ajax_upload"), + query: function(file) {return self.beforeSend(file);}, + // Disable checking for already uploaded chunks + testChunks: false + },this.asyncOptions); + this.asyncOptions.fieldName = this.options.id; + this.createInputWidget(); + this.set_readonly(this.options.readonly); + } + + destroy() + { + super.destroy(); + this.set_drop_target(null); + this.node = null; + this.input = null; + this.span = null; + this.progress = null; + } + + createInputWidget() + { + this.node = jQuery(document.createElement("div")).addClass("et2_file"); + this.span = jQuery(document.createElement("span")) + .addClass('et2_file_span et2_button') + .appendTo (this.node); + if (this.options.label != '') this.span.addClass('et2_button_text'); + let span = this.span; + this.input = jQuery(document.createElement("input")) + .attr("type", "file").attr("placeholder", this.options.blur) + .addClass ("et2_file_upload") + .appendTo(this.node) + .hover(function() { + jQuery(span) + .toggleClass('et2_file_spanHover'); + }) + .on({ + mousedown:function (){ + jQuery(span).addClass('et2_file_spanActive'); + }, + mouseup:function (){ + jQuery(span).removeClass('et2_file_spanActive'); + } + }); + if (this.options.accept) this.input.attr('accept', this.options.accept); + let self = this; + // trigger native input upload file + if (!this.options.readonly) this.span.click(function(){self.input.click()}); + // Check for File interface, should fall back to normal form submit if missing + if(typeof File != "undefined" && typeof (new XMLHttpRequest()).upload != "undefined") + { + this.resumable = new Resumable(this.asyncOptions); + this.resumable.assignBrowse(this.input); + this.resumable.on('fileAdded', jQuery.proxy(this._fileAdded, this)); + this.resumable.on('fileProgress', jQuery.proxy(this._fileProgress, this)); + this.resumable.on('fileSuccess', jQuery.proxy(this.finishUpload, this)); + this.resumable.on('complete', jQuery.proxy(this.onFinish, this)); + } + else + { + // This may be a problem submitting via ajax + } + if(this.options.progress) + { + let widget = this.getRoot().getWidgetById(this.options.progress); + if(widget) + { + //may be not available at createInputWidget time + this.progress = jQuery(widget.getDOMNode()); + } + } + if(!this.progress) + { + this.progress = jQuery(document.createElement("div")).appendTo(this.node); + } + this.progress.addClass("progress"); + + if(this.options.multiple) + { + this.input.attr("multiple","multiple"); + } + + this.setDOMNode(this.node[0]); + } + + /** + * Set a widget or DOM node as a HTML5 file drop target + * + * @param {string} new_target widget ID or DOM node ID to be used as a new target + */ + set_drop_target(new_target : string) + { + // Cancel old drop target + if(this.options.drop_target) + { + let widget = this.getRoot().getWidgetById(this.options.drop_target); + let drop_target = widget && widget.getDOMNode() || document.getElementById(this.options.drop_target); + if(drop_target) + { + this.resumable.unAssignDrop(drop_target); + } + } + + this.options.drop_target = new_target; + + if(!this.options.drop_target) return; + + // Set up new drop target + let widget = this.getRoot().getWidgetById(this.options.drop_target); + let drop_target = widget && widget.getDOMNode() || document.getElementById(this.options.drop_target); + if(drop_target) + { + this.resumable.assignDrop([drop_target]); + } + else + { + this.egw().debug("warn", "Did not find file drop target %s", this.options.drop_target); + } + + } + + attachToDOM() + { + let res = super.attachToDOM(); + // Override parent's change, file widget will fire change when finished uploading + this.input.unbind("change.et2_inputWidget"); + return res; + } + + getValue() + { + return this.options.value ? this.options.value : this.input.val(); + } + + /** + * Set the value of the file widget. + * + * If you pass a FileList or list of files, it will trigger the async upload + * + * @param {FileList|File[]|false} value List of files to be uploaded, or false to reset. + * @param {Event} event Most browsers require the user to initiate file transfers in some way. + * Pass the event in, if you have it. + */ + set_value(value, event?) : boolean + { + if (!value || typeof value == "undefined") { + value = {}; + } + if (jQuery.isEmptyObject(value)) { + this.options.value = {}; + if (this.resumable.progress() == 1) this.progress.empty(); + + // Reset the HTML element + this.input.wrap('').closest('form').get(0).reset(); + this.input.unwrap(); + + return; + } + + let addFile = jQuery.proxy(function (i, file) { + this.resumable.addFile(file, event); + }, this); + if (typeof value == 'object' && value.length && typeof value[0] == 'object' && value[0].name) { + try { + this.input[0].files = value; + + jQuery.each(value, addFile); + } catch (e) { + var self = this; + var args = arguments; + jQuery.each(value, addFile); + } + } + } + + /** + * Set the value for label + * The label is used as caption for span tag which customize the HTML file upload styling + * + * @param {string} value text value of label + */ + set_label(value) + { + if (this.span != null && value != null) + { + this.span.text(value); + } + } + + getInputNode() + { + if (typeof this.input == 'undefined') return false; + return this.input[0]; + } + + + set_mime(mime) + { + if(!mime) + { + this.options.mime = null; + } + if(mime.indexOf("/") != 0) + { + // Lower case it now, if it's not a regex + this.options.mime = mime.toLowerCase(); + } + else + { + // Convert into a js regex + var parts = mime.substr(1).match(/(.*)\/([igm]?)$/); + this.options.mime = new RegExp(parts[1],parts.length > 2 ? parts[2] : ""); + } + } + + set_multiple(_multiple) + { + this.options.multiple = _multiple; + if(_multiple) + { + return this.input.attr("multiple", "multiple"); + } + return this.input.removeAttr("multiple"); + } + + /** + * Check to see if the provided file's mimetype matches + * + * @param f File object + * @return boolean + */ + checkMime(f) + { + // If missing, let the server handle it + if(!this.options.mime || !f.type) return true; + + var is_preg = (typeof this.options.mime == "object"); + if(!is_preg && f.type.toLowerCase() == this.options.mime || is_preg && this.options.mime.test(f.type)) + { + return true; + } + + // Not right mime + return false; + } + + private _fileAdded(file,event) + { + // Manual additions have no event + if(typeof event == 'undefined') + { + event = {}; + } + // Trigger start of uploading, calls callback + if(!this.resumable.isUploading()) + { + if (!(this.onStart(event,this.resumable.files.length))) return; + } + + // Here 'this' is the input + if(this.checkMime(file.file)) + { + if(this.createStatus(event,file)) + { + + // Disable buttons + this.disabled_buttons + .not("[disabled]") + .attr("disabled", true) + .addClass('et2_button_ro') + .removeClass('et2_clickable') + .css('cursor', 'default'); + + // Actually start uploading + this.resumable.upload(); + } + } + else + { + // Wrong mime type - show in the list of files + return this.createStatus( + this.egw().lang("File is of wrong type (%1 != %2)!", file.file.type, this.options.mime), + file + ); + } + + } + + /** + * Add in the request id + */ + beforeSend(form) { + var instance = this.getInstanceManager(); + + return { + request_id: instance.etemplate_exec_id, + widget_id: this.id + }; + } + + /** + * Disables submit buttons while uploading + */ + onStart(event, file_count) { + // Hide any previous errors + this.hideMessage(); + + + event.data = this; + + //Add dropdown_progress + if (this.options.progress_dropdownlist) + { + this._build_progressDropDownList(); + } + + // Callback + if(this.options.onStart) return et2_call(this.options.onStart, event, file_count); + return true; + } + + /** + * Re-enables submit buttons when done + */ + onFinish() { + this.disabled_buttons.attr("disabled", 0).css('cursor','pointer').removeClass('et2_button_ro'); + + var file_count = this.resumable.files.length; + + // Remove files from list + while(this.resumable.files.length > 0) + { + this.resumable.removeFile(this.resumable.files[this.resumable.files.length -1]); + } + + var event = jQuery.Event('upload'); + + event.data = this; + + var result = false; + + //Remove progress_dropDown_fileList class and unbind the click handler from body + if (this.options.progress_dropdownlist) + { + this.progress.removeClass("progress_dropDown_fileList"); + jQuery(this.node).find('span').removeClass('totalProgress_loader'); + jQuery('body').off('click'); + } + + if(this.options.onFinish && !jQuery.isEmptyObject(this.getValue())) + { + result = et2_call(this.options.onFinish, event, file_count); + } + else + { + result = (file_count == 0 || !jQuery.isEmptyObject(this.getValue())); + } + if(result) + { + // Fire legacy change action when done + this.change(this.input); + } + } + + /** + * Build up dropdown progress with total count indicator + * + * @todo Implement totalProgress bar instead of ajax-loader, in order to show how much percent of uploading is completed + */ + private _build_progressDropDownList() + { + this.progress.addClass("progress_dropDown_fileList"); + + //Add uploading indicator and bind hover handler on it + jQuery(this.node).find('span').addClass('totalProgress_loader'); + + jQuery(this.node).find('span.et2_file_span').hover(function(){ + jQuery('.progress_dropDown_fileList').show(); + }); + //Bind click handler to dismiss the dropdown while uploading + jQuery('body').on('click', function(event){ + if (event.target.className != 'remove') + { + jQuery('.progress_dropDown_fileList').hide(); + } + }); + + } + + /** + * Creates the elements used for displaying the file, and it's upload status, and + * attaches them to the DOM + * + * @param _event Either the event, or an error message + */ + createStatus(_event, file) + { + var error = (typeof _event == "object" ? "" : _event); + + if(this.options.max_file_size && file.size > this.options.max_file_size) { + error = this.egw().lang("File too large. Maximum %1", et2_vfsSize.prototype.human_size(this.options.max_file_size)); + } + + if(this.options.progress) + { + var widget = this.getRoot().getWidgetById(this.options.progress); + if(widget) + { + this.progress = jQuery(widget.getDOMNode()); + this.progress.addClass("progress"); + } + } + if(this.progress) + { + var fileName = file.fileName || 'file'; + var status = jQuery("
        • "+fileName + +"

        • ") + .appendTo(this.progress); + jQuery("div.remove",status).on('click', file, jQuery.proxy(this.cancel,this)); + if(error != "") + { + status.addClass("message ui-state-error"); + status.append("
          "+error+""); + jQuery(".progressBar",status).css("display", "none"); + } + } + return error == ""; + } + + private _fileProgress(file) + { + if(this.progress) + { + jQuery("li[data-file='"+file.fileName.replace(/'/g, '"')+"'] > span.progressBar > p").css("width", Math.ceil(file.progress()*100)+"%"); + + } + return true; + } + + onError(event, name, error) + { + console.warn(event,name,error); + } + + /** + * A file upload is finished, update the UI + */ + finishUpload(file, response) + { + var name = file.fileName || 'file'; + + if(typeof response == 'string') response = jQuery.parseJSON(response); + if(response.response[0] && typeof response.response[0].data.length == 'undefined') { + if(typeof this.options.value !== 'object' || !this.options.multiple) + { + this.set_value({}); + } + for(var key in response.response[0].data) { + if(typeof response.response[0].data[key] == "string") + { + // Message from server - probably error + jQuery("[data-file='"+name.replace(/'/g, '"')+"']",this.progress) + .addClass("error") + .css("display", "block") + .text(response.response[0].data[key]); + } + else + { + this.options.value[key] = response.response[0].data[key]; + // If not multiple, we already destroyed the status, so re-create it + if(!this.options.multiple) + { + this.createStatus({}, file); + } + if(this.progress) + { + jQuery("[data-file='"+name.replace(/'/g, '"')+"']",this.progress).addClass("message success"); + } + } + } + } + else if (this.progress) + { + jQuery("[data-file='"+name.replace(/'/g, '"')+"']",this.progress) + .addClass("ui-state-error") + .css("display", "block") + .text(this.egw().lang("Server error")); + } + var event = jQuery.Event('upload'); + + event.data = this; + + // Callback + if(this.options.onFinishOne) + { + return et2_call(this.options.onFinishOne,event,response,name); + } + return true; + } + + /** + * Remove a file from the list of values + * + * @param {File|string} File object, or file name, to remove + */ + remove_file(file) + { + //console.info(filename); + if(typeof file == 'string') + { + file = {fileName: file}; + } + for(var key in this.options.value) + { + if(this.options.value[key].name == file.fileName) + { + delete this.options.value[key]; + jQuery('[data-file="'+file.fileName.replace(/'/g, '"')+'"]',this.node).remove(); + return; + } + } + if(file.isComplete && !file.isComplete() && file.cancel) file.cancel(); + } + + /** + * Cancel a file - event callback + */ + cancel(e) + { + e.preventDefault(); + // Look for file name in list + var target = jQuery(e.target).parents("li"); + + this.remove_file(e.data); + + // In case it didn't make it to the list (error) + target.remove(); + jQuery(e.target).remove(); + } + + /** + * Set readonly + * + * @param {boolean} _ro boolean readonly state, true means readonly + */ + set_readonly(_ro) + { + if (typeof _ro != "undefined") + { + this.options.readonly = _ro; + this.span.toggleClass('et2_file_ro',_ro); + if (this.options.readonly) + { + this.span.unbind('click'); + } + else + { + var self = this; + this.span.off().bind('click',function(){self.input.click()}); + } + } + } +} +et2_register_widget(et2_file, ["file"]); + + diff --git a/api/js/etemplate/et2_widget_grid.js b/api/js/etemplate/et2_widget_grid.js index 4ba7ec27e7..d2018d8ea9 100644 --- a/api/js/etemplate/et2_widget_grid.js +++ b/api/js/etemplate/et2_widget_grid.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - JS Grid object * @@ -9,13 +10,31 @@ * @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 - /vendor/bower-asset/jquery/dist/jquery.js; - et2_core_DOMWidget; - et2_core_xml; + /vendor/bower-asset/jquery/dist/jquery.js; + et2_core_DOMWidget; + et2_core_xml; */ - +require("./et2_core_common"); +require("./et2_core_interfaces"); +var et2_core_widget_1 = require("./et2_core_widget"); +var et2_core_inheritance_1 = require("./et2_core_inheritance"); +var et2_core_DOMWidget_1 = require("./et2_core_DOMWidget"); +require("../egw_action/egw_action.js"); /** * Class which implements the "grid" XET-Tag * @@ -24,1114 +43,892 @@ * * @augments et2_DOMWidget */ -var et2_grid = (function(){ "use strict"; return et2_DOMWidget.extend([et2_IDetachedDOM, et2_IAligned, et2_IResizeable], -{ - createNamespace: true, - - attributes: { - // Better to use CSS, no need to warn about it - "border": { - "ignore": true - }, - "align": { - "name": "Align", - "type": "string", - "default": "left", - "description": "Position of this element in the parent hbox" - }, - "spacing": { - "ignore": true - }, - "padding": { - "ignore": true - }, - "sortable": { - "name": "Sortable callback", - "type": "string", - "default": et2_no_init, - "description": "PHP function called when user sorts the grid. Setting this enables sorting the grid rows. The callback will be passed the ID of the grid and the new order of the rows." - }, - sortable_containment: { - name: "Sortable bounding area", - type: "string", - default: "", - description: "Defines bounding area for sortable items" - }, - sortable_connectWith: { - name: "Sortable connectWith element", - type: "string", - default: "", - description: "Defines other sortable areas that should be connected to sort list" - }, - sortable_placeholder: { - name: "Sortable placeholder", - type: "string", - default: "", - description: "Defines sortable placeholder" - }, - sortable_cancel: { - name: "Sortable cancel class", - type: "string", - default: "", - description: "Defines sortable cancel which prevents sorting the matching element" - }, - sortable_recieveCallback: { - name: "Sortable receive callback", - type: "js", - default: et2_no_init, - description: "Defines sortable receive callback function" - }, - sortable_startCallback: { - name: "Sortable start callback", - type: "js", - default: et2_no_init, - description: "Defines sortable start callback function" - } - }, - - /** - * Constructor - * - * @memberOf et2_grid - */ - init: function() { - // Create the table body and the table - this.table = jQuery(document.createElement("table")) - .addClass("et2_grid"); - this.thead = jQuery(document.createElement("thead")) - .appendTo(this.table); - this.tfoot = jQuery(document.createElement("tfoot")) - .appendTo(this.table); - this.tbody = jQuery(document.createElement("tbody")) - .appendTo(this.table); - - // Call the parent constructor - this._super.apply(this, arguments); - - // Counters for rows and columns - this.rowCount = 0; - this.columnCount = 0; - - // 2D-Array which holds references to the DOM td tags - this.cells = []; - this.rowData = []; - this.colData = []; - this.managementArray = []; - - // Keep the template node for later regeneration - this.template_node = null; - - // Wrapper div for height & overflow, if needed - this.wrapper = null; - }, - - destroy: function() { - this._super.call(this, arguments); - }, - - _initCells: function(_colData, _rowData) { - // Copy the width and height - var w = _colData.length; - var h = _rowData.length; - - // Create the 2D-Cells array - var cells = new Array(h); - for (var y = 0; y < h; y++) - { - cells[y] = new Array(w); - - // Initialize the cell description objects - for (var x = 0; x < w; x++) - { - // Some columns (nm) we do not parse into a boolean - var col_disabled = _colData[x].disabled; - cells[y][x] = { - "td": null, - "widget": null, - "colData": _colData[x], - "rowData": _rowData[y], - "disabled": col_disabled || _rowData[y].disabled, - "class": _colData[x]["class"], - "colSpan": 1, - "autoColSpan": false, - "rowSpan": 1, - "autoRowSpan": false, - "width": _colData[x].width, - "x": x, - "y": y - }; - } - } - - return cells; - }, - - _getColDataEntry: function() { - return { - "width": "auto", - "class": "", - "align": "", - "span": "1", - "disabled": false - }; - }, - - _getRowDataEntry: function() { - return { - "height": "auto", - "class": "", - "valign": "top", - "span": "1", - "disabled": false - }; - }, - - _getCell: function(_cells, _x, _y) { - if ((0 <= _y) && (_y < _cells.length)) - { - var row = _cells[_y]; - if ((0 <= _x) && (_x < row.length)) - { - return row[_x]; - } - } - - throw("Error while accessing grid cells, invalid element count or span value!"); - }, - - _forceNumber: function(_val) { - if (isNaN(_val)) - { - throw(_val + " is not a number!"); - } - - return parseInt(_val); - }, - - _fetchRowColData: function(columns, rows, colData, rowData) { - // Some things cannot be done inside a nextmatch - nm will do the expansion later - var nm = false; - var widget = this; - while(!nm && widget != this.getRoot()) - { - nm = (widget._type == 'nextmatch'); - widget = widget.getParent(); - } - - // Parse the columns tag - et2_filteredNodeIterator(columns, function(node, nodeName) { - var colDataEntry = this._getColDataEntry(); - // This cannot be done inside a nm, it will expand it later - colDataEntry["disabled"] = nm ? - et2_readAttrWithDefault(node, "disabled", "") : - this.getArrayMgr("content") - .parseBoolExpression(et2_readAttrWithDefault(node, "disabled", "")); - if (nodeName == "column") - { - colDataEntry["width"] = et2_readAttrWithDefault(node, "width", "auto"); - colDataEntry["class"] = et2_readAttrWithDefault(node, "class", ""); - colDataEntry["align"] = et2_readAttrWithDefault(node, "align", ""); - colDataEntry["span"] = et2_readAttrWithDefault(node, "span", "1"); - - // Keep any others attributes set, there's no 'column' widget - for(var i in node.attributes) - { - var attr = node.attributes[i]; - if(attr.nodeType == 2 && typeof colDataEntry[attr.nodeName] == 'undefined') - { - colDataEntry[attr.nodeName] = attr.value; - } - } - } - else - { - colDataEntry["span"] = "all"; - } - colData.push(colDataEntry); - }, this); - - // Parse the rows tag - et2_filteredNodeIterator(rows, function(node, nodeName) { - var rowDataEntry = this._getRowDataEntry(); - rowDataEntry["disabled"] = this.getArrayMgr("content") - .parseBoolExpression(et2_readAttrWithDefault(node, "disabled", "")); - if (nodeName == "row") - { - // Remember this row for auto-repeat - it'll eventually be the last one - this.lastRowNode = node; - - rowDataEntry["height"] = et2_readAttrWithDefault(node, "height", "auto"); - rowDataEntry["class"] = et2_readAttrWithDefault(node, "class", ""); - rowDataEntry["valign"] = et2_readAttrWithDefault(node, "valign", ""); - rowDataEntry["span"] = et2_readAttrWithDefault(node, "span", "1"); - rowDataEntry["part"] = et2_readAttrWithDefault(node, "part", "body"); - - var id = et2_readAttrWithDefault(node, "id", ""); - if(id) - { - rowDataEntry["id"] = id; - } - } - else - { - rowDataEntry["span"] = "all"; - } - rowData.push(rowDataEntry); - }, this); - - // Add in repeated rows - // TODO: It would be nice if we could skip header (thead) & footer (tfoot) or treat them separately - var rowIndex = Infinity; - if(this.getArrayMgr("content")) - { - var content = this.getArrayMgr("content"); - var rowDataEntry = rowData[rowData.length-1]; - rowIndex = rowData.length-1; - // Find out if we have any content rows, and how many - var cont = true; - while(cont) - { - if(content.data[rowIndex]) - { - rowData[rowIndex] = jQuery.extend({}, rowDataEntry); - - rowIndex++; - } - - else if (this.lastRowNode != null) - { - // Have to look through actual widgets to support const[$row] - // style names - should be avoided so we can remove this extra check - // Old etemplate checked first two widgets, or first two box children - // This cannot be done inside a nextmatch - nm will do the expansion later - var nm = false; - if(nm) - { - // No further checks for repeated rows - break; - } - - // Not in a nextmatch, so we can expand with abandon - var currentPerspective = jQuery.extend({},content.perspectiveData); - var check = function(node, nodeName) - { - if(nodeName == 'box' || nodeName == 'hbox' || nodeName == 'vbox') - { - return et2_filteredNodeIterator(node, check, this); - } - content.perspectiveData.row = rowIndex; - for(var attr in node.attributes) - { - var value = et2_readAttrWithDefault(node, node.attributes[attr].name, ""); - // Don't include first char, those should be handled by normal means - // and it would break nextmatch - if(value.indexOf('@') > 0 || value.indexOf('$') > 0) - { - // Ok, we found something. How many? Check for values. - var ident = content.expandName(value); - // expandName() handles index into content (@), but we have to look up - // regular values - if(value[0] != '@') - { - // Returns null if there isn't an actual value - ident = content.getEntry(ident,false,true); - } - while(ident != null && rowIndex < 1000) - { - rowData[rowIndex] = jQuery.extend({}, rowDataEntry); - content.perspectiveData.row = ++rowIndex; - ident = content.expandName(value); - if(value[0] != '@') - { - // Returns null if there isn't an actual value - ident = content.getEntry(ident,false,true); - } - } - if(rowIndex >= 1000) - { - egw.debug("error", "Problem in autorepeat fallback: too many rows for '%s'. Use a nextmatch, or start debugging.",value); - } - return; - } - } - }; - et2_filteredNodeIterator(this.lastRowNode, check,this); - cont = false; - content.perspectiveData = currentPerspective; - } - - else - { - // No more rows, stop - break; - } - } - } - if(rowIndex <= rowData.length - 1) - { - // No auto-repeat - this.lastRowNode = null; - } - }, - - _fillCells: function(cells, columns, rows) { - var h = cells.length; - var w = (h > 0) ? cells[0].length : 0; - var currentPerspective = jQuery.extend({},this.getArrayMgr("content").perspectiveData); - - // Read the elements inside the columns - var x = 0; - - et2_filteredNodeIterator(columns, function(node, nodeName) { - - function _readColNode(node, nodeName) { - if (y >= h) - { - this.egw().debug("warn", "Skipped grid cell in column, '" + - nodeName + "'"); - return; - } - - var cell = this._getCell(cells, x, y); - - // Read the span value of the element - if (node.getAttribute("span")) - { - cell.rowSpan = node.getAttribute("span"); - } - else - { - cell.rowSpan = cell.colData["span"]; - cell.autoRowSpan = true; - } - - if (cell.rowSpan == "all") - { - cell.rowSpan = cells.length; - } - - var span = cell.rowSpan = this._forceNumber(cell.rowSpan); - - // Create the widget - var widget = this.createElementFromNode(node, nodeName); - - // Fill all cells the widget is spanning - for (var i = 0; i < span && y < cells.length; i++, y++) - { - this._getCell(cells, x, y).widget = widget; - } - }; - - // If the node is a column, create the widgets which belong into - // the column - var y = 0; - if (nodeName == "column") - { - et2_filteredNodeIterator(node, _readColNode, this); - } - else - { - _readColNode.call(this, node, nodeName); - } - - x++; - }, this); - - // Read the elements inside the rows - var y = 0; - var x = 0; - var readRowNode; - var nm = false; - var widget = this; - while(!nm && widget != this.getRoot()) - { - nm = (widget._type == 'nextmatch'); - widget = widget.getParent(); - } - et2_filteredNodeIterator(rows, function(node, nodeName) { - - readRowNode = function _readRowNode(node, nodeName) { - if (x >= w) - { - if(nodeName != "description") - { - // Only notify it skipping other than description, - // description used to pad - this.egw().debug("warn", "Skipped grid cell in row, '" + - nodeName + "'"); - } - return; - } - - var cell = this._getCell(cells, x, y); - - // Read the span value of the element - if (node.getAttribute("span")) - { - cell.colSpan = node.getAttribute("span"); - } - else - { - cell.colSpan = cell.rowData["span"]; - cell.autoColSpan = true; - } - - if (cell.colSpan == "all") - { - cell.colSpan = cells[y].length; - } - - var span = cell.colSpan = this._forceNumber(cell.colSpan); - - // Read the align value of the element - if (node.getAttribute("align")) - { - cell.align = node.getAttribute("align"); - } - - // store id of nextmatch-*headers, so it is available for disabled widgets, which get not instanciated - if (nodeName.substr(0, 10) == 'nextmatch-') - { - cell.nm_id = node.getAttribute('id'); - } - // Apply widget's class to td, for backward compatability - if(node.getAttribute("class")) - { - cell.class += (cell.class ? " " : "") + node.getAttribute("class"); - } - - // Create the element - if(!cell.disabled || cell.disabled && typeof cell.disabled === 'string') - { - //Skip if it is a nextmatch while the nextmatch handles row adjustment by itself - if(!nm) - { - // Adjust for the row - var mgrs = this.getArrayMgrs(); - for(var name in mgrs) - { - this.getArrayMgr(name).perspectiveData.row = y; - } - if(this._getCell(cells, x, y).rowData.id) - { - this._getCell(cells, x, y).rowData.id = this.getArrayMgr("content").expandName(this._getCell(cells, x, y).rowData.id); - } - if(this._getCell(cells, x, y).rowData.class) - { - this._getCell(cells, x, y).rowData.class = this.getArrayMgr("content").expandName(this._getCell(cells, x, y).rowData.class); - } - - } - if(!nm && typeof cell.disabled === 'string') - { - cell.disabled = this.getArrayMgr("content").parseBoolExpression(cell.disabled); - } - if(nm || !cell.disabled) - { - var widget = this.createElementFromNode(node, nodeName); - } - } - - // Fill all cells the widget is spanning - for (var i = 0; i < span && x < cells[y].length; i++, x++) - { - cell = this._getCell(cells, x, y); - if (cell.widget == null) - { - cell.widget = widget; - } - else - { - throw("Grid cell collision, two elements " + - "defined for cell (" + x + "," + y + ")!"); - } - } - }; - - // If the node is a row, create the widgets which belong into - // the row - x = 0; - if(this.lastRowNode && node == this.lastRowNode) - { - return; - } - if (nodeName == "row") - { - // Adjust for the row - for(var name in this.getArrayMgrs()) - { - //this.getArrayMgr(name).perspectiveData.row = y; - } - - if(this._getCell(cells, x,y).rowData.id) - { - this.getArrayMgr("content").expandName(this.rowData[y].id); - } - // If row disabled, just skip it - var disabled = false; - if(node.getAttribute("disabled") == "1") - { - disabled = true; - } - if(!disabled) - { - et2_filteredNodeIterator(node, readRowNode, this); - } - } - else - { - readRowNode.call(this, node, nodeName); - } - - y++; - }, this); - - // Extra content rows - for(y; y < h; y++) { - var x = 0; - - et2_filteredNodeIterator(this.lastRowNode, readRowNode, this); - } - // Reset - for(var name in this.getArrayMgrs()) - { - this.getArrayMgr(name).perspectiveData = currentPerspective; - } - }, - - _expandLastCells: function(_cells) { - var h = _cells.length; - var w = (h > 0) ? _cells[0].length : 0; - - // Determine the last cell in each row and expand its span value if - // the span has not been explicitly set. - for (var y = 0; y < h; y++) - { - for (var x = w - 1; x >= 0; x--) - { - var cell = _cells[y][x]; - - if (cell.widget != null) - { - if (cell.autoColSpan) - { - cell.colSpan = w - x; - } - break; - } - } - } - - // Determine the last cell in each column and expand its span value if - // the span has not been explicitly set. - for (var x = 0; x < w; x++) - { - for (var y = h - 1; y >= 0; y--) - { - var cell = _cells[y][x]; - - if (cell.widget != null) - { - if (cell.autoRowSpan) - { - cell.rowSpan = h - y; - } - break; - } - } - } - }, - - /** - * As the does not fit very well into the default widget structure, we're - * overwriting the loadFromXML function and doing a two-pass reading - - * in the first step the - * - * @param {object} _node xml node to process - */ - loadFromXML: function(_node) { - // Keep the node for later changing / reloading - this.template_node = _node; - - // Get the columns and rows tag - var rowsElems = et2_directChildrenByTagName(_node, "rows"); - var columnsElems = et2_directChildrenByTagName(_node, "columns"); - - if (rowsElems.length == 1 && columnsElems.length == 1) - { - var columns = columnsElems[0]; - var rows = rowsElems[0]; - var colData = []; - var rowData = []; - - // Fetch the column and row data - this._fetchRowColData(columns, rows, colData, rowData); - - // Initialize the cells - var cells = this._initCells(colData, rowData); - - // Create the widgets inside the cells and read the span values - this._fillCells(cells, columns, rows); - - // Expand the span values of the last cells - this._expandLastCells(cells); - - // Create the table rows - this.createTableFromCells(cells, colData, rowData); - } - else - { - throw("Error while parsing grid, none or multiple rows or columns tags!"); - } - }, - - createTableFromCells: function(_cells, _colData, _rowData) { - this.managementArray = []; - this.cells = _cells; - this.colData = _colData; - this.rowData = _rowData; - - // Set the rowCount and columnCount variables - var h = this.rowCount = _cells.length; - var w = this.columnCount = (h > 0) ? _cells[0].length : 0; - - // Create the table rows. - for (var y = 0; y < h; y++) - { - var row = _cells[y]; - var parent = this.tbody; - switch(this.rowData[y]["part"]) - { - case 'header': - if (!this.tbody.children().length && !this.tfoot.children().length) - { - parent = this.thead; - } - break; - case 'footer': - if (!this.tbody.children().length) - { - parent = this.tfoot; - } - break; - } - var tr = jQuery(document.createElement("tr")).appendTo(parent) - .addClass(this.rowData[y]["class"]); - - if (this.rowData[y].disabled) - { - tr.hide(); - } - - if (this.rowData[y].height != "auto") - { - tr.height(this.rowData[y].height); - } - - if (this.rowData[y].valign) - { - tr.attr("valign", this.rowData[y].valign); - } - - if(this.rowData[y].id) - { - tr.attr("id", this.rowData[y].id); - } - // Create the cells. x is incremented by the colSpan value of the - // cell. - for (var x = 0; x < w;) - { - // Fetch a cell from the cells - var cell = this._getCell(_cells, x, y); - - if (cell.td == null && cell.widget != null) - { - // Create the cell - var td = jQuery(document.createElement("td")).appendTo(tr) - .addClass(cell["class"]); - - if (cell.disabled) - { - td.hide(); - cell.widget.options.disabled = cell.disabled; - } - - if (cell.width != "auto") - { - td.width(cell.width); - } - - if (cell.align) - { - td.attr("align",cell.align); - } - - // Add the entry for the widget to the management array - this.managementArray.push({ - "cell": td[0], - "widget": cell.widget, - "disabled": cell.disabled - }); - - // Set the span values of the cell - var cs = (x == w - 1) ? w - x : Math.min(w - x, cell.colSpan); - var rs = (y == h - 1) ? h - y : Math.min(h - y, cell.rowSpan); - - // Set the col and row span values - if (cs > 1) { - td.attr("colspan", cs); - } - - if (rs > 1) { - td.attr("rowspan", rs); - } - - // Assign the td to the cell - for (var sx = x; sx < x + cs; sx++) - { - for (var sy = y; sy < y + rs; sy++) - { - this._getCell(_cells, sx, sy).td = td; - } - } - - x += cell.colSpan; - } - else - { - x++; - } - } - } - }, - - getDOMNode: function(_sender) { - // If the parent class functions are asking for the DOM-Node, return the - // outer table. - if (_sender == this || typeof _sender == 'undefined') - { - return this.wrapper != null ? this.wrapper[0] : this.table[0]; - } - - // Check whether the _sender object exists inside the management array - for (var i = 0; i < this.managementArray.length; i++) - { - if (this.managementArray[i].widget == _sender) - { - return this.managementArray[i].cell; - } - } - - return null; - }, - - isInTree: function(_sender) { - var vis = true; - - if (typeof _sender != "undefined" && _sender != this) - { - vis = false; - - // Check whether the _sender object exists inside the management array - for (var i = 0; i < this.managementArray.length; i++) - { - if (this.managementArray[i].widget == _sender) - { - vis = !(typeof this.managementArray[i].disabled === 'boolean' ? - this.managementArray[i].disabled : - this.getArrayMgr("content").parseBoolExpression(this.managementArray[i].disabled) - ); - break; - } - } - } - - return this._super(this, vis); - }, - - /** - * Set the overflow attribute - * - * Grid needs special handling because HTML tables don't do overflow. We - * create a wrapper DIV to handle it. - * No value or default visible needs no wrapper, as table is always overflow visible. - * - * @param {string} _value Overflow value, must be a valid CSS overflow value, default 'visible' - */ - set_overflow: function(_value) { - var wrapper = this.wrapper || this.table.parent('[id$="_grid_wrapper"]'); - - this.overflow = _value; - - if(wrapper.length == 0 && _value && _value !== 'visible') - { - this.wrapper = wrapper = this.table.wrap('
          ').parent(); - if(this.height) - { - wrapper.css('height', this.height); - } - } - wrapper.css('overflow', _value); - - if(wrapper.length && (!_value || _value == null || _value === 'visible')) - { - this.table.unwrap(); - } - }, - - set_align: function(_value) { - this.align = _value; - }, - - get_align: function(_value) { - return this.align; - }, - - /** - * Change the content for the grid, and re-generate its contents. - * - * Changing the content does not allow changing the structure of the grid, - * as that is loaded from the template file. The rows and widgets inside - * will be re-created (including auto-repeat). - * - * @param {Object} _value New data for the grid - * @param {Object} [_value.content] New content - * @param {Object} [_value.sel_options] New select options - * @param {Object} [_value.readonlys] New read-only values - */ - set_value: function(_value) { - - // Destroy children, empty grid - for(var i = 0; i < this.managementArray.length; i++) - { - var cell = this.managementArray[i]; - if(cell.widget) - { - cell.widget.destroy(); - } - } - this.managementArray = []; - this.thead.empty(); - this.tfoot.empty(); - this.tbody.empty(); - - // Update array managers - for(var key in _value) - { - this.getArrayMgr(key).data = _value[key]; - } - - // Rebuild grid - this.loadFromXML(this.template_node); - - // New widgets need to finish - this.loadingFinished(); - }, - - /** - * Sortable allows you to reorder grid rows using the mouse. - * The new order is returned as part of the value of the - * grid, in 'sort_order'. - * - * @param {boolean|function} sortable Callback or false to disable - */ - set_sortable: function(sortable) { - if(!sortable) - { - this.tbody.sortable("destroy"); - return; - } - - // Make sure rows have IDs, so sortable has something to return - jQuery('tr', this.tbody).each(function(index) { - var $this = jQuery(this); - - // Header does not participate in sorting - if($this.hasClass('th')) return; - - // If row doesn't have an ID, assign the index as ID - if(!$this.attr("id")) $this.attr("id", index); - }); - - var self = this; - - // Set up sortable - this.tbody.sortable({ - // Header does not participate in sorting - items: "tr:not(.th)", - distance: 15, - cancel: this.options.sortable_cancel, - placeholder: this.options.sortable_placeholder, - containment: this.options.sortable_containment, - connectWith: this.options.sortable_connectWith, - update: function(event, ui) { - self.egw(window).json(sortable,[self.tbody.sortable("toArray"), self.id], - null, - self, - true - ).sendRequest(); - }, - receive: function (event, ui) { - if (typeof self.options.sortable_recieveCallback == 'function') { - self.options.sortable_recieveCallback.call(self, event,ui); - } - }, - start: function (event, ui) { - if (typeof self.options.sortable_startCallback == 'function') { - self.options.sortable_startCallback.call(self, event,ui); - } - } - }); - }, - - /** - * Override parent to apply actions on each row - * - * @param {array} actions [ {ID: attributes..}+] as for set_actions - */ - _link_actions: function(actions) - { - // Get the top level element for the tree - var objectManager = egw_getAppObjectManager(true); - objectManager = objectManager.getObjectById(this.getInstanceManager().uniqueId,2) || objectManager; - 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 - )); - } - - // Delete all old objects - widget_object.clear(); - - // 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); - - // Deal with each row in tbody, ignore action-wise rows in thead or tfooter for now - for(var i = 0, r = 0; i < this.rowData.length; i++) - { - if (this.rowData[i].part != 'body') continue; - var content = this.getArrayMgr('content').getEntry(i); - if(content) - { - // Add a new action object to the object manager - var row = jQuery('tr', this.tbody)[r]; - var aoi = new et2_action_object_impl(this, row); - var obj = widget_object.addObject(content.id || "row_"+r, aoi); - - // Set the data to the content so it's available for the action - obj.data = content; - - obj.updateActionLinks(action_links); - } - r++; - } - }, - - /** - * Code for implementing et2_IDetachedDOM - * This doesn't need to be implemented. - * Individual widgets are detected and handled by the grid, but the interface is needed for this to happen - * - * @param {array} _attrs array to add further attributes to - */ - getDetachedAttributes: function(_attrs) { - }, - - getDetachedNodes: function() { - return [this.getDOMNode()]; - }, - - setDetachedAttributes: function(_nodes, _values) { - }, - - /** - * Generates nextmatch column name for headers in a grid - * - * Implemented here as default implementation in et2_externsion_nextmatch - * only considers children, but grid does NOT instanciate disabled rows as children. - * - * @return {string} - */ - _getColumnName: function() - { - var ids = []; - for(var r=0; r < this.cells.length; ++r) - { - var cols = this.cells[r]; - for(var c=0; c < cols.length; ++c) - { - if (cols[c].nm_id) ids.push(cols[c].nm_id); - } - } - return ids.join('_'); - }, - - resize: function (_height) - { - if (typeof this.options != 'undefined' && _height - && typeof this.options.resize_ratio != 'undefined' && this.options.resize_ratio) - { - // apply the ratio - _height = (this.options.resize_ratio != '')? _height * this.options.resize_ratio: _height; - if (_height != 0) - { - if (this.wrapper) - { - this.wrapper.height(this.wrapper.height() + _height); - } - else - { - this.table.height(this.table.height() + _height ); - } - } - - } - }, - - /** - * Get a dummy row object containing all widget of a row - * - * This is only a temp. solution until rows are implemented as eT2 containers and - * _sender.getParent() will return a real row container. - * - * @param {et2_widget} _sender - * @returns {Array|undefined} - */ - getRow: function(_sender) - { - if (!_sender || !this.cells) return; - - for(var r=0; r < this.cells.length; ++r) - { - var row = this.cells[r]; - for(var c=0; c < row.length; ++c) - { - if (!row[c].widget) continue; - - var found = row[c].widget === _sender; - if (!found) row[c].widget.iterateOver(function(_widget) {if (_widget === _sender) found = true;}); - if (found) - { - // return a fake row object allowing to iterate over it's children - var row_obj = new et2_widget(this, {}); - for(var c=0; c < row.length; ++c) - { - if (row[c].widget) row_obj._children.push(row[c].widget); - } - row_obj.isInTree = jQuery.proxy(this.isInTree, this); - // we must not free the children! - row_obj.destroy = function(){ - delete row_obj._children; - }; - return row_obj; - } - } - } - } -});}).call(this); -et2_register_widget(et2_grid, ["grid"]); +var et2_grid = /** @class */ (function (_super) { + __extends(et2_grid, _super); + /** + * Constructor + * + * @memberOf et2_grid + */ + function et2_grid(_parent, _attrs, _child) { + var _this = + // Call the parent constructor + _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_grid._attributes, _child || {})) || this; + // Counters for rows and columns + _this.rowCount = 0; + _this.columnCount = 0; + // 2D-Array which holds references to the DOM td tags + _this.cells = []; + _this.rowData = []; + _this.colData = []; + _this.managementArray = []; + // Keep the template node for later regeneration + _this.template_node = null; + // Wrapper div for height & overflow, if needed + _this.wrapper = null; + // Create the table body and the table + _this.table = jQuery(document.createElement("table")) + .addClass("et2_grid"); + _this.thead = jQuery(document.createElement("thead")) + .appendTo(_this.table); + _this.tfoot = jQuery(document.createElement("tfoot")) + .appendTo(_this.table); + _this.tbody = jQuery(document.createElement("tbody")) + .appendTo(_this.table); + return _this; + } + et2_grid.prototype._initCells = function (_colData, _rowData) { + // Copy the width and height + var w = _colData.length; + var h = _rowData.length; + // Create the 2D-Cells array + var cells = new Array(h); + for (var y = 0; y < h; y++) { + cells[y] = new Array(w); + // Initialize the cell description objects + for (var x = 0; x < w; x++) { + // Some columns (nm) we do not parse into a boolean + var col_disabled = _colData[x].disabled; + cells[y][x] = { + "td": null, + "widget": null, + "colData": _colData[x], + "rowData": _rowData[y], + "disabled": col_disabled || _rowData[y].disabled, + "class": _colData[x]["class"], + "colSpan": 1, + "autoColSpan": false, + "rowSpan": 1, + "autoRowSpan": false, + "width": _colData[x].width, + "x": x, + "y": y + }; + } + } + return cells; + }; + et2_grid.prototype._getColDataEntry = function () { + return { + width: "auto", + class: "", + align: "", + span: "1", + disabled: false + }; + }; + et2_grid.prototype._getRowDataEntry = function () { + return { + height: "auto", + class: "", + valign: "top", + span: "1", + disabled: false + }; + }; + et2_grid.prototype._getCell = function (_cells, _x, _y) { + if ((0 <= _y) && (_y < _cells.length)) { + var row = _cells[_y]; + if ((0 <= _x) && (_x < row.length)) { + return row[_x]; + } + } + throw ("Error while accessing grid cells, invalid element count or span value!"); + }; + et2_grid.prototype._forceNumber = function (_val) { + if (isNaN(_val)) { + throw (_val + " is not a number!"); + } + return parseInt(_val); + }; + et2_grid.prototype._fetchRowColData = function (columns, rows, colData, rowData) { + // Some things cannot be done inside a nextmatch - nm will do the expansion later + var nm = false; + var widget = this; + while (!nm && widget != this.getRoot()) { + nm = (widget.getType() == 'nextmatch'); + widget = widget.getParent(); + } + // Parse the columns tag + et2_filteredNodeIterator(columns, function (node, nodeName) { + var colDataEntry = this._getColDataEntry(); + // This cannot be done inside a nm, it will expand it later + colDataEntry["disabled"] = nm ? + et2_readAttrWithDefault(node, "disabled", "") : + this.getArrayMgr("content") + .parseBoolExpression(et2_readAttrWithDefault(node, "disabled", "")); + if (nodeName == "column") { + colDataEntry["width"] = et2_readAttrWithDefault(node, "width", "auto"); + colDataEntry["class"] = et2_readAttrWithDefault(node, "class", ""); + colDataEntry["align"] = et2_readAttrWithDefault(node, "align", ""); + colDataEntry["span"] = et2_readAttrWithDefault(node, "span", "1"); + // Keep any others attributes set, there's no 'column' widget + for (var i in node.attributes) { + var attr = node.attributes[i]; + if (attr.nodeType == 2 && typeof colDataEntry[attr.nodeName] == 'undefined') { + colDataEntry[attr.nodeName] = attr.value; + } + } + } + else { + colDataEntry["span"] = "all"; + } + colData.push(colDataEntry); + }, this); + // Parse the rows tag + et2_filteredNodeIterator(rows, function (node, nodeName) { + var rowDataEntry = this._getRowDataEntry(); + rowDataEntry["disabled"] = this.getArrayMgr("content") + .parseBoolExpression(et2_readAttrWithDefault(node, "disabled", "")); + if (nodeName == "row") { + // Remember this row for auto-repeat - it'll eventually be the last one + this.lastRowNode = node; + rowDataEntry["height"] = et2_readAttrWithDefault(node, "height", "auto"); + rowDataEntry["class"] = et2_readAttrWithDefault(node, "class", ""); + rowDataEntry["valign"] = et2_readAttrWithDefault(node, "valign", ""); + rowDataEntry["span"] = et2_readAttrWithDefault(node, "span", "1"); + rowDataEntry["part"] = et2_readAttrWithDefault(node, "part", "body"); + var id = et2_readAttrWithDefault(node, "id", ""); + if (id) { + rowDataEntry["id"] = id; + } + } + else { + rowDataEntry["span"] = "all"; + } + rowData.push(rowDataEntry); + }, this); + // Add in repeated rows + // TODO: It would be nice if we could skip header (thead) & footer (tfoot) or treat them separately + var rowIndex = Infinity; + if (this.getArrayMgr("content")) { + var content_1 = this.getArrayMgr("content"); + var rowDataEntry = rowData[rowData.length - 1]; + rowIndex = rowData.length - 1; + // Find out if we have any content rows, and how many + var cont = true; + var _loop_1 = function () { + if (content_1.data[rowIndex]) { + rowData[rowIndex] = jQuery.extend({}, rowDataEntry); + rowIndex++; + } + else if (this_1.lastRowNode != null) { + // Have to look through actual widgets to support const[$row] + // style names - should be avoided so we can remove this extra check + // Old etemplate checked first two widgets, or first two box children + // This cannot be done inside a nextmatch - nm will do the expansion later + nm = false; + if (nm) { + return "break"; + } + // Not in a nextmatch, so we can expand with abandon + var currentPerspective = jQuery.extend({}, content_1.perspectiveData); + var check_1 = function (node, nodeName) { + if (nodeName == 'box' || nodeName == 'hbox' || nodeName == 'vbox') { + return et2_filteredNodeIterator(node, check_1, this); + } + content_1.perspectiveData.row = rowIndex; + for (var attr in node.attributes) { + var value = et2_readAttrWithDefault(node, node.attributes[attr].name, ""); + // Don't include first char, those should be handled by normal means + // and it would break nextmatch + if (value.indexOf('@') > 0 || value.indexOf('$') > 0) { + // Ok, we found something. How many? Check for values. + var ident = content_1.expandName(value); + // expandName() handles index into content (@), but we have to look up + // regular values + if (value[0] != '@') { + // Returns null if there isn't an actual value + ident = content_1.getEntry(ident, false, true); + } + while (ident != null && rowIndex < 1000) { + rowData[rowIndex] = jQuery.extend({}, rowDataEntry); + content_1.perspectiveData.row = ++rowIndex; + ident = content_1.expandName(value); + if (value[0] != '@') { + // Returns null if there isn't an actual value + ident = content_1.getEntry(ident, false, true); + } + } + if (rowIndex >= 1000) { + egw.debug("error", "Problem in autorepeat fallback: too many rows for '%s'. Use a nextmatch, or start debugging.", value); + } + return; + } + } + }; + et2_filteredNodeIterator(this_1.lastRowNode, check_1, this_1); + cont = false; + content_1.perspectiveData = currentPerspective; + } + else { + return "break"; + } + }; + var this_1 = this, nm; + while (cont) { + var state_1 = _loop_1(); + if (state_1 === "break") + break; + } + } + if (rowIndex <= rowData.length - 1) { + // No auto-repeat + this.lastRowNode = null; + } + }; + et2_grid.prototype._fillCells = function (cells, columns, rows) { + var h = cells.length; + var w = (h > 0) ? cells[0].length : 0; + var currentPerspective = jQuery.extend({}, this.getArrayMgr("content").perspectiveData); + // Read the elements inside the columns + var x = 0; + et2_filteredNodeIterator(columns, function (node, nodeName) { + function _readColNode(node, nodeName) { + if (y >= h) { + this.egw().debug("warn", "Skipped grid cell in column, '" + + nodeName + "'"); + return; + } + var cell = this._getCell(cells, x, y); + // Read the span value of the element + if (node.getAttribute("span")) { + cell.rowSpan = node.getAttribute("span"); + } + else { + cell.rowSpan = cell.colData["span"]; + cell.autoRowSpan = true; + } + if (cell.rowSpan == "all") { + cell.rowSpan = cells.length; + } + var span = cell.rowSpan = this._forceNumber(cell.rowSpan); + // Create the widget + var widget = this.createElementFromNode(node, nodeName); + // Fill all cells the widget is spanning + for (var i = 0; i < span && y < cells.length; i++, y++) { + this._getCell(cells, x, y).widget = widget; + } + } + // If the node is a column, create the widgets which belong into + // the column + var y = 0; + if (nodeName == "column") { + et2_filteredNodeIterator(node, _readColNode, this); + } + else { + _readColNode.call(this, node, nodeName); + } + x++; + }, this); + // Read the elements inside the rows + var y = 0; + x = 0; + var readRowNode; + var nm = false; + var widget = this; + while (!nm && widget != this.getRoot()) { + nm = (widget.getType() == 'nextmatch'); + widget = widget.getParent(); + } + et2_filteredNodeIterator(rows, function (node, nodeName) { + readRowNode = function _readRowNode(node, nodeName) { + if (x >= w) { + if (nodeName != "description") { + // Only notify it skipping other than description, + // description used to pad + this.egw().debug("warn", "Skipped grid cell in row, '" + + nodeName + "'"); + } + return; + } + var cell = this._getCell(cells, x, y); + // Read the span value of the element + if (node.getAttribute("span")) { + cell.colSpan = node.getAttribute("span"); + } + else { + cell.colSpan = cell.rowData["span"]; + cell.autoColSpan = true; + } + if (cell.colSpan == "all") { + cell.colSpan = cells[y].length; + } + var span = cell.colSpan = this._forceNumber(cell.colSpan); + // Read the align value of the element + if (node.getAttribute("align")) { + cell.align = node.getAttribute("align"); + } + // store id of nextmatch-*headers, so it is available for disabled widgets, which get not instanciated + if (nodeName.substr(0, 10) == 'nextmatch-') { + cell.nm_id = node.getAttribute('id'); + } + // Apply widget's class to td, for backward compatability + if (node.getAttribute("class")) { + cell.class += (cell.class ? " " : "") + node.getAttribute("class"); + } + // Create the element + if (!cell.disabled || cell.disabled && typeof cell.disabled === 'string') { + //Skip if it is a nextmatch while the nextmatch handles row adjustment by itself + if (!nm) { + // Adjust for the row + var mgrs = this.getArrayMgrs(); + for (var name_1 in mgrs) { + this.getArrayMgr(name_1).perspectiveData.row = y; + } + if (this._getCell(cells, x, y).rowData.id) { + this._getCell(cells, x, y).rowData.id = this.getArrayMgr("content").expandName(this._getCell(cells, x, y).rowData.id); + } + if (this._getCell(cells, x, y).rowData.class) { + this._getCell(cells, x, y).rowData.class = this.getArrayMgr("content").expandName(this._getCell(cells, x, y).rowData.class); + } + } + if (!nm && typeof cell.disabled === 'string') { + cell.disabled = this.getArrayMgr("content").parseBoolExpression(cell.disabled); + } + if (nm || !cell.disabled) { + var widget = this.createElementFromNode(node, nodeName); + } + } + // Fill all cells the widget is spanning + for (var i = 0; i < span && x < cells[y].length; i++, x++) { + cell = this._getCell(cells, x, y); + if (cell.widget == null) { + cell.widget = widget; + } + else { + throw ("Grid cell collision, two elements " + + "defined for cell (" + x + "," + y + ")!"); + } + } + }; + // If the node is a row, create the widgets which belong into + // the row + x = 0; + if (this.lastRowNode && node == this.lastRowNode) { + return; + } + if (nodeName == "row") { + // Adjust for the row + for (var name in this.getArrayMgrs()) { + //this.getArrayMgr(name).perspectiveData.row = y; + } + var cell = this._getCell(cells, x, y); + if (cell.rowData.id) { + this.getArrayMgr("content").expandName(cell.rowData.id); + } + // If row disabled, just skip it + var disabled = false; + if (node.getAttribute("disabled") == "1") { + disabled = true; + } + if (!disabled) { + et2_filteredNodeIterator(node, readRowNode, this); + } + } + else { + readRowNode.call(this, node, nodeName); + } + y++; + }, this); + // Extra content rows + for (y; y < h; y++) { + x = 0; + et2_filteredNodeIterator(this.lastRowNode, readRowNode, this); + } + // Reset + for (var name in this.getArrayMgrs()) { + this.getArrayMgr(name).perspectiveData = currentPerspective; + } + }; + et2_grid.prototype._expandLastCells = function (_cells) { + var h = _cells.length; + var w = (h > 0) ? _cells[0].length : 0; + // Determine the last cell in each row and expand its span value if + // the span has not been explicitly set. + for (var y = 0; y < h; y++) { + for (var x = w - 1; x >= 0; x--) { + var cell = _cells[y][x]; + if (cell.widget != null) { + if (cell.autoColSpan) { + cell.colSpan = w - x; + } + break; + } + } + } + // Determine the last cell in each column and expand its span value if + // the span has not been explicitly set. + for (var x = 0; x < w; x++) { + for (var y = h - 1; y >= 0; y--) { + var cell = _cells[y][x]; + if (cell.widget != null) { + if (cell.autoRowSpan) { + cell.rowSpan = h - y; + } + break; + } + } + } + }; + et2_grid.prototype._createNamespace = function () { + return true; + }; + /** + * As the does not fit very well into the default widget structure, we're + * overwriting the loadFromXML function and doing a two-pass reading - + * in the first step the + * + * @param {object} _node xml node to process + */ + et2_grid.prototype.loadFromXML = function (_node) { + // Keep the node for later changing / reloading + this.template_node = _node; + // Get the columns and rows tag + var rowsElems = et2_directChildrenByTagName(_node, "rows"); + var columnsElems = et2_directChildrenByTagName(_node, "columns"); + if (rowsElems.length == 1 && columnsElems.length == 1) { + var columns = columnsElems[0]; + var rows = rowsElems[0]; + var colData = []; + var rowData = []; + // Fetch the column and row data + this._fetchRowColData(columns, rows, colData, rowData); + // Initialize the cells + var cells = this._initCells(colData, rowData); + // Create the widgets inside the cells and read the span values + this._fillCells(cells, columns, rows); + // Expand the span values of the last cells + this._expandLastCells(cells); + // Create the table rows + this.createTableFromCells(cells, colData, rowData); + } + else { + throw ("Error while parsing grid, none or multiple rows or columns tags!"); + } + }; + et2_grid.prototype.createTableFromCells = function (_cells, _colData, _rowData) { + this.managementArray = []; + this.cells = _cells; + this.colData = _colData; + this.rowData = _rowData; + // Set the rowCount and columnCount variables + var h = this.rowCount = _cells.length; + var w = this.columnCount = (h > 0) ? _cells[0].length : 0; + // Create the table rows. + for (var y = 0; y < h; y++) { + var parent_1 = this.tbody; + switch (this.rowData[y]["part"]) { + case 'header': + if (!this.tbody.children().length && !this.tfoot.children().length) { + parent_1 = this.thead; + } + break; + case 'footer': + if (!this.tbody.children().length) { + parent_1 = this.tfoot; + } + break; + } + var tr = jQuery(document.createElement("tr")).appendTo(parent_1) + .addClass(this.rowData[y]["class"]); + if (this.rowData[y].disabled) { + tr.hide(); + } + if (this.rowData[y].height != "auto") { + tr.height(this.rowData[y].height); + } + if (this.rowData[y].valign) { + tr.attr("valign", this.rowData[y].valign); + } + if (this.rowData[y].id) { + tr.attr("id", this.rowData[y].id); + } + // Create the cells. x is incremented by the colSpan value of the + // cell. + for (var x = 0; x < w;) { + // Fetch a cell from the cells + var cell = this._getCell(_cells, x, y); + if (cell.td == null && cell.widget != null) { + // Create the cell + var td = jQuery(document.createElement("td")).appendTo(tr) + .addClass(cell["class"]); + if (cell.disabled) { + td.hide(); + cell.widget.options.disabled = cell.disabled; + } + if (cell.width != "auto") { + td.width(cell.width); + } + if (cell.align) { + td.attr("align", cell.align); + } + // Add the entry for the widget to the management array + this.managementArray.push({ + "cell": td[0], + "widget": cell.widget, + "disabled": cell.disabled + }); + // Set the span values of the cell + var cs = (x == w - 1) ? w - x : Math.min(w - x, cell.colSpan); + var rs = (y == h - 1) ? h - y : Math.min(h - y, cell.rowSpan); + // Set the col and row span values + if (cs > 1) { + td.attr("colspan", cs); + } + if (rs > 1) { + td.attr("rowspan", rs); + } + // Assign the td to the cell + for (var sx = x; sx < x + cs; sx++) { + for (var sy = y; sy < y + rs; sy++) { + this._getCell(_cells, sx, sy).td = td; + } + } + x += cell.colSpan; + } + else { + x++; + } + } + } + }; + et2_grid.prototype.getDOMNode = function (_sender) { + // If the parent class functions are asking for the DOM-Node, return the + // outer table. + if (_sender == this || typeof _sender == 'undefined') { + return this.wrapper != null ? this.wrapper[0] : this.table[0]; + } + // Check whether the _sender object exists inside the management array + for (var i = 0; i < this.managementArray.length; i++) { + if (this.managementArray[i].widget == _sender) { + return this.managementArray[i].cell; + } + } + return null; + }; + et2_grid.prototype.isInTree = function (_sender) { + var vis = true; + if (typeof _sender != "undefined" && _sender != this) { + vis = false; + // Check whether the _sender object exists inside the management array + for (var i = 0; i < this.managementArray.length; i++) { + if (this.managementArray[i].widget == _sender) { + vis = !(typeof this.managementArray[i].disabled === 'boolean' ? + this.managementArray[i].disabled : + this.getArrayMgr("content").parseBoolExpression(this.managementArray[i].disabled)); + break; + } + } + } + return _super.prototype.isInTree.call(this, this, vis); + }; + /** + * Set the overflow attribute + * + * Grid needs special handling because HTML tables don't do overflow. We + * create a wrapper DIV to handle it. + * No value or default visible needs no wrapper, as table is always overflow visible. + * + * @param {string} _value Overflow value, must be a valid CSS overflow value, default 'visible' + */ + et2_grid.prototype.set_overflow = function (_value) { + var wrapper = this.wrapper || this.table.parent('[id$="_grid_wrapper"]'); + this.overflow = _value; + if (wrapper.length == 0 && _value && _value !== 'visible') { + this.wrapper = wrapper = this.table.wrap('
          ').parent(); + if (this.height) { + wrapper.css('height', this.height); + } + } + wrapper.css('overflow', _value); + if (wrapper.length && (!_value || _value === 'visible')) { + this.table.unwrap(); + } + }; + /** + * Change the content for the grid, and re-generate its contents. + * + * Changing the content does not allow changing the structure of the grid, + * as that is loaded from the template file. The rows and widgets inside + * will be re-created (including auto-repeat). + * + * @param {Object} _value New data for the grid + * @param {Object} [_value.content] New content + * @param {Object} [_value.sel_options] New select options + * @param {Object} [_value.readonlys] New read-only values + */ + et2_grid.prototype.set_value = function (_value) { + // Destroy children, empty grid + for (var i = 0; i < this.managementArray.length; i++) { + var cell = this.managementArray[i]; + if (cell.widget) { + cell.widget.destroy(); + } + } + this.managementArray = []; + this.thead.empty(); + this.tfoot.empty(); + this.tbody.empty(); + // Update array managers + for (var key in _value) { + this.getArrayMgr(key).data = _value[key]; + } + // Rebuild grid + this.loadFromXML(this.template_node); + // New widgets need to finish + var promises = []; + this.loadingFinished(promises); + }; + /** + * Sortable allows you to reorder grid rows using the mouse. + * The new order is returned as part of the value of the + * grid, in 'sort_order'. + * + * @param {boolean|function} sortable Callback or false to disable + */ + et2_grid.prototype.set_sortable = function (sortable) { + if (!sortable) { + this.tbody.sortable("destroy"); + return; + } + // Make sure rows have IDs, so sortable has something to return + jQuery('tr', this.tbody).each(function (index) { + var $this = jQuery(this); + // Header does not participate in sorting + if ($this.hasClass('th')) + return; + // If row doesn't have an ID, assign the index as ID + if (!$this.attr("id")) + $this.attr("id", index); + }); + var self = this; + // Set up sortable + this.tbody.sortable({ + // Header does not participate in sorting + items: "tr:not(.th)", + distance: 15, + cancel: this.options.sortable_cancel, + placeholder: this.options.sortable_placeholder, + containment: this.options.sortable_containment, + connectWith: this.options.sortable_connectWith, + update: function (event, ui) { + self.egw().json(sortable, [self.tbody.sortable("toArray"), self.id], null, self, true).sendRequest(); + }, + receive: function (event, ui) { + if (typeof self.options.sortable_recieveCallback == 'function') { + self.options.sortable_recieveCallback.call(self, event, ui); + } + }, + start: function (event, ui) { + if (typeof self.options.sortable_startCallback == 'function') { + self.options.sortable_startCallback.call(self, event, ui); + } + } + }); + }; + /** + * Override parent to apply actions on each row + * + * @param {array} actions [ {ID: attributes..}+] as for set_actions + */ + et2_grid.prototype._link_actions = function (actions) { + // Get the top level element for the tree + // @ts-ignore + var objectManager = window.egw_getAppObjectManager(true); + objectManager = objectManager.getObjectById(this.getInstanceManager().uniqueId, 2) || objectManager; + 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_core_DOMWidget_1.et2_action_object_impl(this).getAOI(), this._actionManager || objectManager.manager.getActionById(this.id) || objectManager.manager)); + } + // Delete all old objects + widget_object.clear(); + // 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); + // Deal with each row in tbody, ignore action-wise rows in thead or tfooter for now + var i = 0, r = 0; + for (; i < this.rowData.length; i++) { + if (this.rowData[i].part != 'body') + continue; + var content = this.getArrayMgr('content').getEntry(i); + if (content) { + // Add a new action object to the object manager + var row = jQuery('tr', this.tbody)[r]; + var aoi = new et2_core_DOMWidget_1.et2_action_object_impl(this, row).getAOI(); + var obj = widget_object.addObject(content.id || "row_" + r, aoi); + // Set the data to the content so it's available for the action + obj.data = content; + obj.updateActionLinks(action_links); + } + r++; + } + }; + /** + * Code for implementing et2_IDetachedDOM + * This doesn't need to be implemented. + * Individual widgets are detected and handled by the grid, but the interface is needed for this to happen + * + * @param {array} _attrs array to add further attributes to + */ + et2_grid.prototype.getDetachedAttributes = function (_attrs) { + }; + et2_grid.prototype.getDetachedNodes = function () { + return [this.getDOMNode()]; + }; + et2_grid.prototype.setDetachedAttributes = function (_nodes, _values) { + }; + /** + * Generates nextmatch column name for headers in a grid + * + * Implemented here as default implementation in et2_externsion_nextmatch + * only considers children, but grid does NOT instanciate disabled rows as children. + * + * @return {string} + */ + et2_grid.prototype._getColumnName = function () { + var ids = []; + for (var r = 0; r < this.cells.length; ++r) { + var cols = this.cells[r]; + for (var c = 0; c < cols.length; ++c) { + if (cols[c].nm_id) + ids.push(cols[c].nm_id); + } + } + return ids.join('_'); + }; + et2_grid.prototype.resize = function (_height) { + if (typeof this.options != 'undefined' && _height + && typeof this.options.resize_ratio != 'undefined' && this.options.resize_ratio) { + // apply the ratio + _height = (this.options.resize_ratio != '') ? _height * this.options.resize_ratio : _height; + if (_height != 0) { + if (this.wrapper) { + this.wrapper.height(this.wrapper.height() + _height); + } + else { + this.table.height(this.table.height() + _height); + } + } + } + }; + /** + * Get a dummy row object containing all widget of a row + * + * This is only a temp. solution until rows are implemented as eT2 containers and + * _sender.getParent() will return a real row container. + * + * @deprecated Not used? Remove this if you find something using it + * + * @param {et2_widget} _sender + * @returns {Array|undefined} + */ + et2_grid.prototype.getRow = function (_sender) { + if (!_sender || !this.cells) + return; + for (var r = 0; r < this.cells.length; ++r) { + var row = this.cells[r]; + var _loop_2 = function () { + if (!row[c].widget) + return "continue"; + var found = row[c].widget === _sender; + if (!found) + row[c].widget.iterateOver(function (_widget) { if (_widget === _sender) + found = true; }); + if (found) { + // return a fake row object allowing to iterate over it's children + var row_obj_1 = new et2_core_widget_1.et2_widget(this_2, {}); + for (var c = 0; c < row.length; ++c) { + if (row[c].widget) + row_obj_1.addChild(row[c].widget); + } + row_obj_1.isInTree = jQuery.proxy(this_2.isInTree, this_2); + // we must not free the children! + row_obj_1.destroy = function () { + // @ts-ignore + delete row_obj_1._children; + }; + return { value: row_obj_1 }; + } + }; + var this_2 = this; + for (var c = 0; c < row.length; ++c) { + var state_2 = _loop_2(); + if (typeof state_2 === "object") + return state_2.value; + } + } + }; + /** + * Needed for the align interface, but we're not doing anything with it... + */ + et2_grid.prototype.get_align = function () { + return ""; + }; + et2_grid._attributes = { + // Better to use CSS, no need to warn about it + "border": { + "ignore": true + }, + "align": { + "name": "Align", + "type": "string", + "default": "left", + "description": "Position of this element in the parent hbox" + }, + "spacing": { + "ignore": true + }, + "padding": { + "ignore": true + }, + "sortable": { + "name": "Sortable callback", + "type": "string", + "default": et2_no_init, + "description": "PHP function called when user sorts the grid. Setting this enables sorting the grid rows. The callback will be passed the ID of the grid and the new order of the rows." + }, + sortable_containment: { + name: "Sortable bounding area", + type: "string", + default: "", + description: "Defines bounding area for sortable items" + }, + sortable_connectWith: { + name: "Sortable connectWith element", + type: "string", + default: "", + description: "Defines other sortable areas that should be connected to sort list" + }, + sortable_placeholder: { + name: "Sortable placeholder", + type: "string", + default: "", + description: "Defines sortable placeholder" + }, + sortable_cancel: { + name: "Sortable cancel class", + type: "string", + default: "", + description: "Defines sortable cancel which prevents sorting the matching element" + }, + sortable_recieveCallback: { + name: "Sortable receive callback", + type: "js", + default: et2_no_init, + description: "Defines sortable receive callback function" + }, + sortable_startCallback: { + name: "Sortable start callback", + type: "js", + default: et2_no_init, + description: "Defines sortable start callback function" + } + }; + return et2_grid; +}(et2_core_DOMWidget_1.et2_DOMWidget)); +exports.et2_grid = et2_grid; +et2_core_widget_1.et2_register_widget(et2_grid, ["grid"]); +//# sourceMappingURL=et2_widget_grid.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_grid.ts b/api/js/etemplate/et2_widget_grid.ts new file mode 100644 index 0000000000..258dc4c5dd --- /dev/null +++ b/api/js/etemplate/et2_widget_grid.ts @@ -0,0 +1,1178 @@ +/** + * EGroupware eTemplate2 - JS Grid object + * + * @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 + * @version $Id$ + */ + +/*egw:uses + /vendor/bower-asset/jquery/dist/jquery.js; + et2_core_DOMWidget; + et2_core_xml; +*/ + +import './et2_core_common'; +import './et2_core_interfaces'; +import {et2_register_widget, et2_widget, WidgetConfig} from "./et2_core_widget"; +import {ClassWithAttributes} from "./et2_core_inheritance"; +import {et2_action_object_impl, et2_DOMWidget} from "./et2_core_DOMWidget"; +import '../egw_action/egw_action.js'; + + +/** + * Class which implements the "grid" XET-Tag + * + * This also includes repeating the last row in the grid and filling + * it with content data + * + * @augments et2_DOMWidget + */ +export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAligned, et2_IResizeable +{ + static readonly _attributes : any = { + // Better to use CSS, no need to warn about it + "border": { + "ignore": true + }, + "align": { + "name": "Align", + "type": "string", + "default": "left", + "description": "Position of this element in the parent hbox" + }, + "spacing": { + "ignore": true + }, + "padding": { + "ignore": true + }, + "sortable": { + "name": "Sortable callback", + "type": "string", + "default": et2_no_init, + "description": "PHP function called when user sorts the grid. Setting this enables sorting the grid rows. The callback will be passed the ID of the grid and the new order of the rows." + }, + sortable_containment: { + name: "Sortable bounding area", + type: "string", + default: "", + description: "Defines bounding area for sortable items" + }, + sortable_connectWith: { + name: "Sortable connectWith element", + type: "string", + default: "", + description: "Defines other sortable areas that should be connected to sort list" + }, + sortable_placeholder: { + name: "Sortable placeholder", + type: "string", + default: "", + description: "Defines sortable placeholder" + }, + sortable_cancel: { + name: "Sortable cancel class", + type: "string", + default: "", + description: "Defines sortable cancel which prevents sorting the matching element" + }, + sortable_recieveCallback: { + name: "Sortable receive callback", + type: "js", + default: et2_no_init, + description: "Defines sortable receive callback function" + }, + sortable_startCallback: { + name: "Sortable start callback", + type: "js", + default: et2_no_init, + description: "Defines sortable start callback function" + } + }; + private table: JQuery; + private thead: JQuery; + private tfoot: JQuery; + private tbody: JQuery; + + // Counters for rows and columns + private rowCount: number = 0; + private columnCount: number = 0; + + // 2D-Array which holds references to the DOM td tags + private cells = []; + private rowData = []; + private colData = []; + private managementArray = []; + + // Keep the template node for later regeneration + private template_node = null; + + // Wrapper div for height & overflow, if needed + private wrapper = null; + private lastRowNode: null; + + /** + * Constructor + * + * @memberOf et2_grid + */ + constructor(_parent, _attrs? : WidgetConfig, _child? : object) + { + // Call the parent constructor + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_grid._attributes, _child || {})); + // Create the table body and the table + this.table = jQuery(document.createElement("table")) + .addClass("et2_grid"); + this.thead = jQuery(document.createElement("thead")) + .appendTo(this.table); + this.tfoot = jQuery(document.createElement("tfoot")) + .appendTo(this.table); + this.tbody = jQuery(document.createElement("tbody")) + .appendTo(this.table); + } + + _initCells(_colData : ColumnEntry[], _rowData : RowEntry[]) { + // Copy the width and height + const w = _colData.length; + const h = _rowData.length; + + // Create the 2D-Cells array + const cells = new Array(h); + for (let y = 0; y < h; y++) + { + cells[y] = new Array(w); + + // Initialize the cell description objects + for (let x = 0; x < w; x++) + { + // Some columns (nm) we do not parse into a boolean + const col_disabled = _colData[x].disabled; + cells[y][x] = { + "td": null, + "widget": null, + "colData": _colData[x], + "rowData": _rowData[y], + "disabled": col_disabled || _rowData[y].disabled, + "class": _colData[x]["class"], + "colSpan": 1, + "autoColSpan": false, + "rowSpan": 1, + "autoRowSpan": false, + "width": _colData[x].width, + "x": x, + "y": y + }; + } + } + + return cells; + } + + _getColDataEntry() : ColumnEntry + { + return { + width: "auto", + class: "", + align: "", + span: "1", + disabled: false + }; + } + + _getRowDataEntry() : RowEntry + { + return { + height: "auto", + class: "", + valign: "top", + span: "1", + disabled: false + }; + } + + _getCell(_cells, _x, _y) + { + if ((0 <= _y) && (_y < _cells.length)) + { + const row = _cells[_y]; + if ((0 <= _x) && (_x < row.length)) + { + return row[_x]; + } + } + + throw("Error while accessing grid cells, invalid element count or span value!"); + } + + _forceNumber(_val) + { + if (isNaN(_val)) + { + throw(_val + " is not a number!"); + } + + return parseInt(_val); + } + + _fetchRowColData(columns, rows, colData : ColumnEntry[], rowData : RowEntry[]) + { + // Some things cannot be done inside a nextmatch - nm will do the expansion later + var nm = false; + let widget: et2_widget = this; + while(!nm && widget != this.getRoot()) + { + nm = (widget.getType() == 'nextmatch'); + widget = widget.getParent(); + } + + // Parse the columns tag + et2_filteredNodeIterator(columns, function(node, nodeName) { + const colDataEntry = this._getColDataEntry(); + // This cannot be done inside a nm, it will expand it later + colDataEntry["disabled"] = nm ? + et2_readAttrWithDefault(node, "disabled", "") : + this.getArrayMgr("content") + .parseBoolExpression(et2_readAttrWithDefault(node, "disabled", "")); + if (nodeName == "column") + { + colDataEntry["width"] = et2_readAttrWithDefault(node, "width", "auto"); + colDataEntry["class"] = et2_readAttrWithDefault(node, "class", ""); + colDataEntry["align"] = et2_readAttrWithDefault(node, "align", ""); + colDataEntry["span"] = et2_readAttrWithDefault(node, "span", "1"); + + // Keep any others attributes set, there's no 'column' widget + for(let i in node.attributes) + { + const attr = node.attributes[i]; + if(attr.nodeType == 2 && typeof colDataEntry[attr.nodeName] == 'undefined') + { + colDataEntry[attr.nodeName] = attr.value; + } + } + } + else + { + colDataEntry["span"] = "all"; + } + colData.push(colDataEntry); + }, this); + + // Parse the rows tag + et2_filteredNodeIterator(rows, function(node, nodeName) { + const rowDataEntry = this._getRowDataEntry(); + rowDataEntry["disabled"] = this.getArrayMgr("content") + .parseBoolExpression(et2_readAttrWithDefault(node, "disabled", "")); + if (nodeName == "row") + { + // Remember this row for auto-repeat - it'll eventually be the last one + this.lastRowNode = node; + + rowDataEntry["height"] = et2_readAttrWithDefault(node, "height", "auto"); + rowDataEntry["class"] = et2_readAttrWithDefault(node, "class", ""); + rowDataEntry["valign"] = et2_readAttrWithDefault(node, "valign", ""); + rowDataEntry["span"] = et2_readAttrWithDefault(node, "span", "1"); + rowDataEntry["part"] = et2_readAttrWithDefault(node, "part", "body"); + + const id = et2_readAttrWithDefault(node, "id", ""); + if(id) + { + rowDataEntry["id"] = id; + } + } + else + { + rowDataEntry["span"] = "all"; + } + rowData.push(rowDataEntry); + }, this); + + // Add in repeated rows + // TODO: It would be nice if we could skip header (thead) & footer (tfoot) or treat them separately + let rowIndex = Infinity; + if(this.getArrayMgr("content")) + { + const content = this.getArrayMgr("content"); + var rowDataEntry = rowData[rowData.length-1]; + rowIndex = rowData.length-1; + // Find out if we have any content rows, and how many + let cont = true; + while(cont) + { + if(content.data[rowIndex]) + { + rowData[rowIndex] = jQuery.extend({}, rowDataEntry); + + rowIndex++; + } + + else if (this.lastRowNode != null) + { + // Have to look through actual widgets to support const[$row] + // style names - should be avoided so we can remove this extra check + // Old etemplate checked first two widgets, or first two box children + // This cannot be done inside a nextmatch - nm will do the expansion later + var nm = false; + if(nm) + { + // No further checks for repeated rows + break; + } + + // Not in a nextmatch, so we can expand with abandon + const currentPerspective = jQuery.extend({}, content.perspectiveData); + const check = function (node, nodeName) { + if (nodeName == 'box' || nodeName == 'hbox' || nodeName == 'vbox') { + return et2_filteredNodeIterator(node, check, this); + } + content.perspectiveData.row = rowIndex; + for (let attr in node.attributes) { + const value = et2_readAttrWithDefault(node, node.attributes[attr].name, ""); + // Don't include first char, those should be handled by normal means + // and it would break nextmatch + if (value.indexOf('@') > 0 || value.indexOf('$') > 0) { + // Ok, we found something. How many? Check for values. + let ident = content.expandName(value); + // expandName() handles index into content (@), but we have to look up + // regular values + if (value[0] != '@') { + // Returns null if there isn't an actual value + ident = content.getEntry(ident, false, true); + } + while (ident != null && rowIndex < 1000) { + rowData[rowIndex] = jQuery.extend({}, rowDataEntry); + content.perspectiveData.row = ++rowIndex; + ident = content.expandName(value); + if (value[0] != '@') { + // Returns null if there isn't an actual value + ident = content.getEntry(ident, false, true); + } + } + if (rowIndex >= 1000) { + egw.debug("error", "Problem in autorepeat fallback: too many rows for '%s'. Use a nextmatch, or start debugging.", value); + } + return; + } + } + }; + et2_filteredNodeIterator(this.lastRowNode, check,this); + cont = false; + content.perspectiveData = currentPerspective; + } + + else + { + // No more rows, stop + break; + } + } + } + if(rowIndex <= rowData.length - 1) + { + // No auto-repeat + this.lastRowNode = null; + } + } + + _fillCells(cells, columns : object[], rows : object[]) { + const h = cells.length; + const w = (h > 0) ? cells[0].length : 0; + const currentPerspective = jQuery.extend({}, this.getArrayMgr("content").perspectiveData); + + // Read the elements inside the columns + let x = 0; + + et2_filteredNodeIterator(columns, function(node, nodeName) { + + function _readColNode(node, nodeName) { + if (y >= h) + { + this.egw().debug("warn", "Skipped grid cell in column, '" + + nodeName + "'"); + return; + } + + const cell = this._getCell(cells, x, y); + + // Read the span value of the element + if (node.getAttribute("span")) + { + cell.rowSpan = node.getAttribute("span"); + } + else + { + cell.rowSpan = cell.colData["span"]; + cell.autoRowSpan = true; + } + + if (cell.rowSpan == "all") + { + cell.rowSpan = cells.length; + } + + const span = cell.rowSpan = this._forceNumber(cell.rowSpan); + + // Create the widget + const widget = this.createElementFromNode(node, nodeName); + + // Fill all cells the widget is spanning + for (let i = 0; i < span && y < cells.length; i++, y++) + { + this._getCell(cells, x, y).widget = widget; + } + } + + // If the node is a column, create the widgets which belong into + // the column + var y = 0; + if (nodeName == "column") + { + et2_filteredNodeIterator(node, _readColNode, this); + } + else + { + _readColNode.call(this, node, nodeName); + } + + x++; + }, this); + + // Read the elements inside the rows + var y = 0; + x = 0; + let readRowNode; + let nm = false; + var widget : et2_widget = this; + while(!nm && widget != this.getRoot()) + { + nm = (widget.getType() == 'nextmatch'); + widget = widget.getParent(); + } + et2_filteredNodeIterator(rows, function(node, nodeName) { + + readRowNode = function _readRowNode(node, nodeName) { + if (x >= w) + { + if(nodeName != "description") + { + // Only notify it skipping other than description, + // description used to pad + this.egw().debug("warn", "Skipped grid cell in row, '" + + nodeName + "'"); + } + return; + } + + let cell = this._getCell(cells, x, y); + + // Read the span value of the element + if (node.getAttribute("span")) + { + cell.colSpan = node.getAttribute("span"); + } + else + { + cell.colSpan = cell.rowData["span"]; + cell.autoColSpan = true; + } + + if (cell.colSpan == "all") + { + cell.colSpan = cells[y].length; + } + + const span = cell.colSpan = this._forceNumber(cell.colSpan); + + // Read the align value of the element + if (node.getAttribute("align")) + { + cell.align = node.getAttribute("align"); + } + + // store id of nextmatch-*headers, so it is available for disabled widgets, which get not instanciated + if (nodeName.substr(0, 10) == 'nextmatch-') + { + cell.nm_id = node.getAttribute('id'); + } + // Apply widget's class to td, for backward compatability + if(node.getAttribute("class")) + { + cell.class += (cell.class ? " " : "") + node.getAttribute("class"); + } + + // Create the element + if(!cell.disabled || cell.disabled && typeof cell.disabled === 'string') + { + //Skip if it is a nextmatch while the nextmatch handles row adjustment by itself + if(!nm) + { + // Adjust for the row + const mgrs = this.getArrayMgrs(); + for(let name in mgrs) + { + this.getArrayMgr(name).perspectiveData.row = y; + } + if(this._getCell(cells, x, y).rowData.id) + { + this._getCell(cells, x, y).rowData.id = this.getArrayMgr("content").expandName(this._getCell(cells, x, y).rowData.id); + } + if(this._getCell(cells, x, y).rowData.class) + { + this._getCell(cells, x, y).rowData.class = this.getArrayMgr("content").expandName(this._getCell(cells, x, y).rowData.class); + } + + } + if(!nm && typeof cell.disabled === 'string') + { + cell.disabled = this.getArrayMgr("content").parseBoolExpression(cell.disabled); + } + if(nm || !cell.disabled) + { + var widget = this.createElementFromNode(node, nodeName); + } + } + + // Fill all cells the widget is spanning + for (let i = 0; i < span && x < cells[y].length; i++, x++) + { + cell = this._getCell(cells, x, y); + if (cell.widget == null) + { + cell.widget = widget; + } + else + { + throw("Grid cell collision, two elements " + + "defined for cell (" + x + "," + y + ")!"); + } + } + }; + + // If the node is a row, create the widgets which belong into + // the row + x = 0; + if(this.lastRowNode && node == this.lastRowNode) + { + return; + } + if (nodeName == "row") + { + // Adjust for the row + for(var name in this.getArrayMgrs()) + { + //this.getArrayMgr(name).perspectiveData.row = y; + } + + let cell = this._getCell(cells, x,y); + if(cell.rowData.id) + { + this.getArrayMgr("content").expandName(cell.rowData.id); + } + // If row disabled, just skip it + let disabled = false; + if(node.getAttribute("disabled") == "1") + { + disabled = true; + } + if(!disabled) + { + et2_filteredNodeIterator(node, readRowNode, this); + } + } + else + { + readRowNode.call(this, node, nodeName); + } + + y++; + }, this); + + // Extra content rows + for(y; y < h; y++) { + x = 0; + + et2_filteredNodeIterator(this.lastRowNode, readRowNode, this); + } + // Reset + for(var name in this.getArrayMgrs()) + { + this.getArrayMgr(name).perspectiveData = currentPerspective; + } + } + + _expandLastCells(_cells) + { + const h = _cells.length; + const w = (h > 0) ? _cells[0].length : 0; + + // Determine the last cell in each row and expand its span value if + // the span has not been explicitly set. + for (var y = 0; y < h; y++) + { + for (var x = w - 1; x >= 0; x--) + { + var cell = _cells[y][x]; + + if (cell.widget != null) + { + if (cell.autoColSpan) + { + cell.colSpan = w - x; + } + break; + } + } + } + + // Determine the last cell in each column and expand its span value if + // the span has not been explicitly set. + for (var x = 0; x < w; x++) + { + for (var y = h - 1; y >= 0; y--) + { + var cell = _cells[y][x]; + + if (cell.widget != null) + { + if (cell.autoRowSpan) + { + cell.rowSpan = h - y; + } + break; + } + } + } + } + + _createNamespace() : boolean + { + return true; + } + /** + * As the does not fit very well into the default widget structure, we're + * overwriting the loadFromXML function and doing a two-pass reading - + * in the first step the + * + * @param {object} _node xml node to process + */ + loadFromXML(_node ) { + // Keep the node for later changing / reloading + this.template_node = _node; + + // Get the columns and rows tag + const rowsElems = et2_directChildrenByTagName(_node, "rows"); + const columnsElems = et2_directChildrenByTagName(_node, "columns"); + + if (rowsElems.length == 1 && columnsElems.length == 1) + { + const columns = columnsElems[0]; + const rows = rowsElems[0]; + const colData: ColumnEntry[] = []; + const rowData: RowEntry[] = []; + + // Fetch the column and row data + this._fetchRowColData(columns, rows, colData, rowData); + + // Initialize the cells + const cells = this._initCells(colData, rowData); + + // Create the widgets inside the cells and read the span values + this._fillCells(cells, columns, rows); + + // Expand the span values of the last cells + this._expandLastCells(cells); + + // Create the table rows + this.createTableFromCells(cells, colData, rowData); + } + else + { + throw("Error while parsing grid, none or multiple rows or columns tags!"); + } + } + + createTableFromCells(_cells, _colData: any[], _rowData: any[]) + { + this.managementArray = []; + this.cells = _cells; + this.colData = _colData; + this.rowData = _rowData; + + // Set the rowCount and columnCount variables + const h = this.rowCount = _cells.length; + const w = this.columnCount = (h > 0) ? _cells[0].length : 0; + + // Create the table rows. + for (let y = 0; y < h; y++) + { + let parent = this.tbody; + switch(this.rowData[y]["part"]) + { + case 'header': + if (!this.tbody.children().length && !this.tfoot.children().length) + { + parent = this.thead; + } + break; + case 'footer': + if (!this.tbody.children().length) + { + parent = this.tfoot; + } + break; + } + const tr = jQuery(document.createElement("tr")).appendTo(parent) + .addClass(this.rowData[y]["class"]); + + if (this.rowData[y].disabled) + { + tr.hide(); + } + + if (this.rowData[y].height != "auto") + { + tr.height(this.rowData[y].height); + } + + if (this.rowData[y].valign) + { + tr.attr("valign", this.rowData[y].valign); + } + + if(this.rowData[y].id) + { + tr.attr("id", this.rowData[y].id); + } + // Create the cells. x is incremented by the colSpan value of the + // cell. + for (let x = 0; x < w;) + { + // Fetch a cell from the cells + const cell = this._getCell(_cells, x, y); + + if (cell.td == null && cell.widget != null) + { + // Create the cell + const td = jQuery(document.createElement("td")).appendTo(tr) + .addClass(cell["class"]); + + if (cell.disabled) + { + td.hide(); + cell.widget.options.disabled = cell.disabled; + } + + if (cell.width != "auto") + { + td.width(cell.width); + } + + if (cell.align) + { + td.attr("align",cell.align); + } + + // Add the entry for the widget to the management array + this.managementArray.push({ + "cell": td[0], + "widget": cell.widget, + "disabled": cell.disabled + }); + + // Set the span values of the cell + const cs = (x == w - 1) ? w - x : Math.min(w - x, cell.colSpan); + const rs = (y == h - 1) ? h - y : Math.min(h - y, cell.rowSpan); + + // Set the col and row span values + if (cs > 1) { + td.attr("colspan", cs); + } + + if (rs > 1) { + td.attr("rowspan", rs); + } + + // Assign the td to the cell + for (let sx = x; sx < x + cs; sx++) + { + for (let sy = y; sy < y + rs; sy++) + { + this._getCell(_cells, sx, sy).td = td; + } + } + + x += cell.colSpan; + } + else + { + x++; + } + } + } + } + + getDOMNode(_sender?) : HTMLElement | null + { + // If the parent class functions are asking for the DOM-Node, return the + // outer table. + if (_sender == this || typeof _sender == 'undefined') + { + return this.wrapper != null ? this.wrapper[0] : this.table[0]; + } + + // Check whether the _sender object exists inside the management array + for (let i = 0; i < this.managementArray.length; i++) + { + if (this.managementArray[i].widget == _sender) + { + return this.managementArray[i].cell; + } + } + + return null; + } + + isInTree(_sender?:et2_widget) : boolean + { + let vis = true; + + if (typeof _sender != "undefined" && _sender != this) + { + vis = false; + + // Check whether the _sender object exists inside the management array + for (let i = 0; i < this.managementArray.length; i++) + { + if (this.managementArray[i].widget == _sender) + { + vis = !(typeof this.managementArray[i].disabled === 'boolean' ? + this.managementArray[i].disabled : + this.getArrayMgr("content").parseBoolExpression(this.managementArray[i].disabled) + ); + break; + } + } + } + + return super.isInTree(this, vis); + } + + /** + * Set the overflow attribute + * + * Grid needs special handling because HTML tables don't do overflow. We + * create a wrapper DIV to handle it. + * No value or default visible needs no wrapper, as table is always overflow visible. + * + * @param {string} _value Overflow value, must be a valid CSS overflow value, default 'visible' + */ + set_overflow(_value: string) + { + let wrapper = this.wrapper || this.table.parent('[id$="_grid_wrapper"]'); + + this.overflow = _value; + + if(wrapper.length == 0 && _value && _value !== 'visible') + { + this.wrapper = wrapper = this.table.wrap('
          ').parent(); + if(this.height) + { + wrapper.css('height', this.height); + } + } + wrapper.css('overflow', _value); + + if(wrapper.length && (!_value || _value === 'visible')) { + this.table.unwrap(); + } + } + + /** + * Change the content for the grid, and re-generate its contents. + * + * Changing the content does not allow changing the structure of the grid, + * as that is loaded from the template file. The rows and widgets inside + * will be re-created (including auto-repeat). + * + * @param {Object} _value New data for the grid + * @param {Object} [_value.content] New content + * @param {Object} [_value.sel_options] New select options + * @param {Object} [_value.readonlys] New read-only values + */ + set_value(_value : {content? : object, sel_options? : object, readonlys? : object}) + { + // Destroy children, empty grid + for(let i = 0; i < this.managementArray.length; i++) + { + const cell = this.managementArray[i]; + if(cell.widget) + { + cell.widget.destroy(); + } + } + this.managementArray = []; + this.thead.empty(); + this.tfoot.empty(); + this.tbody.empty(); + + // Update array managers + for(let key in _value) + { + this.getArrayMgr(key).data = _value[key]; + } + + // Rebuild grid + this.loadFromXML(this.template_node); + + // New widgets need to finish + let promises = []; + this.loadingFinished(promises); + } + + /** + * Sortable allows you to reorder grid rows using the mouse. + * The new order is returned as part of the value of the + * grid, in 'sort_order'. + * + * @param {boolean|function} sortable Callback or false to disable + */ + set_sortable(sortable: boolean | Function) + { + if(!sortable) + { + this.tbody.sortable("destroy"); + return; + } + + // Make sure rows have IDs, so sortable has something to return + jQuery('tr', this.tbody).each(function(index) { + const $this = jQuery(this); + + // Header does not participate in sorting + if($this.hasClass('th')) return; + + // If row doesn't have an ID, assign the index as ID + if(!$this.attr("id")) $this.attr("id", index); + }); + + const self = this; + + // Set up sortable + this.tbody.sortable({ + // Header does not participate in sorting + items: "tr:not(.th)", + distance: 15, + cancel: this.options.sortable_cancel, + placeholder: this.options.sortable_placeholder, + containment: this.options.sortable_containment, + connectWith: this.options.sortable_connectWith, + update: function(event, ui) { + self.egw().json(sortable,[self.tbody.sortable("toArray"), self.id], + null, + self, + true + ).sendRequest(); + }, + receive: function (event, ui) { + if (typeof self.options.sortable_recieveCallback == 'function') { + self.options.sortable_recieveCallback.call(self, event,ui); + } + }, + start: function (event, ui) { + if (typeof self.options.sortable_startCallback == 'function') { + self.options.sortable_startCallback.call(self, event,ui); + } + } + }); + } + + /** + * Override parent to apply actions on each row + * + * @param {array} actions [ {ID: attributes..}+] as for set_actions + */ + _link_actions(actions : object[]) + { + // Get the top level element for the tree + // @ts-ignore + let objectManager = window.egw_getAppObjectManager(true); + objectManager = objectManager.getObjectById(this.getInstanceManager().uniqueId,2) || objectManager; + let 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).getAOI(), + this._actionManager || objectManager.manager.getActionById(this.id) || objectManager.manager + )); + } + + // Delete all old objects + widget_object.clear(); + + // Go over the widget & add links - this is where we decide which actions are + // 'allowed' for this widget at this time + const action_links = this._get_action_links(actions); + + // Deal with each row in tbody, ignore action-wise rows in thead or tfooter for now + let i = 0, r = 0; + for(; i < this.rowData.length; i++) + { + if (this.rowData[i].part != 'body') continue; + const content = this.getArrayMgr('content').getEntry(i); + if(content) + { + // Add a new action object to the object manager + const row = jQuery('tr', this.tbody)[r]; + const aoi = new et2_action_object_impl(this, row).getAOI(); + const obj = widget_object.addObject(content.id || "row_" + r, aoi); + + // Set the data to the content so it's available for the action + obj.data = content; + + obj.updateActionLinks(action_links); + } + r++; + } + } + + /** + * Code for implementing et2_IDetachedDOM + * This doesn't need to be implemented. + * Individual widgets are detected and handled by the grid, but the interface is needed for this to happen + * + * @param {array} _attrs array to add further attributes to + */ + getDetachedAttributes(_attrs) + { + } + + getDetachedNodes() + { + return [this.getDOMNode()]; + } + + setDetachedAttributes(_nodes, _values) + { + } + + /** + * Generates nextmatch column name for headers in a grid + * + * Implemented here as default implementation in et2_externsion_nextmatch + * only considers children, but grid does NOT instanciate disabled rows as children. + * + * @return {string} + */ + _getColumnName() + { + const ids = []; + for(let r=0; r < this.cells.length; ++r) + { + const cols = this.cells[r]; + for(let c=0; c < cols.length; ++c) + { + if (cols[c].nm_id) ids.push(cols[c].nm_id); + } + } + return ids.join('_'); + } + + resize (_height) + { + if (typeof this.options != 'undefined' && _height + && typeof this.options.resize_ratio != 'undefined' && this.options.resize_ratio) + { + // apply the ratio + _height = (this.options.resize_ratio != '')? _height * this.options.resize_ratio: _height; + if (_height != 0) + { + if (this.wrapper) + { + this.wrapper.height(this.wrapper.height() + _height); + } + else + { + this.table.height(this.table.height() + _height ); + } + } + + } + } + + /** + * Get a dummy row object containing all widget of a row + * + * This is only a temp. solution until rows are implemented as eT2 containers and + * _sender.getParent() will return a real row container. + * + * @deprecated Not used? Remove this if you find something using it + * + * @param {et2_widget} _sender + * @returns {Array|undefined} + */ + getRow(_sender : et2_widget) : et2_widget + { + if (!_sender || !this.cells) return; + + for(let r=0; r < this.cells.length; ++r) + { + const row = this.cells[r]; + for(var c=0; c < row.length; ++c) + { + if (!row[c].widget) continue; + + let found = row[c].widget === _sender; + if (!found) row[c].widget.iterateOver(function(_widget) {if (_widget === _sender) found = true;}); + if (found) + { + // return a fake row object allowing to iterate over it's children + const row_obj: et2_widget = new et2_widget(this, {}); + for(var c=0; c < row.length; ++c) + { + if (row[c].widget) row_obj.addChild(row[c].widget); + } + row_obj.isInTree = jQuery.proxy(this.isInTree, this); + // we must not free the children! + row_obj.destroy = function(){ + // @ts-ignore + delete row_obj._children; + }; + return row_obj; + } + } + } + } + + /** + * Needed for the align interface, but we're not doing anything with it... + */ + get_align(): string { + return ""; + } +} +et2_register_widget(et2_grid, ["grid"]); + +interface ColumnEntry +{ + id? : string, + width: string | number, // 'auto' + class: string, // "", + align: string, // "", + span: string | number // "1", + disabled: boolean // false +} + +interface RowEntry +{ + id? : string + height: string | number, // "auto", + class: string, // "", + valign: string, // "top", + span: string | number, // "1", + disabled: boolean // false +} \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_groupbox.js b/api/js/etemplate/et2_widget_groupbox.js index 2a16f1d5b7..c62be1486b 100644 --- a/api/js/etemplate/et2_widget_groupbox.js +++ b/api/js/etemplate/et2_widget_groupbox.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - JS Groupbox object * @@ -9,56 +10,76 @@ * @copyright Nathan Gray 2012 * @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_baseWidget; + et2_core_baseWidget; */ - +var et2_core_widget_1 = require("./et2_core_widget"); +var et2_core_baseWidget_1 = require("./et2_core_baseWidget"); +var et2_core_inheritance_1 = require("./et2_core_inheritance"); /** * Class which implements the groupbox tag * * @augments et2_baseWidget */ -var et2_groupbox = (function(){ "use strict"; return et2_baseWidget.extend( -{ - /** - * Constructor - * - * @memberOf et2_groupbox - */ - init: function() { - this._super.apply(this, arguments); - - this.setDOMNode(document.createElement("fieldset")); - } -});}).call(this); -et2_register_widget(et2_groupbox, ["groupbox"]); - +var et2_groupbox = /** @class */ (function (_super) { + __extends(et2_groupbox, _super); + /** + * Constructor + * + * @memberOf et2_groupbox + */ + function et2_groupbox(_parent, _attrs, _child) { + var _this = + // Call the inherited constructor + _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_groupbox._attributes, _child || {})) || this; + _this.setDOMNode(document.createElement("fieldset")); + return _this; + } + return et2_groupbox; +}(et2_core_baseWidget_1.et2_baseWidget)); +et2_core_widget_1.et2_register_widget(et2_groupbox, ["groupbox"]); /** * @augments et2_baseWidget */ -var et2_groupbox_legend = (function(){ "use strict"; return et2_baseWidget.extend( -{ - attributes: { - "label": { - "name": "Label", - "type": "string", - "default": "", - "description": "Label for group box", - "translate" : true - } - }, - - /** - * Constructor - * - * @memberOf et2_groupbox_legend - */ - init: function() { - this._super.apply(this, arguments); - - var legend = jQuery(document.createElement("legend")).text(this.options.label); - this.setDOMNode(legend[0]); - } -});}).call(this); -et2_register_widget(et2_groupbox_legend, ["caption"]); +var et2_groupbox_legend = /** @class */ (function (_super) { + __extends(et2_groupbox_legend, _super); + /** + * Constructor + * + * @memberOf et2_groupbox_legend + */ + function et2_groupbox_legend(_parent, _attrs, _child) { + var _this = + // Call the inherited constructor + _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_groupbox_legend._attributes, _child || {})) || this; + var legend = jQuery(document.createElement("legend")).text(_this.options.label); + _this.setDOMNode(legend[0]); + return _this; + } + et2_groupbox_legend._attributes = { + "label": { + "name": "Label", + "type": "string", + "default": "", + "description": "Label for group box", + "translate": true + } + }; + return et2_groupbox_legend; +}(et2_core_baseWidget_1.et2_baseWidget)); +et2_core_widget_1.et2_register_widget(et2_groupbox_legend, ["caption"]); +//# sourceMappingURL=et2_widget_groupbox.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_groupbox.ts b/api/js/etemplate/et2_widget_groupbox.ts new file mode 100644 index 0000000000..4182444340 --- /dev/null +++ b/api/js/etemplate/et2_widget_groupbox.ts @@ -0,0 +1,72 @@ +/** + * EGroupware eTemplate2 - JS Groupbox object + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage api + * @link http://www.egroupware.org + * @author Nathan Gray + * @copyright Nathan Gray 2012 + * @version $Id$ + */ + +/*egw:uses + et2_core_baseWidget; +*/ + +import {et2_register_widget, WidgetConfig} from "./et2_core_widget"; +import {et2_baseWidget} from "./et2_core_baseWidget"; +import {ClassWithAttributes} from "./et2_core_inheritance"; + +/** + * Class which implements the groupbox tag + * + * @augments et2_baseWidget + */ +class et2_groupbox extends et2_baseWidget +{ + /** + * Constructor + * + * @memberOf et2_groupbox + */ + constructor(_parent, _attrs? : WidgetConfig, _child? : object) + { + // Call the inherited constructor + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_groupbox._attributes, _child || {})); + + this.setDOMNode(document.createElement("fieldset")); + } +} +et2_register_widget(et2_groupbox, ["groupbox"]); + +/** + * @augments et2_baseWidget + */ +class et2_groupbox_legend extends et2_baseWidget +{ + static readonly _attributes : any = { + "label": { + "name": "Label", + "type": "string", + "default": "", + "description": "Label for group box", + "translate" : true + } + }; + + /** + * Constructor + * + * @memberOf et2_groupbox_legend + */ + constructor(_parent, _attrs? : WidgetConfig, _child? : object) + { + // Call the inherited constructor + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_groupbox_legend._attributes, _child || {})); + let legend = jQuery(document.createElement("legend")).text(this.options.label); + this.setDOMNode(legend[0]); + } +} +et2_register_widget(et2_groupbox_legend, ["caption"]); + diff --git a/api/js/etemplate/et2_widget_hbox.js b/api/js/etemplate/et2_widget_hbox.js index f3055b7394..e7ac66297e 100644 --- a/api/js/etemplate/et2_widget_hbox.js +++ b/api/js/etemplate/et2_widget_hbox.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - JS HBox object * @@ -6,182 +7,166 @@ * @subpackage api * @link http://www.egroupware.org * @author Andreas Stöckel - * @copyright Stylite 2011 - * @version $Id: et2_box.js 36147 2011-08-16 13:12:39Z igel457 $ */ - +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 - /vendor/bower-asset/jquery/dist/jquery.js; - et2_core_baseWidget; + /vendor/bower-asset/jquery/dist/jquery.js; + et2_core_baseWidget; */ - +var et2_core_inheritance_1 = require("./et2_core_inheritance"); +var et2_core_widget_1 = require("./et2_core_widget"); +var et2_core_baseWidget_1 = require("./et2_core_baseWidget"); /** * Class which implements hbox tag * * @augments et2_baseWidget */ -var et2_hbox = (function(){ "use strict"; return et2_baseWidget.extend( -{ - createNamespace: true, - - /** - * Constructor - * - * @memberOf et2_hbox - */ - init: function() { - this._super.apply(this, arguments); - - this.alignData = { - "hasAlign": false, - "hasLeft": false, - "hasCenter": false, - "hasRight": false, - "lastAlign": "left" - }; - - this.leftDiv = null; - this.rightDiv = null; - - this.div = jQuery(document.createElement("div")) - .addClass("et2_" + this._type) - .addClass("et2_box_widget"); - - this.setDOMNode(this.div[0]); - }, - - _buildAlignCells: function() { - if (this.alignData.hasAlign) - { - // Check whether we have more than one type of align - var mto = (this.alignData.hasLeft && this.alignData.hasRight) || - (this.alignData.hasLeft && this.alignData.hasCenter) || - (this.alignData.hasCenter && this.alignData.hasRight); - - if (!mto) - { - // If there is only one type of align, we simply have to set - // the align of the top container - if (this.alignData.lastAlign != "left") - { - this.div.addClass("et2_hbox_al_" + this.alignData.lastAlign); - } - } - else - { - // Create an additional container for elements with align type - // "right" - if (this.alignData.hasRight) - { - this.rightDiv = jQuery(document.createElement("div")) - .addClass("et2_hbox_right") - .appendTo(this.div); - } - - // Create an additional container for elements with align type - // left, as the top container is used for the centered elements - if (this.alignData.hasCenter) - { - // Create the left div if an element is centered - this.leftDiv = jQuery(document.createElement("div")) - .addClass("et2_hbox_left") - .appendTo(this.div); - - this.div.addClass("et2_hbox_al_center"); - } - } - } - }, - - /** - * The overwritten loadFromXML function checks whether any child element has - * a special align value. - * - * @param {object} _node - */ - loadFromXML: function(_node) { - // Check whether any child node has an alignment tag - et2_filteredNodeIterator(_node, function(_node) { - var align = _node.getAttribute("align"); - - if (!align) - { - align = "left"; - } - - if (align != "left") - { - this.alignData.hasAlign = true; - } - - this.alignData.lastAlign = align; - - switch (align) - { - case "left": - this.alignData.hasLeft = true; - break; - case "right": - this.alignData.hasRight = true; - break; - case "center": - this.alignData.hasCenter = true; - break; - } - }, this); - - // Build the align cells - this._buildAlignCells(); - - // Load the nodes as usual - this._super.apply(this, arguments); - }, - - assign: function(_obj) { - // Copy the align data and the cells from the object which should be - // assigned - this.alignData = et2_cloneObject(_obj.alignData); - this._buildAlignCells(); - - // Call the inherited assign function - this._super.apply(this, arguments); - }, - - getDOMNode: function(_sender) { - // Return a special align container if this hbox needs it - if (_sender != this && this.alignData.hasAlign) - { - // Check whether we've create a special container for the widget - var align = (_sender.implements(et2_IAligned) ? - _sender.get_align() : "left"); - - if (align == "left" && this.leftDiv != null) - { - return this.leftDiv[0]; - } - - if (align == "right" && this.rightDiv != null) - { - return this.rightDiv[0]; - } - } - - // Normally simply return the hbox-div - return this._super.apply(this, arguments); - }, - - /** - * Tables added to the root node need to be inline instead of blocks - * - * @param {et2_widget} child child-widget to add - */ - addChild: function(child) { - this._super.apply(this, arguments); - if(child.instanceOf && child.instanceOf(et2_grid) || child._type == 'et2_grid') - { - jQuery(child.getDOMNode(child)).css("display", "inline-table"); - } - } -});}).call(this); -et2_register_widget(et2_hbox, ["hbox"]); - +var et2_hbox = /** @class */ (function (_super) { + __extends(et2_hbox, _super); + /** + * Constructor + * + * @memberOf et2_hbox + */ + function et2_hbox(_parent, _attrs, _child) { + var _this = _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_hbox._attributes, _child || {})) || this; + _this.alignData = { + "hasAlign": false, + "hasLeft": false, + "hasCenter": false, + "hasRight": false, + "lastAlign": "left" + }; + _this.leftDiv = null; + _this.rightDiv = null; + _this.div = null; + _this.leftDiv = null; + _this.rightDiv = null; + _this.div = jQuery(document.createElement("div")) + .addClass("et2_" + _super.prototype.getType.call(_this)) + .addClass("et2_box_widget"); + _super.prototype.setDOMNode.call(_this, _this.div[0]); + return _this; + } + et2_hbox.prototype._createNamespace = function () { + return true; + }; + et2_hbox.prototype._buildAlignCells = function () { + if (this.alignData.hasAlign) { + // Check whether we have more than one type of align + var mto = (this.alignData.hasLeft && this.alignData.hasRight) || + (this.alignData.hasLeft && this.alignData.hasCenter) || + (this.alignData.hasCenter && this.alignData.hasRight); + if (!mto) { + // If there is only one type of align, we simply have to set + // the align of the top container + if (this.alignData.lastAlign != "left") { + this.div.addClass("et2_hbox_al_" + this.alignData.lastAlign); + } + } + else { + // Create an additional container for elements with align type + // "right" + if (this.alignData.hasRight) { + this.rightDiv = jQuery(document.createElement("div")) + .addClass("et2_hbox_right") + .appendTo(this.div); + } + // Create an additional container for elements with align type + // left, as the top container is used for the centered elements + if (this.alignData.hasCenter) { + // Create the left div if an element is centered + this.leftDiv = jQuery(document.createElement("div")) + .addClass("et2_hbox_left") + .appendTo(this.div); + this.div.addClass("et2_hbox_al_center"); + } + } + } + }; + /** + * The overwritten loadFromXML function checks whether any child element has + * a special align value. + * + * @param {object} _node + */ + et2_hbox.prototype.loadFromXML = function (_node) { + // Check whether any child node has an alignment tag + et2_filteredNodeIterator(_node, function (_node) { + var align = _node.getAttribute("align"); + if (!align) { + align = "left"; + } + if (align != "left") { + this.alignData.hasAlign = true; + } + this.alignData.lastAlign = align; + switch (align) { + case "left": + this.alignData.hasLeft = true; + break; + case "right": + this.alignData.hasRight = true; + break; + case "center": + this.alignData.hasCenter = true; + break; + } + }, this); + // Build the align cells + this._buildAlignCells(); + // Load the nodes as usual + _super.prototype.loadFromXML.call(this, _node); + }; + et2_hbox.prototype.assign = function (_obj) { + // Copy the align data and the cells from the object which should be + // assigned + this.alignData = et2_cloneObject(_obj.alignData); + this._buildAlignCells(); + // Call the inherited assign function + _super.prototype.assign.call(this, _obj); + }; + et2_hbox.prototype.getDOMNode = function (_sender) { + // Return a special align container if this hbox needs it + if (_sender != this && this.alignData.hasAlign) { + // Check whether we've create a special container for the widget + var align = (_sender.implements(et2_IAligned) ? + _sender.get_align() : "left"); + if (align == "left" && this.leftDiv != null) { + return this.leftDiv[0]; + } + if (align == "right" && this.rightDiv != null) { + return this.rightDiv[0]; + } + } + // Normally simply return the hbox-div + return _super.prototype.getDOMNode.call(this, _sender); + }; + /** + * Tables added to the root node need to be inline instead of blocks + * + * @param {et2_widget} child child-widget to add + */ + et2_hbox.prototype.addChild = function (child) { + _super.prototype.addChild.call(this, child); + if (child.instanceOf && child.instanceOf(et2_grid) && this.isAttached() || child._type == 'et2_grid' && this.isAttached()) { + jQuery(child.getDOMNode(child)).css("display", "inline-table"); + } + }; + return et2_hbox; +}(et2_core_baseWidget_1.et2_baseWidget)); +et2_core_widget_1.et2_register_widget(et2_hbox, ["hbox"]); +//# sourceMappingURL=et2_widget_hbox.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_hbox.ts b/api/js/etemplate/et2_widget_hbox.ts new file mode 100644 index 0000000000..944564f68c --- /dev/null +++ b/api/js/etemplate/et2_widget_hbox.ts @@ -0,0 +1,197 @@ +/** + * EGroupware eTemplate2 - JS HBox object + * + * @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 + /vendor/bower-asset/jquery/dist/jquery.js; + et2_core_baseWidget; +*/ + +import {ClassWithAttributes} from "./et2_core_inheritance"; +import {et2_register_widget, WidgetConfig} from "./et2_core_widget"; +import {et2_baseWidget} from "./et2_core_baseWidget"; + +/** + * Class which implements hbox tag + * + * @augments et2_baseWidget + */ +class et2_hbox extends et2_baseWidget +{ + alignData : any = { + "hasAlign": false, + "hasLeft": false, + "hasCenter": false, + "hasRight": false, + "lastAlign": "left" + }; + leftDiv : JQuery = null; + rightDiv : JQuery = null; + div : JQuery = null; + + /** + * Constructor + * + * @memberOf et2_hbox + */ + constructor(_parent?, _attrs? : WidgetConfig, _child? : object) + { + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_hbox._attributes, _child || {})); + + this.leftDiv = null; + this.rightDiv = null; + + this.div = jQuery(document.createElement("div")) + .addClass("et2_" + super.getType()) + .addClass("et2_box_widget"); + + super.setDOMNode(this.div[0]); + } + + _createNamespace() : boolean + { + return true; + } + + _buildAlignCells() { + if (this.alignData.hasAlign) + { + // Check whether we have more than one type of align + let mto = (this.alignData.hasLeft && this.alignData.hasRight) || + (this.alignData.hasLeft && this.alignData.hasCenter) || + (this.alignData.hasCenter && this.alignData.hasRight); + + if (!mto) + { + // If there is only one type of align, we simply have to set + // the align of the top container + if (this.alignData.lastAlign != "left") + { + this.div.addClass("et2_hbox_al_" + this.alignData.lastAlign); + } + } + else + { + // Create an additional container for elements with align type + // "right" + if (this.alignData.hasRight) + { + this.rightDiv = jQuery(document.createElement("div")) + .addClass("et2_hbox_right") + .appendTo(this.div); + } + + // Create an additional container for elements with align type + // left, as the top container is used for the centered elements + if (this.alignData.hasCenter) + { + // Create the left div if an element is centered + this.leftDiv = jQuery(document.createElement("div")) + .addClass("et2_hbox_left") + .appendTo(this.div); + + this.div.addClass("et2_hbox_al_center"); + } + } + } + } + + /** + * The overwritten loadFromXML function checks whether any child element has + * a special align value. + * + * @param {object} _node + */ + loadFromXML(_node) { + // Check whether any child node has an alignment tag + et2_filteredNodeIterator(_node, function(_node) { + let align = _node.getAttribute("align"); + + if (!align) + { + align = "left"; + } + + if (align != "left") + { + this.alignData.hasAlign = true; + } + + this.alignData.lastAlign = align; + + switch (align) + { + case "left": + this.alignData.hasLeft = true; + break; + case "right": + this.alignData.hasRight = true; + break; + case "center": + this.alignData.hasCenter = true; + break; + } + }, this); + + // Build the align cells + this._buildAlignCells(); + + // Load the nodes as usual + super.loadFromXML(_node); + } + + assign(_obj) { + // Copy the align data and the cells from the object which should be + // assigned + this.alignData = et2_cloneObject(_obj.alignData); + this._buildAlignCells(); + + // Call the inherited assign function + super.assign(_obj); + } + + getDOMNode(_sender) { + // Return a special align container if this hbox needs it + if (_sender != this && this.alignData.hasAlign) + { + // Check whether we've create a special container for the widget + let align = (_sender.implements(et2_IAligned) ? + _sender.get_align() : "left"); + + if (align == "left" && this.leftDiv != null) + { + return this.leftDiv[0]; + } + + if (align == "right" && this.rightDiv != null) + { + return this.rightDiv[0]; + } + } + + // Normally simply return the hbox-div + return super.getDOMNode(_sender); + } + + /** + * Tables added to the root node need to be inline instead of blocks + * + * @param {et2_widget} child child-widget to add + */ + addChild(child) { + super.addChild(child); + if(child.instanceOf && child.instanceOf(et2_grid) && this.isAttached() || child._type == 'et2_grid' && this.isAttached()) + { + jQuery(child.getDOMNode(child)).css("display", "inline-table"); + } + } +} +et2_register_widget(et2_hbox, ["hbox"]); + + diff --git a/api/js/etemplate/et2_widget_historylog.js b/api/js/etemplate/et2_widget_historylog.js index aa2f345abe..b96f98c6e5 100644 --- a/api/js/etemplate/et2_widget_historylog.js +++ b/api/js/etemplate/et2_widget_historylog.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - JS History log * @@ -8,16 +9,27 @@ * @author Nathan Gray * @copyright 2012 Nathan Gray */ - -/*egw:uses - /vendor/bower-asset/jquery/dist/jquery.js; - /vendor/bower-asset/jquery-ui/jquery-ui.js; - et2_core_valueWidget; - - // Include the grid classes - et2_dataview; -*/ - +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 }); +var et2_core_widget_1 = require("./et2_core_widget"); +var et2_core_inheritance_1 = require("./et2_core_inheritance"); +var et2_core_valueWidget_1 = require("./et2_core_valueWidget"); +var et2_dataview_1 = require("./et2_dataview"); +var et2_dataview_model_columns_1 = require("./et2_dataview_model_columns"); +var et2_dataview_controller_1 = require("./et2_dataview_controller"); +var et2_widget_diff_1 = require("./et2_widget_diff"); /** * eTemplate history log widget displays a list of changes to the current record. * The widget is encapsulated, and only needs the record's ID, and a map of @@ -28,692 +40,554 @@ * * @augments et2_valueWidget */ -var et2_historylog = (function(){ "use strict"; return et2_valueWidget.extend([et2_IDataProvider,et2_IResizeable], -{ - createNamespace: true, - attributes: { - "value": { - "name": "Value", - "type": "any", - "description": "Object {app: ..., id: ..., status-widgets: {}} where status-widgets is a map of fields to widgets used to display those fields" - }, - "status_id":{ - "name": "status_id", - "type": "string", - "default": "status", - "description": "The history widget is traditionally named 'status'. If you name another widget in the same template 'status', you can use this attribute to re-name the history widget. " - }, - "columns": { - "name": "columns", - "type": "string", - "default": "user_ts,owner,status,new_value,old_value", - "description": "Columns to display. Default is user_ts,owner,status,new_value,old_value" - }, - "get_rows": { - "name": "get_rows", - "type": "string", - "default": "EGroupware\\Api\\Storage\\History::get_rows", - "description": "Method to get rows" - } - }, - - legacyOptions: ["status_id"], - columns: [ - {'id': 'user_ts', caption: 'Date', 'width': '120px', widget_type: 'date-time'}, - {'id': 'owner', caption: 'User', 'width': '150px', widget_type: 'select-account'}, - {'id': 'status', caption: 'Changed', 'width': '120px', widget_type: 'select'}, - {'id': 'new_value', caption: 'New Value', 'width': '50%'}, - {'id': 'old_value', caption: 'Old Value', 'width': '50%'} - ], - - TIMESTAMP: 0, OWNER: 1, FIELD: 2, NEW_VALUE: 3, OLD_VALUE: 4, - - /** - * Constructor - * - * @memberOf et2_historylog - */ - init: function() { - this._super.apply(this, arguments); - this.div = jQuery(document.createElement("div")) - .addClass("et2_historylog"); - - this.innerDiv = jQuery(document.createElement("div")) - .appendTo(this.div); - }, - - set_status_id: function(_new_id) { - this.options.status_id = _new_id; - }, - - doLoadingFinished: function() { - this._super.apply(this, arguments); - - // Find the tab widget, if there is one - var tabs = this; - do { - tabs = tabs._parent; - } while (tabs != this.getRoot() && tabs._type != 'tabbox'); - if(tabs != this.getRoot()) - { - // Find the tab index - for(var i = 0; i < tabs.tabData.length; i++) - { - // Find the tab - if(tabs.tabData[i].contentDiv.has(this.div).length) - { - // Bind the action to when the tab is selected - var handler = function(e) { - e.data.div.unbind("click.history"); - // Bind on click tap, because we need to update history size - // after a rezise happend and history log was not the active tab - e.data.div.bind("click.history",{"history": e.data.history, div: tabs.tabData[i].flagDiv}, function(e){ - if(e.data.history && e.data.history.dynheight) - { - e.data.history.dynheight.update(function(_w, _h) - { - e.data.history.dataview.resize(_w, _h); - }); - } - }); - - if (typeof e.data.history.dataview == "undefined") - { - e.data.history.finishInit(); - if(e.data.history.dynheight) - { - e.data.history.dynheight.update(function(_w, _h) { - e.data.history.dataview.resize(_w, _h); - }); - } - } - - }; - tabs.tabData[i].flagDiv.bind("click.history",{"history": this, div: tabs.tabData[i].flagDiv}, handler); - - // Display if history tab is selected - if(i == tabs.get_active_tab() && typeof this.dataview == 'undefined') - { - tabs.tabData[i].flagDiv.trigger("click.history"); - } - break; - } - } - } - else - { - this.finishInit(); - } - }, - - /** - * Finish initialization which was skipped until tab was selected - */ - finishInit: function() { - // No point with no ID - if(!this.options.value || !this.options.value.id) - { - return; - } - this._filters = { - record_id: this.options.value.id, - appname: this.options.value.app, - get_rows: this.options.get_rows - }; - - // Warn if status_id is the same as history id, that causes overlap and missing labels - if(this.options.status_id === this.id) - { - this.egw().debug("warn", "status_id attribute should not be the same as historylog ID"); - } - var _columns = typeof this.options.columns === "string" ? - this.options.columns.split(',') : this.options.columns; - - // Create the dynheight component which dynamically scales the inner - // container. - this.div.parentsUntil('.et2_tabs').height('100%'); - var parent = this.get_tab_info(); - this.dynheight = new et2_dynheight(parent ? parent.contentDiv : this.div.parent(), - this.innerDiv, 250 - ); - - // Create the outer grid container - this.dataview = new et2_dataview(this.innerDiv, this.egw()); - var dataview_columns = []; - var _columns = typeof this.options.columns === "string" ? - this.options.columns.split(',') : this.options.columns; - for (var i = 0; i < this.columns.length; i++) - { - dataview_columns[i] = { - "id": this.columns[i].id, - "caption": this.columns[i].caption, - "width":this.columns[i].width, - "visibility":_columns.indexOf(this.columns[i].id) < 0 ? - ET2_COL_VISIBILITY_INVISIBLE : ET2_COL_VISIBILITY_VISIBLE - }; - } - this.dataview.setColumns(dataview_columns); - - // Create widgets for columns that stay the same, and set up varying widgets - this.createWidgets(); - - // Create the gridview controller - var linkCallback = function() {}; - this.controller = new et2_dataview_controller(null, this.dataview.grid, - this, this.rowCallback, linkCallback, this, - null - ); - - var total = typeof this.options.value.total !== "undefined" ? - this.options.value.total : 0; - - // This triggers an invalidate, which updates the grid - this.dataview.grid.setTotalCount(total); - - // Insert any data sent from server, so invalidate finds data already - if(this.options.value.rows && this.options.value.num_rows) - { - this.controller.loadInitialData( - this.options.value.dataStorePrefix, - this.options.value.row_id, - this.options.value.rows - ); - // Remove, to prevent duplication - delete this.options.value.rows; - // This triggers an invalidate, which updates the grid - this.dataview.grid.setTotalCount(total); - } - else - { - // Trigger the initial update - this.controller.update(); - } - - // Write something inside the column headers - for (var i = 0; i < this.columns.length; i++) - { - jQuery(this.dataview.getHeaderContainerNode(i)).text(this.columns[i].caption); - } - - // Register a resize callback - var self = this; - jQuery(window).on('resize.' +this.options.value.app + this.options.value.id, function() { - if (self && typeof self.dynheight != 'undefined') self.dynheight.update(function(_w, _h) { - self.dataview.resize(_w, _h); - }); - }); - }, - - /** - * Destroys all - */ - destroy: function() { - // Unbind, if bound - if(this.options.value && !this.options.value.id) - { - jQuery(window).off('.' +this.options.value.app + this.options.value.id); - } - - // Free the widgets - for(var i = 0; i < this.columns.length; i++) - { - if(this.columns[i].widget) this.columns[i].widget.destroy(); - } - for(var key in this.fields) - { - this.fields[key].widget.destroy(); - } - if(this.diff) this.diff.widget.destroy(); - - // Free the grid components - if(this.dataview) this.dataview.free(); - if(this.rowProvider) this.rowProvider.free(); - if(this.controller) this.controller.free(); - if(this.dynheight) this.dynheight.free(); - - this._super.apply(this, arguments); - }, - - /** - * Create all needed widgets for new / old values - */ - createWidgets: function() { - - // Constant widgets - first 3 columns - for(var i = 0; i < this.columns.length; i++) - { - if(this.columns[i].widget_type) - { - // Status ID is allowed to be remapped to something else. Only affects the widget ID though - var attrs = {'readonly': true, 'id': (i == this.FIELD ? this.options.status_id : this.columns[i].id)}; - this.columns[i].widget = et2_createWidget(this.columns[i].widget_type, attrs, this); - this.columns[i].widget.transformAttributes(attrs); - this.columns[i].nodes = jQuery(this.columns[i].widget.getDetachedNodes()); - } - } - - // Add in handling for links - if(typeof this.options.value['status-widgets']['~link~'] == 'undefined') - { - this.columns[this.FIELD].widget.optionValues['~link~'] = this.egw().lang('link'); - this.options.value['status-widgets']['~link~'] = 'link'; - } - - // Add in handling for files - if(typeof this.options.value['status-widgets']['~file~'] == 'undefined') - { - this.columns[this.FIELD].widget.optionValues['~file~'] = this.egw().lang('File'); - this.options.value['status-widgets']['~file~'] = 'vfs'; - } - - // Add in handling for user-agent & action - if(typeof this.options.value['status-widgets']['user_agent_action'] == 'undefined') - { - this.columns[this.FIELD].widget.optionValues['user_agent_action'] = this.egw().lang('User-agent & action'); - } - - // Per-field widgets - new value & old value - this.fields = {}; - - var labels = this.columns[this.FIELD].widget.optionValues; - - // Custom fields - Need to create one that's all read-only for proper display - var cf_widget = et2_createWidget('customfields', {'readonly':true}, this); - cf_widget.loadFields(); - // Override this or it may damage the real values - cf_widget.getValue = function() {return null;}; - for(var key in cf_widget.widgets) - { - // Add label - labels[cf_widget.prefix + key] = cf_widget.options.customfields[key].label; - - // If it doesn't support detached nodes, just treat it as text - if(cf_widget.widgets[key].getDetachedNodes) - { - var nodes = cf_widget.widgets[key].getDetachedNodes(); - for(var i = 0; i < nodes.length; i++) - { - if(nodes[i] == null) nodes.splice(i,1); - } - - // Save to use for each row - this.fields[cf_widget.prefix + key] = { - attrs: cf_widget.widgets[key].options, - widget: cf_widget.widgets[key], - nodes: jQuery(nodes) - }; - } - } - // Add all cf labels - this.columns[this.FIELD].widget.set_select_options(labels); - - // From app - for(var key in this.options.value['status-widgets']) - { - var attrs = jQuery.extend({'readonly': true, 'id': key}, this.getArrayMgr('modifications').getEntry(key)); - var field = attrs.type || this.options.value['status-widgets'][key]; - var options = null; - var widget = this._create_widget(key, field, attrs, options); - if(widget === null) - { - continue; - } - if(widget.instanceOf(et2_selectbox)) widget.options.multiple = true; - widget.transformAttributes(attrs); - - // Save to use for each row - var nodes = widget._children.length ? [] : jQuery(widget.getDetachedNodes()); - for(var i = 0; i < widget._children.length; i++) - { - nodes.push(jQuery(widget._children[i].getDetachedNodes())); - } - this.fields[key] = { - attrs: attrs, - widget: widget, - nodes: nodes - }; - } - // Widget for text diffs - var diff = et2_createWidget('diff', {}, this); - this.diff = { - widget: diff, - nodes: jQuery(diff.getDetachedNodes()) - }; - }, - - _create_widget(key, field, attrs, options) - { - var widget = null; - - // If field has multiple parts (is object) and isn't an obvious select box - if(typeof field === 'object') - { - // Check for multi-part statuses needing multiple widgets - var need_box = false;//!this.getArrayMgr('sel_options').getEntry(key); - for(var j in field) - { - // Require widget to be a widget, to avoid invalid widgets - // (and template, which is a widget and an infolog todo status) - if(et2_registry[field[j]] && ['template'].indexOf(field[j]) < 0)// && (et2_registry[field[j]].prototype.instanceOf(et2_valueWidget)) - { - need_box = true; - break; - } - } - - if(need_box) - { - // Multi-part value needs multiple widgets - widget = et2_createWidget('vbox', attrs, this); - for(var i in field) - { - var type = field[i]; - var child_attrs = jQuery.extend({}, attrs); - if(typeof type === 'object') - { - child_attrs['select_options'] = field[i]; - type = 'select'; - } - else - { - delete child_attrs['select_options']; - } - child_attrs.id = i; - var child = this._create_widget(i, type, child_attrs, options) - widget.addChild(child); - child.transformAttributes(child_attrs); - } - } - else - { - attrs['select_options'] = field; - } - } - // Check for options after the type, ex: link-entry:infolog - else if (field.indexOf(':') > 0) - { - var options = field.split(':'); - field = options.shift(); - } - - if(widget === null) - { - widget = et2_createWidget(typeof field === 'string' ? field : 'select', attrs, this); - } - - if(!widget.instanceOf(et2_IDetachedDOM)) - { - this.egw().debug("warn", this, "Invalid widget " + field + " for " + key + ". Status widgets must implement et2_IDetachedDOM."); - return null; - } - - // Parse / set legacy options - if(options) - { - var mgr = this.getArrayMgr("content"); - for(var i = 0; i < options.length && i < widget.legacyOptions.length; i++) - { - // Not set - if(options[i] === "") continue; - - var attr = widget.attributes[widget.legacyOptions[i]]; - var attrValue = options[i]; - - // 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); - } - attrs[widget.legacyOptions[i]] = attrValue; - if(typeof widget['set_'+widget.legacyOptions[i]] === 'function') - { - widget['set_'+widget.legacyOptions[i]].call(widget, attrValue); - } - else - { - widget.options[widget.legacyOptions[i]] = attrValue; - } - } - } - return widget; - }, - - getDOMNode: function(_sender) { - if (_sender == this) - { - return this.div[0]; - } - - for (var i = 0; i < this.columns.length; i++) - { - if (_sender == this.columns[i].widget) - { - return this.dataview.getHeaderContainerNode(i); - } - } - return null; - }, - - - dataFetch: function (_queriedRange, _callback, _context) { - // Skip getting data if there's no ID - if(!this.value.id) return; - - // Set num_rows to fetch via nextmatch - if ( this.options.value['num_rows'] ) - _queriedRange['num_rows'] = this.options.value['num_rows']; - - var historylog = this; - // Pass the fetch call to the API - this.egw().dataFetch( - this.getInstanceManager().etemplate_exec_id, - _queriedRange, - this._filters, - this.id, - function(_response) { - _callback.call(this,_response); - // This seems to prevent unwanted scrollbars - historylog.div.hide(); - window.setTimeout(function() { - historylog.div.show(); - }.bind(historylog),100); - }, - _context, - [] - ); - }, - - - // Needed by interface - dataRegisterUID: function (_uid, _callback, _context) { - this.egw().dataRegisterUID(_uid, _callback, _context, this.getInstanceManager().etemplate_exec_id, - this.id); - }, - - dataUnregisterUID: function (_uid, _callback, _context) { - // Needed by interface - }, - - /** - * The row callback gets called by the gridview controller whenever - * the actual DOM-Nodes for a node with the given data have to be - * created. - * - * @param {type} _data - * @param {type} _row - * @param {type} _idx - * @param {type} _entry - */ - rowCallback: function(_data, _row, _idx, _entry) { - var tr = _row.getDOMNode(); - jQuery(tr).attr("valign","top"); - - var row = this.dataview.rowProvider.getPrototype("default"); - var self = this; - jQuery("div", row).each(function (i) { - var nodes = []; - var widget = self.columns[i].widget; - var value = _data[self.columns[i].id]; - if(self.OWNER === i && _data['share_email']) - { - // Show share email instead of owner - widget = undefined; - value = _data['share_email']; - } - // Get widget from list, unless it needs a diff widget - if(typeof widget == 'undefined' && typeof self.fields[_data.status] != 'undefined' && ( - i < self.NEW_VALUE || - i >= self.NEW_VALUE && ( - self.fields[_data.status].nodes || !self._needsDiffWidget(_data['status'], _data[self.columns[self.OLD_VALUE].id]) - ) - )) - { - widget = self.fields[_data.status].widget; - if(!widget._children.length) - { - nodes = self.fields[_data.status].nodes.clone(); - } - for(var j = 0; j < widget._children.length; j++) - { - nodes.push(self.fields[_data.status].nodes[j].clone()); - if(widget._children[j].instanceOf(et2_diff)) - { - self._spanValueColumns(jQuery(this)); - } - } - } - else if (widget) - { - nodes = self.columns[i].nodes.clone(); - } - else if (( - // Already parsed & cached - typeof _data[self.columns[self.NEW_VALUE].id] == "object" && - typeof _data[self.columns[self.NEW_VALUE].id] != "undefined" && - _data[self.columns[self.NEW_VALUE].id] !== null) || // typeof null === 'object' - // Large old value - self._needsDiffWidget(_data['status'], _data[self.columns[self.OLD_VALUE].id]) || - // Large new value - self._needsDiffWidget(_data['status'], _data[self.columns[self.NEW_VALUE].id])) - { - // Large text value - span both columns, and show a nice diff - var jthis = jQuery(this); - if(i === self.NEW_VALUE) - { - // Diff widget - widget = self.diff.widget; - nodes = self.diff.nodes.clone(); - - if(widget) widget.setDetachedAttributes(nodes, { - value: value, - label: jthis.parents("td").prev().text() - }); - - self._spanValueColumns(jthis); - } - } - else - { - // No widget fallback - display actual value - nodes = jQuery('').text(value === null ? '' : value); - } - if(widget) - { - if(widget._children.length) - { - // Multi-part values - var box = jQuery(widget.getDOMNode()).clone(); - for(var j = 0; j < widget._children.length; j++) - { - var id = widget._children[j].id; - var widget_value = value ? value[id] || "" : ""; - widget._children[j].setDetachedAttributes(nodes[j], {value:widget_value}); - box.append(nodes[j]); - } - nodes = box; - } - else - { - widget.setDetachedAttributes(nodes, {value:value}); - } - } - jQuery(this).append(nodes); - }); - jQuery(tr).append(row.children()); - - return tr; - }, - - /** - * How to tell if the row needs a diff widget or not - * - * @param {string} columnName - * @param {string} value - * @returns {Boolean} - */ - _needsDiffWidget: function(columnName, value) { - if(typeof value !== "string" && value) - { - this.egw().debug("warn", "Crazy diff value", value); - return false; - } - return value === '***diff***'; - }, - - /** - * Make a single row's new value cell span across both new value and old value - * columns. Used for diff widget. - * - * @param {jQuery} row jQuery wrapped row node - */ - _spanValueColumns: function(row) - { - // Stretch column 4 - row.parents("td").attr("colspan", 2) - .css("border-right", "none"); - row.css("width", ( - this.dataview.columnMgr.columnWidths[this.NEW_VALUE] + - this.dataview.columnMgr.columnWidths[this.OLD_VALUE]-10)+'px'); - - // Skip column 5 - row.parents("td").next().remove(); - }, - - resize: function (_height) - { - if (typeof this.options != 'undefined' && _height - && typeof this.options.resize_ratio != 'undefined') - { - // apply the ratio - _height = (this.options.resize_ratio != '')? _height * this.options.resize_ratio: _height; - if (_height != 0) - { - // 250px is the default value for history widget - // if it's not loaded yet and window is resized - // then add the default height with excess_height - if (this.div.height() == 0) _height += 250; - this.div.height(this.div.height() + _height); - - // trigger the history registered resize - // in order to update the height with new value - this.div.trigger('resize.' +this.options.value.app + this.options.value.id); - } - } - if(this.dynheight) - { - this.dynheight.update(); - } - // Resize diff widgets to match new space - if(this.dataview) - { - var columns = this.dataview.getColumnMgr().columnWidths; - jQuery('.et2_diff', this.div).closest('.innerContainer').width(columns[this.NEW_VALUE] + columns[this.OLD_VALUE]); - } - } -});}).call(this); -et2_register_widget(et2_historylog, ['historylog']); +var et2_historylog = /** @class */ (function (_super) { + __extends(et2_historylog, _super); + /** + * Constructor + * + * @memberOf et2_historylog + */ + function et2_historylog(_parent, _attrs, _child) { + var _this = _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_historylog._attributes, _child || {})) || this; + _this.legacyOptions = ["status_id"]; + _this.div = jQuery(document.createElement("div")) + .addClass("et2_historylog"); + _this.innerDiv = jQuery(document.createElement("div")) + .appendTo(_this.div); + return _this; + } + et2_historylog.prototype.set_status_id = function (_new_id) { + this.options.status_id = _new_id; + }; + et2_historylog.prototype.doLoadingFinished = function () { + _super.prototype.doLoadingFinished.call(this); + // Find the tab + var tab = this.get_tab_info(); + if (tab) { + // Bind the action to when the tab is selected + var handler = function (e) { + e.data.div.unbind("click.history"); + // Bind on click tap, because we need to update history size + // after a rezise happend and history log was not the active tab + e.data.div.bind("click.history", { "history": e.data.history, div: tab.flagDiv }, function (e) { + if (e.data.history && e.data.history.dynheight) { + e.data.history.dynheight.update(function (_w, _h) { + e.data.history.dataview.resize(_w, _h); + }); + } + }); + if (typeof e.data.history.dataview == "undefined") { + e.data.history.finishInit(); + if (e.data.history.dynheight) { + e.data.history.dynheight.update(function (_w, _h) { + e.data.history.dataview.resize(_w, _h); + }); + } + } + }; + tab.flagDiv.bind("click.history", { "history": this, div: tab.flagDiv }, handler); + // Display if history tab is selected + if (tab.contentDiv.is(':visible') && typeof this.dataview == 'undefined') { + tab.flagDiv.trigger("click.history"); + } + } + else { + this.finishInit(); + } + return true; + }; + et2_historylog.prototype._createNamespace = function () { + return true; + }; + /** + * Finish initialization which was skipped until tab was selected + */ + et2_historylog.prototype.finishInit = function () { + // No point with no ID + if (!this.options.value || !this.options.value.id) { + return; + } + this._filters = { + record_id: this.options.value.id, + appname: this.options.value.app, + get_rows: this.options.get_rows + }; + // Warn if status_id is the same as history id, that causes overlap and missing labels + if (this.options.status_id === this.id) { + this.egw().debug("warn", "status_id attribute should not be the same as historylog ID"); + } + // Create the dynheight component which dynamically scales the inner + // container. + this.div.parentsUntil('.et2_tabs').height('100%'); + var parent = this.get_tab_info(); + this.dynheight = new et2_dynheight(parent ? parent.contentDiv : this.div.parent(), this.innerDiv, 250); + // Create the outer grid container + this.dataview = new et2_dataview_1.et2_dataview(this.innerDiv, this.egw()); + var dataview_columns = []; + var _columns = typeof this.options.columns === "string" ? + this.options.columns.split(',') : this.options.columns; + for (var i = 0; i < et2_historylog.columns.length; i++) { + dataview_columns[i] = { + "id": et2_historylog.columns[i].id, + "caption": et2_historylog.columns[i].caption, + "width": et2_historylog.columns[i].width, + "visibility": _columns.indexOf(et2_historylog.columns[i].id) < 0 ? + et2_dataview_model_columns_1.et2_dataview_column.ET2_COL_VISIBILITY_INVISIBLE : et2_dataview_model_columns_1.et2_dataview_column.ET2_COL_VISIBILITY_VISIBLE + }; + } + this.dataview.setColumns(dataview_columns); + // Create widgets for columns that stay the same, and set up varying widgets + this.createWidgets(); + // Create the gridview controller + var linkCallback = function () { + }; + this.controller = new et2_dataview_controller_1.et2_dataview_controller(null, this.dataview.grid); + this.controller.setContext(this); + this.controller.setDataProvider(this); + this.controller.setLinkCallback(linkCallback); + this.controller.setRowCallback(this.rowCallback); + this.controller.setActionObjectManager(null); + var total = typeof this.options.value.total !== "undefined" ? + this.options.value.total : 0; + // This triggers an invalidate, which updates the grid + this.dataview.grid.setTotalCount(total); + // Insert any data sent from server, so invalidate finds data already + if (this.options.value.rows && this.options.value.num_rows) { + this.controller.loadInitialData(this.options.value.dataStorePrefix, this.options.value.row_id, this.options.value.rows); + // Remove, to prevent duplication + delete this.options.value.rows; + // This triggers an invalidate, which updates the grid + this.dataview.grid.setTotalCount(total); + } + else { + // Trigger the initial update + this.controller.update(); + } + // Write something inside the column headers + for (var i = 0; i < et2_historylog.columns.length; i++) { + jQuery(this.dataview.getHeaderContainerNode(i)).text(et2_historylog.columns[i].caption); + } + // Register a resize callback + var self = this; + jQuery(window).on('resize.' + this.options.value.app + this.options.value.id, function () { + if (self && typeof self.dynheight != 'undefined') + self.dynheight.update(function (_w, _h) { + self.dataview.resize(_w, _h); + }); + }); + }; + /** + * Destroys all + */ + et2_historylog.prototype.destroy = function () { + // Unbind, if bound + if (this.options.value && !this.options.value.id) { + jQuery(window).off('.' + this.options.value.app + this.options.value.id); + } + // Free the widgets + for (var i = 0; i < et2_historylog.columns.length; i++) { + if (et2_historylog.columns[i].widget) + et2_historylog.columns[i].widget.destroy(); + } + for (var key in this.fields) { + this.fields[key].widget.destroy(); + } + // Free the grid components + if (this.dataview) + this.dataview.destroy(); + if (this.controller) + this.controller.destroy(); + if (this.dynheight) + this.dynheight.destroy(); + _super.prototype.destroy.call(this); + }; + /** + * Create all needed widgets for new / old values + */ + et2_historylog.prototype.createWidgets = function () { + // Constant widgets - first 3 columns + for (var i_1 = 0; i_1 < et2_historylog.columns.length; i_1++) { + if (et2_historylog.columns[i_1].widget_type) { + // Status ID is allowed to be remapped to something else. Only affects the widget ID though + var attrs = { 'readonly': true, 'id': (i_1 == et2_historylog.FIELD ? this.options.status_id : et2_historylog.columns[i_1].id) }; + et2_historylog.columns[i_1].widget = et2_createWidget(et2_historylog.columns[i_1].widget_type, attrs, this); + et2_historylog.columns[i_1].widget.transformAttributes(attrs); + et2_historylog.columns[i_1].nodes = jQuery(et2_historylog.columns[i_1].widget.getDetachedNodes()); + } + } + // Add in handling for links + if (typeof this.options.value['status-widgets']['~link~'] == 'undefined') { + et2_historylog.columns[et2_historylog.FIELD].widget.optionValues['~link~'] = this.egw().lang('link'); + this.options.value['status-widgets']['~link~'] = 'link'; + } + // Add in handling for files + if (typeof this.options.value['status-widgets']['~file~'] == 'undefined') { + et2_historylog.columns[et2_historylog.FIELD].widget.optionValues['~file~'] = this.egw().lang('File'); + this.options.value['status-widgets']['~file~'] = 'vfs'; + } + // Add in handling for user-agent & action + if (typeof this.options.value['status-widgets']['user_agent_action'] == 'undefined') { + et2_historylog.columns[et2_historylog.FIELD].widget.optionValues['user_agent_action'] = this.egw().lang('User-agent & action'); + } + // Per-field widgets - new value & old value + this.fields = {}; + var labels = et2_historylog.columns[et2_historylog.FIELD].widget.optionValues; + // Custom fields - Need to create one that's all read-only for proper display + var cf_widget = et2_createWidget('customfields', { 'readonly': true }, this); + cf_widget.loadFields(); + // Override this or it may damage the real values + cf_widget.getValue = function () { return null; }; + for (var key_1 in cf_widget.widgets) { + // Add label + labels[cf_widget.prefix + key_1] = cf_widget.options.customfields[key_1].label; + // If it doesn't support detached nodes, just treat it as text + if (cf_widget.widgets[key_1].getDetachedNodes) { + var nodes = cf_widget.widgets[key_1].getDetachedNodes(); + for (var i = 0; i < nodes.length; i++) { + if (nodes[i] == null) + nodes.splice(i, 1); + } + // Save to use for each row + this.fields[cf_widget.prefix + key_1] = { + attrs: cf_widget.widgets[key_1].options, + widget: cf_widget.widgets[key_1], + nodes: jQuery(nodes) + }; + } + } + // Add all cf labels + et2_historylog.columns[et2_historylog.FIELD].widget.set_select_options(labels); + // From app + for (var key in this.options.value['status-widgets']) { + var attrs_1 = jQuery.extend({ 'readonly': true, 'id': key }, this.getArrayMgr('modifications').getEntry(key)); + var field = attrs_1.type || this.options.value['status-widgets'][key]; + var options = null; + var widget = this._create_widget(key, field, attrs_1, options); + if (widget === null) { + continue; + } + if (widget.instanceOf(et2_selectbox)) + widget.options.multiple = true; + widget.transformAttributes(attrs_1); + // Save to use for each row + var nodes_1 = widget._children.length ? [] : jQuery(widget.getDetachedNodes()); + for (var i_2 = 0; i_2 < widget._children.length; i_2++) { + // @ts-ignore + nodes_1.push(jQuery(widget._children[i_2].getDetachedNodes())); + } + this.fields[key] = { + attrs: attrs_1, + widget: widget, + nodes: nodes_1 + }; + } + // Widget for text diffs + var diff = et2_createWidget('diff', {}, this); + this.diff = { + // @ts-ignore + widget: diff, + nodes: jQuery(diff.getDetachedNodes()) + }; + }; + et2_historylog.prototype._create_widget = function (key, field, attrs, options) { + var widget = null; + // If field has multiple parts (is object) and isn't an obvious select box + if (typeof field === 'object') { + // Check for multi-part statuses needing multiple widgets + var need_box = false; //!this.getArrayMgr('sel_options').getEntry(key); + for (var j in field) { + // Require widget to be a widget, to avoid invalid widgets + // (and template, which is a widget and an infolog todo status) + if (et2_registry[field[j]] && ['template'].indexOf(field[j]) < 0) // && (et2_registry[field[j]].prototype.instanceOf(et2_valueWidget)) + { + need_box = true; + break; + } + } + if (need_box) { + // Multi-part value needs multiple widgets + widget = et2_createWidget('vbox', attrs, this); + for (var i in field) { + var type = field[i]; + var child_attrs = jQuery.extend({}, attrs); + if (typeof type === 'object') { + child_attrs['select_options'] = field[i]; + type = 'select'; + } + else { + delete child_attrs['select_options']; + } + child_attrs.id = i; + var child = this._create_widget(i, type, child_attrs, options); + widget.addChild(child); + child.transformAttributes(child_attrs); + } + } + else { + attrs['select_options'] = field; + } + } + // Check for options after the type, ex: link-entry:infolog + else if (field.indexOf(':') > 0) { + var options = field.split(':'); + field = options.shift(); + } + if (widget === null) { + widget = et2_createWidget(typeof field === 'string' ? field : 'select', attrs, this); + } + if (!widget.instanceOf(et2_IDetachedDOM)) { + this.egw().debug("warn", this, "Invalid widget " + field + " for " + key + ". Status widgets must implement et2_IDetachedDOM."); + return null; + } + // Parse / set legacy options + if (options) { + var mgr = this.getArrayMgr("content"); + for (var i_3 = 0; i_3 < options.length && i_3 < widget.legacyOptions.length; i_3++) { + // Not set + if (options[i_3] === "") + continue; + var attr = widget.attributes[widget.legacyOptions[i_3]]; + var attrValue = options[i_3]; + // 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); + } + attrs[widget.legacyOptions[i_3]] = attrValue; + if (typeof widget['set_' + widget.legacyOptions[i_3]] === 'function') { + widget['set_' + widget.legacyOptions[i_3]].call(widget, attrValue); + } + else { + widget.options[widget.legacyOptions[i_3]] = attrValue; + } + } + } + return widget; + }; + et2_historylog.prototype.getDOMNode = function (_sender) { + if (_sender == this) { + return this.div[0]; + } + for (var i = 0; i < et2_historylog.columns.length; i++) { + if (_sender == et2_historylog.columns[i].widget) { + return this.dataview.getHeaderContainerNode(i); + } + } + return null; + }; + et2_historylog.prototype.dataFetch = function (_queriedRange, _callback, _context) { + // Skip getting data if there's no ID + if (!this.value.id) + return; + // Set num_rows to fetch via nextmatch + if (this.options.value['num_rows']) + _queriedRange['num_rows'] = this.options.value['num_rows']; + var historylog = this; + // Pass the fetch call to the API + this.egw().dataFetch(this.getInstanceManager().etemplate_exec_id, _queriedRange, this._filters, this.id, function (_response) { + _callback.call(this, _response); + // This seems to prevent unwanted scrollbars + historylog.div.hide(); + window.setTimeout(function () { + historylog.div.show(); + }.bind(historylog), 100); + }, _context, []); + }; + // Needed by interface + et2_historylog.prototype.dataRegisterUID = function (_uid, _callback, _context) { + this.egw().dataRegisterUID(_uid, _callback, _context, this.getInstanceManager().etemplate_exec_id, this.id); + }; + et2_historylog.prototype.dataUnregisterUID = function (_uid, _callback, _context) { + // Needed by interface + }; + /** + * The row callback gets called by the gridview controller whenever + * the actual DOM-Nodes for a node with the given data have to be + * created. + * + * @param {type} _data + * @param {type} _row + * @param {type} _idx + * @param {type} _entry + */ + et2_historylog.prototype.rowCallback = function (_data, _row, _idx, _entry) { + var tr = _row.getDOMNode(); + jQuery(tr).attr("valign", "top"); + var row = this.dataview.rowProvider.getPrototype("default"); + var self = this; + jQuery("div", row).each(function (i) { + var nodes = []; + var widget = et2_historylog.columns[i].widget; + var value = _data[et2_historylog.columns[i].id]; + if (et2_historylog.OWNER === i && _data['share_email']) { + // Show share email instead of owner + widget = undefined; + value = _data['share_email']; + } + // Get widget from list, unless it needs a diff widget + if (typeof widget == 'undefined' && typeof self.fields[_data.status] != 'undefined' && (i < et2_historylog.NEW_VALUE || + i >= et2_historylog.NEW_VALUE && (self.fields[_data.status].nodes || !self._needsDiffWidget(_data['status'], _data[et2_historylog.columns[et2_historylog.OLD_VALUE].id])))) { + widget = self.fields[_data.status].widget; + if (!widget._children.length) { + nodes = self.fields[_data.status].nodes.clone(); + } + for (var j = 0; j < widget._children.length; j++) { + // @ts-ignore + nodes.push(self.fields[_data.status].nodes[j].clone()); + if (widget._children[j].instanceOf(et2_widget_diff_1.et2_diff)) { + self._spanValueColumns(jQuery(this)); + } + } + } + else if (widget) { + nodes = et2_historylog.columns[i].nodes.clone(); + } + else if (( + // Already parsed & cached + typeof _data[et2_historylog.columns[et2_historylog.NEW_VALUE].id] == "object" && + typeof _data[et2_historylog.columns[et2_historylog.NEW_VALUE].id] != "undefined" && + _data[et2_historylog.columns[et2_historylog.NEW_VALUE].id] !== null) || // typeof null === 'object' + // Large old value + self._needsDiffWidget(_data['status'], _data[et2_historylog.columns[et2_historylog.OLD_VALUE].id]) || + // Large new value + self._needsDiffWidget(_data['status'], _data[et2_historylog.columns[et2_historylog.NEW_VALUE].id])) { + // Large text value - span both columns, and show a nice diff + var jthis = jQuery(this); + if (i === et2_historylog.NEW_VALUE) { + // Diff widget + widget = self.diff.widget; + nodes = self.diff.nodes.clone(); + if (widget) + widget.setDetachedAttributes(nodes, { + value: value, + label: jthis.parents("td").prev().text() + }); + self._spanValueColumns(jthis); + } + } + else { + // No widget fallback - display actual value + nodes = jQuery('').text(value === null ? '' : value); + } + if (widget) { + if (widget._children.length) { + // Multi-part values + var box = jQuery(widget.getDOMNode()).clone(); + for (var j = 0; j < widget._children.length; j++) { + var id = widget._children[j].id; + var widget_value = value ? value[id] || "" : ""; + widget._children[j].setDetachedAttributes(nodes[j], { value: widget_value }); + box.append(nodes[j]); + } + nodes = box; + } + else { + widget.setDetachedAttributes(nodes, { value: value }); + } + } + jQuery(this).append(nodes); + }); + jQuery(tr).append(row.children()); + return tr; + }; + /** + * How to tell if the row needs a diff widget or not + * + * @param {string} columnName + * @param {string} value + * @returns {Boolean} + */ + et2_historylog.prototype._needsDiffWidget = function (columnName, value) { + if (typeof value !== "string" && value) { + this.egw().debug("warn", "Crazy diff value", value); + return false; + } + return value === '***diff***'; + }; + /** + * Make a single row's new value cell span across both new value and old value + * columns. Used for diff widget. + * + * @param {jQuery} row jQuery wrapped row node + */ + et2_historylog.prototype._spanValueColumns = function (row) { + // Stretch column 4 + row.parents("td").attr("colspan", 2) + .css("border-right", "none"); + row.css("width", (this.dataview.getColumnMgr().getColumnWidth(et2_historylog.NEW_VALUE) + + this.dataview.getColumnMgr().getColumnWidth(et2_historylog.OLD_VALUE) - 10) + 'px'); + // Skip column 5 + row.parents("td").next().remove(); + }; + et2_historylog.prototype.resize = function (_height) { + if (typeof this.options != 'undefined' && _height + && typeof this.options.resize_ratio != 'undefined') { + // apply the ratio + _height = (this.options.resize_ratio != '') ? _height * this.options.resize_ratio : _height; + if (_height != 0) { + // 250px is the default value for history widget + // if it's not loaded yet and window is resized + // then add the default height with excess_height + if (this.div.height() == 0) + _height += 250; + this.div.height(this.div.height() + _height); + // trigger the history registered resize + // in order to update the height with new value + this.div.trigger('resize.' + this.options.value.app + this.options.value.id); + } + } + if (this.dynheight) { + this.dynheight.update(); + } + // Resize diff widgets to match new space + if (this.dataview) { + var columns = this.dataview.getColumnMgr(); + jQuery('.et2_diff', this.div).closest('.innerContainer') + .width(columns.getColumnWidth(et2_historylog.NEW_VALUE) + columns.getColumnWidth(et2_historylog.OLD_VALUE)); + } + }; + et2_historylog._attributes = { + "value": { + "name": "Value", + "type": "any", + "description": "Object {app: ..., id: ..., status-widgets: {}} where status-widgets is a map of fields to widgets used to display those fields" + }, + "status_id": { + "name": "status_id", + "type": "string", + "default": "status", + "description": "The history widget is traditionally named 'status'. If you name another widget in the same template 'status', you can use this attribute to re-name the history widget. " + }, + "columns": { + "name": "columns", + "type": "string", + "default": "user_ts,owner,status,new_value,old_value", + "description": "Columns to display. Default is user_ts,owner,status,new_value,old_value" + }, + "get_rows": { + "name": "get_rows", + "type": "string", + "default": "EGroupware\\Api\\Storage\\History::get_rows", + "description": "Method to get rows" + } + }; + et2_historylog.columns = [ + { 'id': 'user_ts', caption: 'Date', 'width': '120px', widget_type: 'date-time', widget: null, nodes: null }, + { 'id': 'owner', caption: 'User', 'width': '150px', widget_type: 'select-account', widget: null, nodes: null }, + { 'id': 'status', caption: 'Changed', 'width': '120px', widget_type: 'select', widget: null, nodes: null }, + { 'id': 'new_value', caption: 'New Value', 'width': '50%', widget: null, nodes: null }, + { 'id': 'old_value', caption: 'Old Value', 'width': '50%', widget: null, nodes: null } + ]; + et2_historylog.TIMESTAMP = 0; + et2_historylog.OWNER = 1; + et2_historylog.FIELD = 2; + et2_historylog.NEW_VALUE = 3; + et2_historylog.OLD_VALUE = 4; + return et2_historylog; +}(et2_core_valueWidget_1.et2_valueWidget)); +exports.et2_historylog = et2_historylog; +et2_core_widget_1.et2_register_widget(et2_historylog, ['historylog']); +//# sourceMappingURL=et2_widget_historylog.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_historylog.ts b/api/js/etemplate/et2_widget_historylog.ts new file mode 100644 index 0000000000..dd49497834 --- /dev/null +++ b/api/js/etemplate/et2_widget_historylog.ts @@ -0,0 +1,747 @@ +/** + * EGroupware eTemplate2 - JS History log + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage api + * @link http://www.egroupware.org + * @author Nathan Gray + * @copyright 2012 Nathan Gray + */ + +/*egw:uses + /vendor/bower-asset/jquery/dist/jquery.js; + /vendor/bower-asset/jquery-ui/jquery-ui.js; + et2_core_valueWidget; + + // Include the grid classes + et2_dataview; +*/ + +import {et2_IDataProvider} from "./et2_dataview_interfaces"; +import {et2_register_widget, WidgetConfig} from "./et2_core_widget"; +import {ClassWithAttributes} from "./et2_core_inheritance"; +import {et2_valueWidget} from "./et2_core_valueWidget"; +import {et2_dataview} from "./et2_dataview"; +import {et2_dataview_column} from "./et2_dataview_model_columns"; +import {et2_dataview_controller} from "./et2_dataview_controller"; +import {et2_diff} from "./et2_widget_diff"; + +/** + * eTemplate history log widget displays a list of changes to the current record. + * The widget is encapsulated, and only needs the record's ID, and a map of + * fields:widgets for display. + * + * It defers its initialization until the tab that it's on is selected, to avoid + * wasting time if the user never looks at it. + * + * @augments et2_valueWidget + */ +export class et2_historylog extends et2_valueWidget implements et2_IDataProvider,et2_IResizeable +{ + static readonly _attributes = { + "value": { + "name": "Value", + "type": "any", + "description": "Object {app: ..., id: ..., status-widgets: {}} where status-widgets is a map of fields to widgets used to display those fields" + }, + "status_id":{ + "name": "status_id", + "type": "string", + "default": "status", + "description": "The history widget is traditionally named 'status'. If you name another widget in the same template 'status', you can use this attribute to re-name the history widget. " + }, + "columns": { + "name": "columns", + "type": "string", + "default": "user_ts,owner,status,new_value,old_value", + "description": "Columns to display. Default is user_ts,owner,status,new_value,old_value" + }, + "get_rows": { + "name": "get_rows", + "type": "string", + "default": "EGroupware\\Api\\Storage\\History::get_rows", + "description": "Method to get rows" + } + }; + + legacyOptions = ["status_id"]; + protected static columns = [ + {'id': 'user_ts', caption: 'Date', 'width': '120px', widget_type: 'date-time', widget: null, nodes: null}, + {'id': 'owner', caption: 'User', 'width': '150px', widget_type: 'select-account', widget: null, nodes: null}, + {'id': 'status', caption: 'Changed', 'width': '120px', widget_type: 'select', widget: null, nodes: null}, + {'id': 'new_value', caption: 'New Value', 'width': '50%', widget: null, nodes: null}, + {'id': 'old_value', caption: 'Old Value', 'width': '50%', widget: null, nodes: null} + ]; + + static readonly TIMESTAMP = 0; + static readonly OWNER = 1; + static readonly FIELD = 2; + static readonly NEW_VALUE = 3; + static readonly OLD_VALUE = 4; + + private div: JQuery; + private innerDiv: JQuery; + private _filters: { appname: string; record_id: string; get_rows: string; }; + private dynheight: et2_dynheight; + private dataview: et2_dataview; + private controller: et2_dataview_controller; + private fields: any; + private diff: et2_diff; + private value: any; + + /** + * Constructor + * + * @memberOf et2_historylog + */ + constructor(_parent?, _attrs? : WidgetConfig, _child? : object) { + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_historylog._attributes, _child || {})); + this.div = jQuery(document.createElement("div")) + .addClass("et2_historylog"); + + this.innerDiv = jQuery(document.createElement("div")) + .appendTo(this.div); + } + + set_status_id( _new_id) + { + this.options.status_id = _new_id; + } + + doLoadingFinished( ) : boolean | JQueryPromise + { + super.doLoadingFinished(); + + // Find the tab + let tab = this.get_tab_info(); + if(tab) + { + // Bind the action to when the tab is selected + const handler = function (e) { + e.data.div.unbind("click.history"); + // Bind on click tap, because we need to update history size + // after a rezise happend and history log was not the active tab + e.data.div.bind("click.history", {"history": e.data.history, div: tab.flagDiv}, function (e) { + if (e.data.history && e.data.history.dynheight) { + e.data.history.dynheight.update(function (_w, _h) { + e.data.history.dataview.resize(_w, _h); + }); + } + }); + + if (typeof e.data.history.dataview == "undefined") { + e.data.history.finishInit(); + if (e.data.history.dynheight) { + e.data.history.dynheight.update(function (_w, _h) { + e.data.history.dataview.resize(_w, _h); + }); + } + } + + }; + tab.flagDiv.bind("click.history",{"history": this, div: tab.flagDiv}, handler); + + // Display if history tab is selected + if(tab.contentDiv.is(':visible') && typeof this.dataview == 'undefined') + { + tab.flagDiv.trigger("click.history"); + } + } + else + { + this.finishInit(); + } + return true; + } + + _createNamespace() + { + return true; + } + /** + * Finish initialization which was skipped until tab was selected + */ + finishInit( ) + { + // No point with no ID + if(!this.options.value || !this.options.value.id) + { + return; + } + this._filters = { + record_id: this.options.value.id, + appname: this.options.value.app, + get_rows: this.options.get_rows + }; + + // Warn if status_id is the same as history id, that causes overlap and missing labels + if(this.options.status_id === this.id) + { + this.egw().debug("warn", "status_id attribute should not be the same as historylog ID"); + } + + // Create the dynheight component which dynamically scales the inner + // container. + this.div.parentsUntil('.et2_tabs').height('100%'); + const parent = this.get_tab_info(); + this.dynheight = new et2_dynheight(parent ? parent.contentDiv : this.div.parent(), + this.innerDiv, 250 + ); + + // Create the outer grid container + this.dataview = new et2_dataview(this.innerDiv, this.egw()); + const dataview_columns = []; + let _columns = typeof this.options.columns === "string" ? + this.options.columns.split(',') : this.options.columns; + for (var i = 0; i < et2_historylog.columns.length; i++) + { + dataview_columns[i] = { + "id": et2_historylog.columns[i].id, + "caption": et2_historylog.columns[i].caption, + "width":et2_historylog.columns[i].width, + "visibility":_columns.indexOf(et2_historylog.columns[i].id) < 0 ? + et2_dataview_column.ET2_COL_VISIBILITY_INVISIBLE : et2_dataview_column.ET2_COL_VISIBILITY_VISIBLE + }; + } + this.dataview.setColumns(dataview_columns); + + // Create widgets for columns that stay the same, and set up varying widgets + this.createWidgets(); + + // Create the gridview controller + const linkCallback = function () + { + }; + this.controller = new et2_dataview_controller(null, this.dataview.grid); + this.controller.setContext(this); + this.controller.setDataProvider(this); + this.controller.setLinkCallback(linkCallback); + this.controller.setRowCallback(this.rowCallback); + this.controller.setActionObjectManager(null); + + const total = typeof this.options.value.total !== "undefined" ? + this.options.value.total : 0; + + // This triggers an invalidate, which updates the grid + this.dataview.grid.setTotalCount(total); + + // Insert any data sent from server, so invalidate finds data already + if(this.options.value.rows && this.options.value.num_rows) + { + this.controller.loadInitialData( + this.options.value.dataStorePrefix, + this.options.value.row_id, + this.options.value.rows + ); + // Remove, to prevent duplication + delete this.options.value.rows; + // This triggers an invalidate, which updates the grid + this.dataview.grid.setTotalCount(total); + } + else + { + // Trigger the initial update + this.controller.update(); + } + + // Write something inside the column headers + for (var i = 0; i < et2_historylog.columns.length; i++) + { + jQuery(this.dataview.getHeaderContainerNode(i)).text(et2_historylog.columns[i].caption); + } + + // Register a resize callback + const self = this; + jQuery(window).on('resize.' +this.options.value.app + this.options.value.id, function() + { + if (self && typeof self.dynheight != 'undefined') self.dynheight.update(function(_w, _h) { + self.dataview.resize(_w, _h); + }); + }); + } + + /** + * Destroys all + */ + destroy( ) + { + // Unbind, if bound + if(this.options.value && !this.options.value.id) + { + jQuery(window).off('.' +this.options.value.app + this.options.value.id); + } + + // Free the widgets + for(let i = 0; i < et2_historylog.columns.length; i++) + { + if(et2_historylog.columns[i].widget) et2_historylog.columns[i].widget.destroy(); + } + for(let key in this.fields) + { + this.fields[key].widget.destroy(); + } + + // Free the grid components + if(this.dataview) this.dataview.destroy(); + if(this.controller) this.controller.destroy(); + if(this.dynheight) this.dynheight.destroy(); + + super.destroy(); + } + + /** + * Create all needed widgets for new / old values + */ + createWidgets( ) + { + + // Constant widgets - first 3 columns + for(let i = 0; i < et2_historylog.columns.length; i++) + { + if(et2_historylog.columns[i].widget_type) + { + // Status ID is allowed to be remapped to something else. Only affects the widget ID though + var attrs = {'readonly': true, 'id': (i == et2_historylog.FIELD ? this.options.status_id : et2_historylog.columns[i].id)}; + et2_historylog.columns[i].widget = et2_createWidget(et2_historylog.columns[i].widget_type, attrs, this); + et2_historylog.columns[i].widget.transformAttributes(attrs); + et2_historylog.columns[i].nodes = jQuery(et2_historylog.columns[i].widget.getDetachedNodes()); + } + } + + // Add in handling for links + if(typeof this.options.value['status-widgets']['~link~'] == 'undefined') + { + et2_historylog.columns[et2_historylog.FIELD].widget.optionValues['~link~'] = this.egw().lang('link'); + this.options.value['status-widgets']['~link~'] = 'link'; + } + + // Add in handling for files + if(typeof this.options.value['status-widgets']['~file~'] == 'undefined') + { + et2_historylog.columns[et2_historylog.FIELD].widget.optionValues['~file~'] = this.egw().lang('File'); + this.options.value['status-widgets']['~file~'] = 'vfs'; + } + + // Add in handling for user-agent & action + if(typeof this.options.value['status-widgets']['user_agent_action'] == 'undefined') + { + et2_historylog.columns[et2_historylog.FIELD].widget.optionValues['user_agent_action'] = this.egw().lang('User-agent & action'); + } + + // Per-field widgets - new value & old value + this.fields = {}; + + let labels = et2_historylog.columns[et2_historylog.FIELD].widget.optionValues; + + // Custom fields - Need to create one that's all read-only for proper display + let cf_widget = et2_createWidget('customfields', {'readonly':true}, this); + cf_widget.loadFields(); + // Override this or it may damage the real values + cf_widget.getValue = function() {return null;}; + for(let key in cf_widget.widgets) + { + // Add label + labels[cf_widget.prefix + key] = cf_widget.options.customfields[key].label; + + // If it doesn't support detached nodes, just treat it as text + if(cf_widget.widgets[key].getDetachedNodes) + { + var nodes = cf_widget.widgets[key].getDetachedNodes(); + for(var i = 0; i < nodes.length; i++) + { + if(nodes[i] == null) nodes.splice(i,1); + } + + // Save to use for each row + this.fields[cf_widget.prefix + key] = { + attrs: cf_widget.widgets[key].options, + widget: cf_widget.widgets[key], + nodes: jQuery(nodes) + }; + } + } + + // Add all cf labels + et2_historylog.columns[et2_historylog.FIELD].widget.set_select_options(labels); + + // From app + for(var key in this.options.value['status-widgets']) + { + let attrs = jQuery.extend({'readonly': true, 'id': key}, this.getArrayMgr('modifications').getEntry(key)); + const field = attrs.type || this.options.value['status-widgets'][key]; + const options = null; + const widget = this._create_widget(key, field, attrs, options); + if(widget === null) + { + continue; + } + if(widget.instanceOf(et2_selectbox)) widget.options.multiple = true; + widget.transformAttributes(attrs); + + // Save to use for each row + let nodes = widget._children.length ? [] : jQuery(widget.getDetachedNodes()); + for(let i = 0; i < widget._children.length; i++) + { + // @ts-ignore + nodes.push(jQuery(widget._children[i].getDetachedNodes())); + } + this.fields[key] = { + attrs: attrs, + widget: widget, + nodes: nodes + }; + } + // Widget for text diffs + const diff = et2_createWidget('diff', {}, this); + this.diff = { + // @ts-ignore + widget: diff, + nodes: jQuery(diff.getDetachedNodes()) + }; + } + + _create_widget(key, field, attrs, options) + { + let widget = null; + + // If field has multiple parts (is object) and isn't an obvious select box + if(typeof field === 'object') + { + // Check for multi-part statuses needing multiple widgets + let need_box = false;//!this.getArrayMgr('sel_options').getEntry(key); + for(let j in field) + { + // Require widget to be a widget, to avoid invalid widgets + // (and template, which is a widget and an infolog todo status) + if(et2_registry[field[j]] && ['template'].indexOf(field[j]) < 0)// && (et2_registry[field[j]].prototype.instanceOf(et2_valueWidget)) + { + need_box = true; + break; + } + } + + if(need_box) + { + // Multi-part value needs multiple widgets + widget = et2_createWidget('vbox', attrs, this); + for(var i in field) + { + let type = field[i]; + const child_attrs = jQuery.extend({}, attrs); + if(typeof type === 'object') + { + child_attrs['select_options'] = field[i]; + type = 'select'; + } + else + { + delete child_attrs['select_options']; + } + child_attrs.id = i; + const child = this._create_widget(i, type, child_attrs, options); + widget.addChild(child); + child.transformAttributes(child_attrs); + } + } + else + { + attrs['select_options'] = field; + } + } + // Check for options after the type, ex: link-entry:infolog + else if (field.indexOf(':') > 0) + { + var options = field.split(':'); + field = options.shift(); + } + + if(widget === null) + { + widget = et2_createWidget(typeof field === 'string' ? field : 'select', attrs, this); + } + + if(!widget.instanceOf(et2_IDetachedDOM)) + { + this.egw().debug("warn", this, "Invalid widget " + field + " for " + key + ". Status widgets must implement et2_IDetachedDOM."); + return null; + } + + // Parse / set legacy options + if(options) + { + const mgr = this.getArrayMgr("content"); + for(let i = 0; i < options.length && i < widget.legacyOptions.length; i++) + { + // Not set + if(options[i] === "") continue; + + const attr = widget.attributes[widget.legacyOptions[i]]; + let attrValue = options[i]; + + // 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); + } + attrs[widget.legacyOptions[i]] = attrValue; + if(typeof widget['set_'+widget.legacyOptions[i]] === 'function') + { + widget['set_'+widget.legacyOptions[i]].call(widget, attrValue); + } + else + { + widget.options[widget.legacyOptions[i]] = attrValue; + } + } + } + return widget; + } + + getDOMNode( _sender) + { + if (_sender == this) + { + return this.div[0]; + } + + for (let i = 0; i < et2_historylog.columns.length; i++) + { + if (_sender == et2_historylog.columns[i].widget) + { + return this.dataview.getHeaderContainerNode(i); + } + } + return null; + } + + + dataFetch( _queriedRange, _callback, _context) + { + // Skip getting data if there's no ID + if(!this.value.id) return; + + // Set num_rows to fetch via nextmatch + if ( this.options.value['num_rows'] ) + _queriedRange['num_rows'] = this.options.value['num_rows']; + + const historylog = this; + // Pass the fetch call to the API + this.egw().dataFetch( + this.getInstanceManager().etemplate_exec_id, + _queriedRange, + this._filters, + this.id, + function(_response) { + _callback.call(this,_response); + // This seems to prevent unwanted scrollbars + historylog.div.hide(); + window.setTimeout(function() { + historylog.div.show(); + }.bind(historylog),100); + }, + _context, + [] + ); + } + + + // Needed by interface + dataRegisterUID( _uid, _callback, _context) + { + this.egw().dataRegisterUID(_uid, _callback, _context, this.getInstanceManager().etemplate_exec_id, + this.id); + } + + dataUnregisterUID( _uid, _callback, _context) + { + // Needed by interface + } + + /** + * The row callback gets called by the gridview controller whenever + * the actual DOM-Nodes for a node with the given data have to be + * created. + * + * @param {type} _data + * @param {type} _row + * @param {type} _idx + * @param {type} _entry + */ + rowCallback( _data, _row, _idx, _entry) + { + let tr = _row.getDOMNode(); + jQuery(tr).attr("valign","top"); + + let row = this.dataview.rowProvider.getPrototype("default"); + let self = this; + jQuery("div", row).each(function (i) { + let nodes : any[] | JQuery = []; + let widget = et2_historylog.columns[i].widget; + let value = _data[et2_historylog.columns[i].id]; + if(et2_historylog.OWNER === i && _data['share_email']) + { + // Show share email instead of owner + widget = undefined; + value = _data['share_email']; + } + // Get widget from list, unless it needs a diff widget + if(typeof widget == 'undefined' && typeof self.fields[_data.status] != 'undefined' && ( + i < et2_historylog.NEW_VALUE || + i >= et2_historylog.NEW_VALUE && ( + self.fields[_data.status].nodes || !self._needsDiffWidget(_data['status'], _data[et2_historylog.columns[et2_historylog.OLD_VALUE].id]) + ) + )) + { + widget = self.fields[_data.status].widget; + if(!widget._children.length) + { + nodes = self.fields[_data.status].nodes.clone(); + } + for(var j = 0; j < widget._children.length; j++) + { + // @ts-ignore + nodes.push(self.fields[_data.status].nodes[j].clone()); + if(widget._children[j].instanceOf(et2_diff)) + { + self._spanValueColumns(jQuery(this)); + } + } + } + else if (widget) + { + nodes = et2_historylog.columns[i].nodes.clone(); + } + else if (( + // Already parsed & cached + typeof _data[et2_historylog.columns[et2_historylog.NEW_VALUE].id] == "object" && + typeof _data[et2_historylog.columns[et2_historylog.NEW_VALUE].id] != "undefined" && + _data[et2_historylog.columns[et2_historylog.NEW_VALUE].id] !== null) || // typeof null === 'object' + // Large old value + self._needsDiffWidget(_data['status'], _data[et2_historylog.columns[et2_historylog.OLD_VALUE].id]) || + // Large new value + self._needsDiffWidget(_data['status'], _data[et2_historylog.columns[et2_historylog.NEW_VALUE].id])) + { + // Large text value - span both columns, and show a nice diff + let jthis = jQuery(this); + if(i === et2_historylog.NEW_VALUE) + { + // Diff widget + widget = self.diff.widget; + nodes = self.diff.nodes.clone(); + + if(widget) widget.setDetachedAttributes(nodes, { + value: value, + label: jthis.parents("td").prev().text() + }); + + self._spanValueColumns(jthis); + } + } + else + { + // No widget fallback - display actual value + nodes = jQuery('').text(value === null ? '' : value); + } + if(widget) + { + if(widget._children.length) + { + // Multi-part values + const box = jQuery(widget.getDOMNode()).clone(); + for(var j = 0; j < widget._children.length; j++) + { + const id = widget._children[j].id; + const widget_value = value ? value[id] || "" : ""; + widget._children[j].setDetachedAttributes(nodes[j], {value:widget_value}); + box.append(nodes[j]); + } + nodes = box; + } + else + { + widget.setDetachedAttributes(nodes, {value:value}); + } + } + jQuery(this).append(nodes); + }); + jQuery(tr).append(row.children()); + + return tr; + } + + /** + * How to tell if the row needs a diff widget or not + * + * @param {string} columnName + * @param {string} value + * @returns {Boolean} + */ + _needsDiffWidget( columnName, value) + { + if(typeof value !== "string" && value) + { + this.egw().debug("warn", "Crazy diff value", value); + return false; + } + return value === '***diff***'; + } + + /** + * Make a single row's new value cell span across both new value and old value + * columns. Used for diff widget. + * + * @param {jQuery} row jQuery wrapped row node + */ + _spanValueColumns(row) + { + // Stretch column 4 + row.parents("td").attr("colspan", 2) + .css("border-right", "none"); + row.css("width", ( + this.dataview.getColumnMgr().getColumnWidth(et2_historylog.NEW_VALUE) + + this.dataview.getColumnMgr().getColumnWidth(et2_historylog.OLD_VALUE)-10)+'px'); + + // Skip column 5 + row.parents("td").next().remove(); + } + + resize(_height) + { + if (typeof this.options != 'undefined' && _height + && typeof this.options.resize_ratio != 'undefined') + { + // apply the ratio + _height = (this.options.resize_ratio != '')? _height * this.options.resize_ratio: _height; + if (_height != 0) + { + // 250px is the default value for history widget + // if it's not loaded yet and window is resized + // then add the default height with excess_height + if (this.div.height() == 0) _height += 250; + this.div.height(this.div.height() + _height); + + // trigger the history registered resize + // in order to update the height with new value + this.div.trigger('resize.' +this.options.value.app + this.options.value.id); + } + } + if(this.dynheight) + { + this.dynheight.update(); + } + // Resize diff widgets to match new space + if(this.dataview) + { + const columns = this.dataview.getColumnMgr(); + jQuery('.et2_diff', this.div).closest('.innerContainer') + .width(columns.getColumnWidth(et2_historylog.NEW_VALUE) + columns.getColumnWidth(et2_historylog.OLD_VALUE)); + } + } +} +et2_register_widget(et2_historylog, ['historylog']); diff --git a/api/js/etemplate/et2_widget_hrule.js b/api/js/etemplate/et2_widget_hrule.js index c82b2bb82e..451eb07084 100644 --- a/api/js/etemplate/et2_widget_hrule.js +++ b/api/js/etemplate/et2_widget_hrule.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - JS HRule object * @@ -6,31 +7,47 @@ * @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_baseWidget; + et2_core_baseWidget; */ - +var et2_core_widget_1 = require("./et2_core_widget"); +var et2_core_baseWidget_1 = require("./et2_core_baseWidget"); +var et2_core_inheritance_1 = require("./et2_core_inheritance"); /** * Class which implements the hrule tag * * @augments et2_baseWidget */ -var et2_hrule = (function(){ "use strict"; return et2_baseWidget.extend( -{ - /** - * Constructor - * - * @memberOf et2_hrule - */ - init: function() { - this._super.apply(this, arguments); - - this.setDOMNode(document.createElement("hr")); - } -});}).call(this); -et2_register_widget(et2_hrule, ["hrule"]); - +var et2_hrule = /** @class */ (function (_super) { + __extends(et2_hrule, _super); + /** + * Constructor + * + * @memberOf et2_hrule + */ + function et2_hrule(_parent, _attrs, _child) { + var _this = + // Call the inherited constructor + _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_hrule._attributes, _child || {})) || this; + _this.setDOMNode(document.createElement("hr")); + return _this; + } + return et2_hrule; +}(et2_core_baseWidget_1.et2_baseWidget)); +et2_core_widget_1.et2_register_widget(et2_hrule, ["hrule"]); +//# sourceMappingURL=et2_widget_hrule.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_hrule.ts b/api/js/etemplate/et2_widget_hrule.ts new file mode 100644 index 0000000000..4bc6d41e2b --- /dev/null +++ b/api/js/etemplate/et2_widget_hrule.ts @@ -0,0 +1,41 @@ +/** + * EGroupware eTemplate2 - JS HRule object + * + * @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_baseWidget; +*/ + +import {et2_register_widget, WidgetConfig} from "./et2_core_widget"; +import {et2_baseWidget} from "./et2_core_baseWidget"; +import {ClassWithAttributes} from "./et2_core_inheritance"; + +/** + * Class which implements the hrule tag + * + * @augments et2_baseWidget + */ +class et2_hrule extends et2_baseWidget +{ + /** + * Constructor + * + * @memberOf et2_hrule + */ + constructor(_parent, _attrs? : WidgetConfig, _child? : object) + { + // Call the inherited constructor + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_hrule._attributes, _child || {})); + + this.setDOMNode(document.createElement("hr")); + } +} +et2_register_widget(et2_hrule, ["hrule"]); + + diff --git a/api/js/etemplate/et2_widget_html.js b/api/js/etemplate/et2_widget_html.js index 381ef43f2b..0d3f8a0fb9 100644 --- a/api/js/etemplate/et2_widget_html.js +++ b/api/js/etemplate/et2_widget_html.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - JS widget class containing raw HTML * @@ -6,110 +7,110 @@ * @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 - jsapi.jsapi; // Needed for egw_seperateJavaScript - /vendor/bower-asset/jquery/dist/jquery.js; - et2_core_baseWidget; + jsapi.jsapi; // Needed for egw_seperateJavaScript + /vendor/bower-asset/jquery/dist/jquery.js; + et2_core_baseWidget; */ - +var et2_core_valueWidget_1 = require("./et2_core_valueWidget"); +var et2_core_widget_1 = require("./et2_core_widget"); +var et2_core_inheritance_1 = require("./et2_core_inheritance"); /** * @augments et2_valueWidget */ -var et2_html = (function(){ "use strict"; return et2_valueWidget.extend([et2_IDetachedDOM], -{ - attributes: { - 'label': { - 'default': "", - description: "The label is displayed by default in front (for radiobuttons behind) each widget (if not empty). If you want to specify a different position, use a '%s' in the label, which gets replaced by the widget itself. Eg. '%s Name' to have the label Name behind a checkbox. The label can contain variables, as descript for name. If the label starts with a '@' it is replaced by the value of the content-array at this index (with the '@'-removed and after expanding the variables).", - ignore: false, - name: "Label", - translate: true, - type: "string" - }, - "needed": { - "ignore": true - }, - value: { - name: "Value", - description: "The value of the widget", - type: "html", // "string" would remove html tags by running html_entity_decode - default: et2_no_init - } - }, - - /** - * Constructor - * - * @memberOf et2_html - */ - init: function() { - this._super.apply(this, arguments); - - // Allow no child widgets - this.supportedWidgetClasses = []; - - this.htmlNode = jQuery(document.createElement("span")); - if(this._type == 'htmlarea') - { - this.htmlNode.addClass('et2_textbox_ro'); - } - if(this.options.label) - { - this.htmlNode.append(''+this.options.label+''); - } - this.setDOMNode(this.htmlNode[0]); - }, - - loadContent: function(_data) { - // Create an object containg the given value and an empty js string - var html = {html: _data ? _data : '', js: ''}; - - // Seperate the javascript from the given html. The js code will be - // written to the previously created empty js string - egw_seperateJavaScript(html); - - // Append the html to the parent element - if(this.options.label) - { - this.htmlNode.append(''+this.options.label+''); - } - this.htmlNode.append(html.html); - this.htmlNode.append(html.js); - }, - - set_value: function(_value) { - this.htmlNode.empty(); - this.loadContent(_value); - }, - - /** - * Code for implementing et2_IDetachedDOM - * - * @param {array} _attrs - */ - getDetachedAttributes: function(_attrs) - { - _attrs.push("value", "class"); - }, - - getDetachedNodes: function() - { - return [this.htmlNode[0]]; - }, - - setDetachedAttributes: function(_nodes, _values) - { - this.htmlNode = jQuery(_nodes[0]); - if(typeof _values['value'] !== 'undefined') - { - this.set_value(_values['value']); - } - } - -});}).call(this); -et2_register_widget(et2_html, ["html","htmlarea_ro"]); - +var et2_html = /** @class */ (function (_super) { + __extends(et2_html, _super); + /** + * Constructor + * + * @memberOf et2_html + */ + function et2_html(_parent, _attrs, _child) { + var _this = + // Call the inherited constructor + _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_html._attributes, _child || {})) || this; + _this.htmlNode = null; + // Allow no child widgets + _this.supportedWidgetClasses = []; + _this.htmlNode = jQuery(document.createElement("span")); + if (_this.getType() == 'htmlarea') { + _this.htmlNode.addClass('et2_textbox_ro'); + } + if (_this.options.label) { + _this.htmlNode.append('' + _this.options.label + ''); + } + _this.setDOMNode(_this.htmlNode[0]); + return _this; + } + et2_html.prototype.loadContent = function (_data) { + // Create an object containg the given value and an empty js string + var html = { html: _data ? _data : '', js: '' }; + // Seperate the javascript from the given html. The js code will be + // written to the previously created empty js string + egw_seperateJavaScript(html); + // Append the html to the parent element + if (this.options.label) { + this.htmlNode.append('' + this.options.label + ''); + } + this.htmlNode.append(html.html); + this.htmlNode.append(html.js); + }; + et2_html.prototype.set_value = function (_value) { + this.htmlNode.empty(); + this.loadContent(_value); + }; + /** + * Code for implementing et2_IDetachedDOM + * + * @param {array} _attrs + */ + et2_html.prototype.getDetachedAttributes = function (_attrs) { + _attrs.push("value", "class"); + }; + et2_html.prototype.getDetachedNodes = function () { + return [this.htmlNode[0]]; + }; + et2_html.prototype.setDetachedAttributes = function (_nodes, _values) { + this.htmlNode = jQuery(_nodes[0]); + if (typeof _values['value'] !== 'undefined') { + this.set_value(_values['value']); + } + }; + et2_html._attributes = { + 'label': { + 'default': "", + description: "The label is displayed by default in front (for radiobuttons behind) each widget (if not empty). If you want to specify a different position, use a '%s' in the label, which gets replaced by the widget itself. Eg. '%s Name' to have the label Name behind a checkbox. The label can contain variables, as descript for name. If the label starts with a '@' it is replaced by the value of the content-array at this index (with the '@'-removed and after expanding the variables).", + ignore: false, + name: "Label", + translate: true, + type: "string" + }, + "needed": { + "ignore": true + }, + value: { + name: "Value", + description: "The value of the widget", + type: "html", + default: et2_no_init + } + }; + return et2_html; +}(et2_core_valueWidget_1.et2_valueWidget)); +et2_core_widget_1.et2_register_widget(et2_html, ["html", "htmlarea_ro"]); +//# sourceMappingURL=et2_widget_html.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_html.ts b/api/js/etemplate/et2_widget_html.ts new file mode 100644 index 0000000000..7f8f58c974 --- /dev/null +++ b/api/js/etemplate/et2_widget_html.ts @@ -0,0 +1,124 @@ +/** + * EGroupware eTemplate2 - JS widget class containing raw HTML + * + * @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 + jsapi.jsapi; // Needed for egw_seperateJavaScript + /vendor/bower-asset/jquery/dist/jquery.js; + et2_core_baseWidget; +*/ + +import {et2_valueWidget} from "./et2_core_valueWidget"; +import {WidgetConfig, et2_register_widget} from "./et2_core_widget"; +import {ClassWithAttributes} from "./et2_core_inheritance"; + +/** + * @augments et2_valueWidget + */ +class et2_html extends et2_valueWidget implements et2_IDetachedDOM +{ + static readonly _attributes : any = { + 'label': { + 'default': "", + description: "The label is displayed by default in front (for radiobuttons behind) each widget (if not empty). If you want to specify a different position, use a '%s' in the label, which gets replaced by the widget itself. Eg. '%s Name' to have the label Name behind a checkbox. The label can contain variables, as descript for name. If the label starts with a '@' it is replaced by the value of the content-array at this index (with the '@'-removed and after expanding the variables).", + ignore: false, + name: "Label", + translate: true, + type: "string" + }, + "needed": { + "ignore": true + }, + value: { + name: "Value", + description: "The value of the widget", + type: "html", // "string" would remove html tags by running html_entity_decode + default: et2_no_init + } + }; + + htmlNode : JQuery = null; + + /** + * Constructor + * + * @memberOf et2_html + */ + constructor(_parent, _attrs? : WidgetConfig, _child? : object) + { + // Call the inherited constructor + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_html._attributes, _child || {})); + + // Allow no child widgets + this.supportedWidgetClasses = []; + + this.htmlNode = jQuery(document.createElement("span")); + if(this.getType() == 'htmlarea') + { + this.htmlNode.addClass('et2_textbox_ro'); + } + if(this.options.label) + { + this.htmlNode.append(''+this.options.label+''); + } + this.setDOMNode(this.htmlNode[0]); + } + + loadContent(_data) + { + // Create an object containg the given value and an empty js string + let html = {html: _data ? _data : '', js: ''}; + + // Seperate the javascript from the given html. The js code will be + // written to the previously created empty js string + egw_seperateJavaScript(html); + + // Append the html to the parent element + if(this.options.label) + { + this.htmlNode.append(''+this.options.label+''); + } + this.htmlNode.append(html.html); + this.htmlNode.append(html.js); + } + + set_value(_value) + { + this.htmlNode.empty(); + this.loadContent(_value); + } + + /** + * Code for implementing et2_IDetachedDOM + * + * @param {array} _attrs + */ + getDetachedAttributes(_attrs) + { + _attrs.push("value", "class"); + } + + getDetachedNodes() + { + return [this.htmlNode[0]]; + } + + setDetachedAttributes(_nodes, _values) + { + this.htmlNode = jQuery(_nodes[0]); + if(typeof _values['value'] !== 'undefined') + { + this.set_value(_values['value']); + } + } + +} +et2_register_widget(et2_html, ["html","htmlarea_ro"]); + + diff --git a/api/js/etemplate/et2_widget_htmlarea.js b/api/js/etemplate/et2_widget_htmlarea.js index ef1ffe96d0..08c550a5ea 100644 --- a/api/js/etemplate/et2_widget_htmlarea.js +++ b/api/js/etemplate/et2_widget_htmlarea.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - JS widget for HTML editing * @@ -9,499 +10,465 @@ * @copyright Hadi Nategh * @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.jsapi; // Needed for egw_seperateJavaScript - /vendor/tinymce/tinymce/tinymce.min.js; - et2_core_editableWidget; + jsapi.jsapi; // Needed for egw_seperateJavaScript + /vendor/tinymce/tinymce/tinymce.min.js; + et2_core_editableWidget; */ - +var et2_core_editableWidget_1 = require("./et2_core_editableWidget"); +var et2_core_inheritance_1 = require("./et2_core_inheritance"); +var et2_core_widget_1 = require("./et2_core_widget"); /** * @augments et2_inputWidget */ -var et2_htmlarea = (function(){ "use strict"; return et2_editableWidget.extend([et2_IResizeable], -{ - attributes: { - mode: { - 'name': 'Mode', - 'description': 'One of {ascii|simple|extended|advanced}', - 'default': '', - 'type': 'string' - }, - height: { - 'name': 'Height', - 'default': et2_no_init, - 'type': 'string' - }, - width: { - 'name': 'Width', - 'default': et2_no_init, - 'type': 'string' - }, - value: { - name: "Value", - description: "The value of the widget", - type: "html", // "string" would remove html tags by running html_entity_decode - default: et2_no_init - }, - imageUpload: { - name: "imageUpload", - description: "Url to upload images dragged in or id of link_to widget to it's vfs upload. Can also be just a name for which content array contains a path to upload the picture.", - type: "string", - default: null - }, - file_picker_callback: { - name: "File picker callback", - description: "Callback function to get called when file picker is clicked", - type: 'js', - default: et2_no_init - }, - images_upload_handler: { - name: "Images upload handler", - description: "Callback function for handling image upload", - type: 'js', - default: et2_no_init - }, - menubar: { - name: "Menubar", - description: "Display menubar at the top of the editor", - type: "boolean", - default: true - }, - statusbar: { - name: "Status bar", - description: "Enable/disable status bar on the bottom of editor", - type: "boolean", - default: true - }, - valid_children: { - name: "Valid children", - description: "Enables to control what child tag is allowed or not allowed of the present tag. For instance: +body[style], makes style tag allowed inside body", - type: "string", - default: "+body[style]" - } - }, - - /** - * Constructor - * - * @param _parent - * @param _attrs - * @memberOf et2_htmlarea - */ - init: function(_parent, _attrs) { - this._super.apply(this, arguments); - this.editor = null; // TinyMce editor instance - this.supportedWidgetClasses = []; // Allow no child widgets - this.htmlNode = jQuery(document.createElement(this.options.readonly ? "div" : "textarea")) - .addClass('et2_textbox_ro'); - if(this.options.height) - { - this.htmlNode.css('height', this.options.height); - } - this.setDOMNode(this.htmlNode[0]); - }, - - /** - * - * @returns {undefined} - */ - doLoadingFinished: function() { - this._super.apply(this, arguments); - - this.init_editor(); - }, - - init_editor: function() { - if(this.mode == 'ascii' || this.editor != null || this.options.readonly) return; - var imageUpload = ''; - var self = this; - if (this.options.imageUpload && this.options.imageUpload[0] !== '/' && this.options.imageUpload.substr(0, 4) != 'http') - { - imageUpload = egw.ajaxUrl("EGroupware\\Api\\Etemplate\\Widget\\Vfs::ajax_htmlarea_upload")+ - '&request_id='+this.getInstanceManager().etemplate_exec_id+'&widget_id='+this.options.imageUpload+'&type=htmlarea'; - imageUpload = imageUpload.substr(egw.webserverUrl.length+1); - } - else if (imageUpload) - { - imageUpload = this.options.imageUpload.substr(egw.webserverUrl.length+1); - } - else - { - imageUpload = egw.ajaxUrl("EGroupware\\Api\\Etemplate\\Widget\\Vfs::ajax_htmlarea_upload")+ - '&request_id='+this.getInstanceManager().etemplate_exec_id+'&type=htmlarea'; - } - // default settings for initialization - var settings = { - target: this.htmlNode[0], - body_id: this.dom + '_htmlarea', - menubar: false, - statusbar: this.options.statusbar, - branding: false, - resize: false, - height: this.options.height, - width: this.options.width, - mobile: { - theme: 'silver' - }, - formats: { - customparagraph: { block: 'p', styles: {"margin-block-start": "0px", "margin-block-end": "0px"}} - }, - min_height: 100, - convert_urls: false, - language: et2_htmlarea.LANGUAGE_CODE[egw.preference('lang', 'common')], - language_url: egw.webserverUrl+'/api/js/tinymce/langs/'+et2_htmlarea.LANGUAGE_CODE[egw.preference('lang', 'common')]+'.js', - paste_data_images: true, - paste_filter_drop: true, - browser_spellcheck: true, - contextmenu: false, - images_upload_url: imageUpload, - file_picker_callback: jQuery.proxy(this._file_picker_callback, this), - images_upload_handler: this.options.images_upload_handler, - init_instance_callback : jQuery.proxy(this._instanceIsReady, this), - auto_focus: false, - valid_children : this.options.valid_children, - plugins: [ - "print searchreplace autolink directionality ", - "visualblocks visualchars image link media template | fullscreen", - "codesample table charmap hr pagebreak nonbreaking anchor toc ", - "insertdatetime advlist lists textcolor wordcount imagetools ", - "colorpicker textpattern help paste code searchreplace tabfocus" - ], - toolbar: et2_htmlarea.TOOLBAR_SIMPLE, - block_formats: "Paragraph=p;Heading 1=h1;Heading 2=h2;Heading 3=h3;"+ - "Heading 4=h4;Heading 5=h5;Heading 6=h6;Preformatted=pre;Custom Paragraph=customparagraph", - font_formats: "Andale Mono=andale mono,times;Arial=arial,helvetica,"+ - "sans-serif;Arial Black=arial black,avant garde;Book Antiqua=book "+ - "antiqua,palatino;Comic Sans MS=comic sans ms,sans-serif;"+ - "Courier New=courier new,courier;Georgia=georgia,palatino;"+ - "Helvetica=helvetica;Impact=impact,chicago;Symbol=symbol;"+ - "Tahoma=tahoma,arial,helvetica,sans-serif;Terminal=terminal,"+ - "monaco;Times New Roman=times new roman,times;Trebuchet "+ - "MS=trebuchet ms,geneva;Verdana=verdana,geneva;Webdings=webdings;"+ - "Wingdings=wingdings,zapf dingbats", - fontsize_formats: '8pt 10pt 12pt 14pt 18pt 24pt 36pt', - setup : function(ed) - { - ed.on('init', function() - { +var et2_htmlarea = /** @class */ (function (_super) { + __extends(et2_htmlarea, _super); + /** + * Constructor + */ + function et2_htmlarea(_parent, _attrs, _child) { + var _this = + // Call the inherited constructor + _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_htmlarea._attributes, _child || {})) || this; + _this.editor = null; + _this.htmlNode = null; + _this.editor = null; // TinyMce editor instance + _this.supportedWidgetClasses = []; // Allow no child widgets + _this.htmlNode = jQuery(document.createElement(_this.options.readonly ? "div" : "textarea")) + .addClass('et2_textbox_ro'); + if (_this.options.height) { + _this.htmlNode.css('height', _this.options.height); + } + _this.setDOMNode(_this.htmlNode[0]); + return _this; + } + /** + * + * @returns {undefined} + */ + et2_htmlarea.prototype.doLoadingFinished = function () { + _super.prototype.doLoadingFinished.call(this); + this.init_editor(); + return true; + }; + et2_htmlarea.prototype.init_editor = function () { + if (this.mode == 'ascii' || this.editor != null || this.options.readonly) + return; + var imageUpload; + var self = this; + if (this.options.imageUpload && this.options.imageUpload[0] !== '/' && this.options.imageUpload.substr(0, 4) != 'http') { + imageUpload = egw.ajaxUrl("EGroupware\\Api\\Etemplate\\Widget\\Vfs::ajax_htmlarea_upload") + + '&request_id=' + this.getInstanceManager().etemplate_exec_id + '&widget_id=' + this.options.imageUpload + '&type=htmlarea'; + imageUpload = imageUpload.substr(egw.webserverUrl.length + 1); + } + else if (imageUpload) { + imageUpload = this.options.imageUpload.substr(egw.webserverUrl.length + 1); + } + else { + imageUpload = egw.ajaxUrl("EGroupware\\Api\\Etemplate\\Widget\\Vfs::ajax_htmlarea_upload") + + '&request_id=' + this.getInstanceManager().etemplate_exec_id + '&type=htmlarea'; + } + // default settings for initialization + var settings = { + target: this.htmlNode[0], + body_id: this.dom_id + '_htmlarea', + menubar: false, + statusbar: this.options.statusbar, + branding: false, + resize: false, + height: this.options.height, + width: this.options.width, + mobile: { + theme: 'silver' + }, + formats: { + customparagraph: { block: 'p', styles: { "margin-block-start": "0px", "margin-block-end": "0px" } } + }, + min_height: 100, + convert_urls: false, + language: et2_htmlarea.LANGUAGE_CODE[egw.preference('lang', 'common')], + language_url: egw.webserverUrl + '/api/js/tinymce/langs/' + et2_htmlarea.LANGUAGE_CODE[egw.preference('lang', 'common')] + '.js', + paste_data_images: true, + paste_filter_drop: true, + browser_spellcheck: true, + contextmenu: false, + images_upload_url: imageUpload, + file_picker_callback: jQuery.proxy(this._file_picker_callback, this), + images_upload_handler: this.options.images_upload_handler, + init_instance_callback: jQuery.proxy(this._instanceIsReady, this), + auto_focus: false, + valid_children: this.options.valid_children, + plugins: [ + "print searchreplace autolink directionality ", + "visualblocks visualchars image link media template fullscreen", + "codesample table charmap hr pagebreak nonbreaking anchor toc ", + "insertdatetime advlist lists textcolor wordcount imagetools ", + "colorpicker textpattern help paste code searchreplace tabfocus" + ], + toolbar: et2_htmlarea.TOOLBAR_SIMPLE, + block_formats: "Paragraph=p;Heading 1=h1;Heading 2=h2;Heading 3=h3;" + + "Heading 4=h4;Heading 5=h5;Heading 6=h6;Preformatted=pre;Custom Paragraph=customparagraph", + font_formats: "Andale Mono=andale mono,times;Arial=arial,helvetica," + + "sans-serif;Arial Black=arial black,avant garde;Book Antiqua=book " + + "antiqua,palatino;Comic Sans MS=comic sans ms,sans-serif;" + + "Courier New=courier new,courier;Georgia=georgia,palatino;" + + "Helvetica=helvetica;Impact=impact,chicago;Symbol=symbol;" + + "Tahoma=tahoma,arial,helvetica,sans-serif;Terminal=terminal," + + "monaco;Times New Roman=times new roman,times;Trebuchet " + + "MS=trebuchet ms,geneva;Verdana=verdana,geneva;Webdings=webdings;" + + "Wingdings=wingdings,zapf dingbats", + fontsize_formats: '8pt 10pt 12pt 14pt 18pt 24pt 36pt', + setup: function (ed) { + ed.on('init', function () { this.getDoc().body.style.fontSize = egw.preference('rte_font_size', 'common') - + egw.preference('rte_font_unit', 'common'); + + egw.preference('rte_font_unit', 'common'); this.getDoc().body.style.fontFamily = egw.preference('rte_font', 'common'); }); } - }; - - // extend default settings with configured options and preferences - jQuery.extend(settings, this._extendedSettings()); - this.tinymce = tinymce.init(settings); - // make sure value gets set in case of widget gets loaded by delay like - // inside an inactive tabs - this.tinymce.then(function() { - self.set_value(self.htmlNode.val()); - if (self.editor && self.editor.editorContainer) - { - self.editor.formatter.toggle(egw.preference('rte_formatblock', 'common')); - jQuery(self.editor.editorContainer).height(self.options.height); - jQuery(self.editor.iframeElement.contentWindow.document).on('dragenter', function(){ - if (jQuery('#dragover-tinymce').length < 1) jQuery("").appendTo('head'); - }); - } - }); - }, - - /** - * set disabled - * - * @param {type} _value - * @returns {undefined} - */ - set_disabled: function(_value) - { - this._super.apply(this, arguments); - if (_value) - { - jQuery(this.tinymce_container).css('display', 'none'); - } - else - { - jQuery(this.tinymce_container).css('display', 'flex'); - } - }, - - set_readonly: function(_value) - { - if(this.options.readonly === _value) return; - var value = this.get_value(); - this.options.readonly = _value; - if(this.options.readonly) - { - if (this. editor) this.editor.remove(); - this.htmlNode = jQuery(document.createElement(this.options.readonly ? "div" : "textarea")) - .addClass('et2_textbox_ro'); - if(this.options.height) - { - this.htmlNode.css('height', this.options.height) - } - this.editor = null; - this.setDOMNode(this.htmlNode[0]); - this.set_value(value); - } - else - { - if(!this.editor) - { - this.htmlNode = jQuery(document.createElement("textarea")) - .val(value); - if(this.options.height || this.options.editable_height) - { - this.htmlNode.css('height', (this.options.editable_height ? this.options.editable_height : this.options.height)); - } - this.setDOMNode(this.htmlNode[0]); - this.init_editor(); - } - } - }, - - /** - * Callback function runs when the filepicker in image dialog is clicked - * - * @param {type} _callback - * @param {type} _value - * @param {type} _meta - * @returns {unresolved} - */ - _file_picker_callback: function(_callback, _value, _meta) { - if (typeof this.file_picker_callback == 'function') return this.file_picker_callback.call(arguments, this); - var callback = _callback; - - // Don't rely only on app_name to fetch et2 object as app_name may not - // always represent current app of the window, e.g.: mail admin account. - // Try to fetch et2 from its template name. - var etemplate = jQuery('form').data('etemplate'); - var et2 = {}; - if (etemplate && etemplate.name && !app[egw(window).app_name()]) - { - et2 = etemplate2.getByTemplate(etemplate.name)[0]['widgetContainer']; - } - else - { - et2 = app[egw(window).app_name()].et2; - } - - var vfsSelect = et2_createWidget('vfs-select', { - id:'upload', - mode: 'open', - name: '', - button_caption:"Link", - button_label:"Link", - dialog_title: "Link file", - method: "download" - }, et2); - jQuery(vfsSelect.getDOMNode()).on('change', function (){ - callback(vfsSelect.get_value(), {alt:vfsSelect.get_value()}); - }); - - // start the file selector dialog - vfsSelect.click(); - }, - - /** - * Callback when instance is ready - * - * @param {type} _editor - */ - _instanceIsReady: function(_editor) { - console.log("Editor: " + _editor.id + " is now initialized."); - // try to reserve focus state as running command on editor may steal the - // current focus. - var focusedEl = jQuery(':focus'); - this.editor = _editor; - - this.editor.on('drop', function(e){ - e.preventDefault(); - }); - - if (!this.disabled) jQuery(this.editor.editorContainer).css('display', 'flex'); - this.tinymce_container = this.editor.editorContainer; - // go back to reserved focused element - focusedEl.focus(); - }, - - /** - * Takes all relevant preferences into account and set settings accordingly - * - * @returns {object} returns a object including all settings - */ - _extendedSettings: function () { - - var rte_menubar = egw.preference('rte_menubar', 'common'); - var rte_toolbar = egw.preference('rte_toolbar', 'common'); - // we need to have rte_toolbar values as an array - if (rte_toolbar && typeof rte_toolbar == "object") - { - rte_toolbar = Object.values(rte_toolbar); - } - var settings = { - fontsize_formats: et2_htmlarea.FONT_SIZE_FORMATS[egw.preference('rte_font_unit', 'common')], - menubar: parseInt(rte_menubar) && this.menubar ? true : typeof rte_menubar != 'undefined' ? false : this.menubar - }; - - switch (this.mode) - { - case 'simple': - settings.toolbar = et2_htmlarea.TOOLBAR_SIMPLE; - break; - case 'extended': - settings.toolbar = et2_htmlarea.TOOLBAR_EXTENDED; - break; - case 'advanced': - settings.toolbar = et2_htmlarea.TOOLBAR_ADVANCED; - break; - default: - this.mode = ''; - } - - // take rte_toolbar into account if no mode restrictly set from template - if (rte_toolbar && !this.mode) - { - var toolbar_diff = et2_htmlarea.TOOLBAR_LIST.filter(function(i){return !(rte_toolbar.indexOf(i) > -1);}); - settings.toolbar = et2_htmlarea.TOOLBAR_ADVANCED; - toolbar_diff.forEach(function(a){ - var r = new RegExp(a); - settings.toolbar = settings.toolbar.replace(r, ''); - }); - } - return settings; - }, - - destroy: function() { - if (this.editor) - { - this.editor.destroy(); - } - this.editor = null; - this.tinymce = null; - this.tinymce_container = null; - this.htmlNode.remove(); - this.htmlNode = null; - this._super.apply(this, arguments); - }, - set_value: function(_value) { - this._oldValue = _value; - if (this.editor) - { - this.editor.setContent(_value); - } - else - { - if(this.options.readonly) - { - this.htmlNode.empty().append(_value); - } - else - { - this.htmlNode.val(_value); - } - } - this.value = _value; - }, - - getValue: function() { - return this.editor ? this.editor.getContent() : ( - this.options.readonly ? this.value : this.htmlNode.val() - ); - }, - - /** - * Resize htmlNode tag according to window size - * @param {type} _height excess height which comes from window resize - */ - resize: function (_height) - { - if (_height && this.options.resize_ratio !== '0') - { - // apply the ratio - _height = (this.options.resize_ratio != '')? _height * this.options.resize_ratio: _height; - if (_height != 0) - { - if (this.editor) // TinyMCE HTML - { - var h = 0; - if (typeof this.editor.iframeElement !='undefined' && this.editor.editorContainer.clientHeight > 0) - { - h = (this.editor.editorContainer.clientHeight + _height) > 0 ? - (this.editor.editorContainer.clientHeight) + _height: this.editor.settings.min_height; - } - else // fallback height size - { - h = this.editor.settings.min_height + _height; - } - jQuery(this.editor.editorContainer).height(h); - jQuery(this.editor.iframeElement).height(h - (this.editor.editorContainer.getElementsByClassName('tox-toolbar')[0].clientHeight + - this.editor.editorContainer.getElementsByClassName('tox-statusbar')[0].clientHeight)); - } - else // No TinyMCE - { - this.htmlNode.height(this.htmlNode.height() + _height); - } - } - } - } -});}).call(this); -et2_register_widget(et2_htmlarea, ["htmlarea"]); - -// Static class stuff -jQuery.extend(et2_htmlarea, { - /** - * Array of toolbars - * @constant - */ - TOOLBAR_LIST: ['undo', 'redo', 'formatselect', 'fontselect', 'fontsizeselect', - 'bold', 'italic', 'strikethrough', 'forecolor', 'backcolor', 'link', - 'alignleft', 'aligncenter', 'alignright', 'alignjustify', 'numlist', - 'bullist', 'outdent', 'indent', 'ltr', 'rtl', 'removeformat', 'code', 'image', 'searchreplace' - ], - /** - * arranged toolbars as simple mode - * @constant - */ - TOOLBAR_SIMPLE: "undo redo|formatselect fontselect fontsizeselect | bold italic removeformat forecolor backcolor | "+ - "alignleft aligncenter alignright alignjustify | numlist "+ - "bullist outdent indent| link image pastetext", - /** - * arranged toolbars as extended mode - * @constant - */ - TOOLBAR_EXTENDED: "fontselect fontsizeselect | bold italic strikethrough forecolor backcolor | "+ - "link | alignleft aligncenter alignright alignjustify | numlist "+ - "bullist outdent indent | removeformat | image | fullscreen", - /** - * arranged toolbars as advanced mode - * @constant - */ - TOOLBAR_ADVANCED: "undo redo| formatselect | fontselect fontsizeselect | bold italic strikethrough forecolor backcolor | "+ - "link | alignleft aligncenter alignright alignjustify | numlist "+ - "bullist outdent indent ltr rtl | removeformat code| image | searchreplace | fullscreen", - /** - * font size formats - * @constant - */ - FONT_SIZE_FORMATS: { - pt: "8pt 9pt 10pt 11pt 12pt 14pt 16pt 18pt 20pt 22pt 24pt 26pt 28pt 36pt 48pt 72pt", - px: "8px 9px 10px 11px 12px 14px 16px 18px 20px 22px 24px 26px 28px 36px 48px 72px" - }, - - /** - * language code represention for TinyMCE lang code - */ - LANGUAGE_CODE: { - bg: "bg_BG", ca: "ca", cs: "cs", da: "da", de: "de", en:"en_CA", - el:"el", "es-es":"es", et: "et", eu: "eu" , fa: "fa_IR", fi: "fi", - fr: "fr_FR", hi:"", hr:"hr", hu:"hu_HU", id: "id", it: "it", iw: "", - ja: "ja", ko: "ko_KR", lo: "", lt: "lt", lv: "lv", nl: "nl", no: "nb_NO", - pl: "pl", pt: "pt_PT", "pt-br": "pt_BR", ru: "ru", sk: "sk", sl: "sl_SI", - sv: "sv_SE", th: "th_TH", tr: "tr_TR", uk: "en_GB", vi: "vi_VN", zh: "zh_CN", - "zh-tw": "zh_TW" - } -}); \ No newline at end of file + }; + // extend default settings with configured options and preferences + jQuery.extend(settings, this._extendedSettings()); + this.tinymce = tinymce.init(settings); + // make sure value gets set in case of widget gets loaded by delay like + // inside an inactive tabs + this.tinymce.then(function () { + self.set_value(self.htmlNode.val()); + if (self.editor && self.editor.editorContainer) { + self.editor.formatter.toggle(egw.preference('rte_formatblock', 'common')); + jQuery(self.editor.editorContainer).height(self.options.height); + jQuery(self.editor.iframeElement.contentWindow.document).on('dragenter', function () { + if (jQuery('#dragover-tinymce').length < 1) + jQuery("").appendTo('head'); + }); + } + }); + }; + /** + * set disabled + * + * @param {type} _value + * @returns {undefined} + */ + et2_htmlarea.prototype.set_disabled = function (_value) { + _super.prototype.set_disabled.call(this, _value); + if (_value) { + jQuery(this.tinymce_container).css('display', 'none'); + } + else { + jQuery(this.tinymce_container).css('display', 'flex'); + } + }; + et2_htmlarea.prototype.set_readonly = function (_value) { + if (this.options.readonly === _value) + return; + var value = this.get_value(); + this.options.readonly = _value; + if (this.options.readonly) { + if (this.editor) + this.editor.remove(); + this.htmlNode = jQuery(document.createElement(this.options.readonly ? "div" : "textarea")) + .addClass('et2_textbox_ro'); + if (this.options.height) { + this.htmlNode.css('height', this.options.height); + } + this.editor = null; + this.setDOMNode(this.htmlNode[0]); + this.set_value(value); + } + else { + if (!this.editor) { + this.htmlNode = jQuery(document.createElement("textarea")) + .val(value); + if (this.options.height || this.options.editable_height) { + this.htmlNode.css('height', (this.options.editable_height ? this.options.editable_height : this.options.height)); + } + this.setDOMNode(this.htmlNode[0]); + this.init_editor(); + } + } + }; + /** + * Callback function runs when the filepicker in image dialog is clicked + * + * @param {type} _callback + * @param {type} _value + * @param {type} _meta + */ + et2_htmlarea.prototype._file_picker_callback = function (_callback, _value, _meta) { + if (typeof this.file_picker_callback == 'function') + return this.file_picker_callback.call(arguments, this); + var callback = _callback; + // Don't rely only on app_name to fetch et2 object as app_name may not + // always represent current app of the window, e.g.: mail admin account. + // Try to fetch et2 from its template name. + var etemplate = jQuery('form').data('etemplate'); + var et2; + if (etemplate && etemplate.name && !app[egw(window).app_name()]) { + et2 = etemplate2.getByTemplate(etemplate.name)[0]['widgetContainer']; + } + else { + et2 = app[egw(window).app_name()].et2; + } + var vfsSelect = et2_createWidget('vfs-select', { + id: 'upload', + mode: 'open', + name: '', + button_caption: "Link", + button_label: "Link", + dialog_title: "Link file", + method: "download" + }, et2); + jQuery(vfsSelect.getDOMNode()).on('change', function () { + callback(vfsSelect.get_value(), { alt: vfsSelect.get_value() }); + }); + // start the file selector dialog + vfsSelect.click(); + }; + /** + * Callback when instance is ready + * + * @param {type} _editor + */ + et2_htmlarea.prototype._instanceIsReady = function (_editor) { + console.log("Editor: " + _editor.id + " is now initialized."); + // try to reserve focus state as running command on editor may steal the + // current focus. + var focusedEl = jQuery(':focus'); + this.editor = _editor; + this.editor.on('drop', function (e) { + e.preventDefault(); + }); + if (!this.disabled) + jQuery(this.editor.editorContainer).css('display', 'flex'); + this.tinymce_container = this.editor.editorContainer; + // go back to reserved focused element + focusedEl.focus(); + }; + /** + * Takes all relevant preferences into account and set settings accordingly + * + * @returns {object} returns a object including all settings + */ + et2_htmlarea.prototype._extendedSettings = function () { + var rte_menubar = egw.preference('rte_menubar', 'common'); + var rte_toolbar = egw.preference('rte_toolbar', 'common'); + // we need to have rte_toolbar values as an array + if (rte_toolbar && typeof rte_toolbar == "object") { + rte_toolbar = Object.keys(rte_toolbar).map(function (key) { return rte_toolbar[key]; }); + } + var settings = { + fontsize_formats: et2_htmlarea.FONT_SIZE_FORMATS[egw.preference('rte_font_unit', 'common')], + menubar: parseInt(rte_menubar) && this.menubar ? true : typeof rte_menubar != 'undefined' ? false : this.menubar + }; + switch (this.mode) { + case 'simple': + settings['toolbar'] = et2_htmlarea.TOOLBAR_SIMPLE; + break; + case 'extended': + settings['toolbar'] = et2_htmlarea.TOOLBAR_EXTENDED; + break; + case 'advanced': + settings['toolbar'] = et2_htmlarea.TOOLBAR_ADVANCED; + break; + default: + this.mode = ''; + } + // take rte_toolbar into account if no mode restrictly set from template + if (rte_toolbar && !this.mode) { + var toolbar_diff = et2_htmlarea.TOOLBAR_LIST.filter(function (i) { return !(rte_toolbar.indexOf(i) > -1); }); + settings['toolbar'] = et2_htmlarea.TOOLBAR_ADVANCED; + toolbar_diff.forEach(function (a) { + var r = new RegExp(a); + settings['toolbar'] = settings['toolbar'].replace(r, ''); + }); + } + return settings; + }; + et2_htmlarea.prototype.destroy = function () { + if (this.editor) { + this.editor.destroy(); + } + this.editor = null; + this.tinymce = null; + this.tinymce_container = null; + this.htmlNode.remove(); + this.htmlNode = null; + _super.prototype.destroy.call(this); + }; + et2_htmlarea.prototype.set_value = function (_value) { + this._oldValue = _value; + if (this.editor) { + this.editor.setContent(_value); + } + else { + if (this.options.readonly) { + this.htmlNode.empty().append(_value); + } + else { + this.htmlNode.val(_value); + } + } + this.value = _value; + }; + et2_htmlarea.prototype.getValue = function () { + return this.editor ? this.editor.getContent() : (this.options.readonly ? this.value : this.htmlNode.val()); + }; + /** + * Resize htmlNode tag according to window size + * @param {type} _height excess height which comes from window resize + */ + et2_htmlarea.prototype.resize = function (_height) { + if (_height && this.options.resize_ratio !== '0') { + // apply the ratio + _height = (this.options.resize_ratio != '') ? _height * this.options.resize_ratio : _height; + if (_height != 0) { + if (this.editor) // TinyMCE HTML + { + var h = void 0; + if (typeof this.editor.iframeElement != 'undefined' && this.editor.editorContainer.clientHeight > 0) { + h = (this.editor.editorContainer.clientHeight + _height) > 0 ? + (this.editor.editorContainer.clientHeight) + _height : this.editor.settings.min_height; + } + else // fallback height size + { + h = this.editor.settings.min_height + _height; + } + jQuery(this.editor.editorContainer).height(h); + jQuery(this.editor.iframeElement).height(h - (this.editor.editorContainer.getElementsByClassName('tox-toolbar')[0].clientHeight + + this.editor.editorContainer.getElementsByClassName('tox-statusbar')[0].clientHeight)); + } + else // No TinyMCE + { + this.htmlNode.height(this.htmlNode.height() + _height); + } + } + } + }; + et2_htmlarea._attributes = { + mode: { + 'name': 'Mode', + 'description': 'One of {ascii|simple|extended|advanced}', + 'default': '', + 'type': 'string' + }, + height: { + 'name': 'Height', + 'default': et2_no_init, + 'type': 'string' + }, + width: { + 'name': 'Width', + 'default': et2_no_init, + 'type': 'string' + }, + value: { + name: "Value", + description: "The value of the widget", + type: "html", + default: et2_no_init + }, + imageUpload: { + name: "imageUpload", + description: "Url to upload images dragged in or id of link_to widget to it's vfs upload. Can also be just a name for which content array contains a path to upload the picture.", + type: "string", + default: null + }, + file_picker_callback: { + name: "File picker callback", + description: "Callback function to get called when file picker is clicked", + type: 'js', + default: et2_no_init + }, + images_upload_handler: { + name: "Images upload handler", + description: "Callback function for handling image upload", + type: 'js', + default: et2_no_init + }, + menubar: { + name: "Menubar", + description: "Display menubar at the top of the editor", + type: "boolean", + default: true + }, + statusbar: { + name: "Status bar", + description: "Enable/disable status bar on the bottom of editor", + type: "boolean", + default: true + }, + valid_children: { + name: "Valid children", + description: "Enables to control what child tag is allowed or not allowed of the present tag. For instance: +body[style], makes style tag allowed inside body", + type: "string", + default: "+body[style]" + } + }; + /** + * Array of toolbars + * @constant + */ + et2_htmlarea.TOOLBAR_LIST = ['undo', 'redo', 'formatselect', 'fontselect', 'fontsizeselect', + 'bold', 'italic', 'strikethrough', 'forecolor', 'backcolor', 'link', + 'alignleft', 'aligncenter', 'alignright', 'alignjustify', 'numlist', + 'bullist', 'outdent', 'indent', 'ltr', 'rtl', 'removeformat', 'code', 'image', 'searchreplace' + ]; + /** + * arranged toolbars as simple mode + * @constant + */ + et2_htmlarea.TOOLBAR_SIMPLE = "undo redo|formatselect fontselect fontsizeselect | bold italic removeformat forecolor backcolor | " + + "alignleft aligncenter alignright alignjustify | numlist " + + "bullist outdent indent| link image pastetext"; + /** + * arranged toolbars as extended mode + * @constant + */ + et2_htmlarea.TOOLBAR_EXTENDED = "fontselect fontsizeselect | bold italic strikethrough forecolor backcolor | " + + "link | alignleft aligncenter alignright alignjustify | numlist " + + "bullist outdent indent | removeformat | image | fullscreen"; + /** + * arranged toolbars as advanced mode + * @constant + */ + et2_htmlarea.TOOLBAR_ADVANCED = "undo redo| formatselect | fontselect fontsizeselect | bold italic strikethrough forecolor backcolor | " + + "link | alignleft aligncenter alignright alignjustify | numlist " + + "bullist outdent indent ltr rtl | removeformat code| image | searchreplace | fullscreen"; + /** + * font size formats + * @constant + */ + et2_htmlarea.FONT_SIZE_FORMATS = { + pt: "8pt 9pt 10pt 11pt 12pt 14pt 16pt 18pt 20pt 22pt 24pt 26pt 28pt 36pt 48pt 72pt", + px: "8px 9px 10px 11px 12px 14px 16px 18px 20px 22px 24px 26px 28px 36px 48px 72px" + }; + /** + * language code represention for TinyMCE lang code + */ + et2_htmlarea.LANGUAGE_CODE = { + bg: "bg_BG", ca: "ca", cs: "cs", da: "da", de: "de", en: "en_CA", + el: "el", "es-es": "es", et: "et", eu: "eu", fa: "fa_IR", fi: "fi", + fr: "fr_FR", hi: "", hr: "hr", hu: "hu_HU", id: "id", it: "it", iw: "", + ja: "ja", ko: "ko_KR", lo: "", lt: "lt", lv: "lv", nl: "nl", no: "nb_NO", + pl: "pl", pt: "pt_PT", "pt-br": "pt_BR", ru: "ru", sk: "sk", sl: "sl_SI", + sv: "sv_SE", th: "th_TH", tr: "tr_TR", uk: "en_GB", vi: "vi_VN", zh: "zh_CN", + "zh-tw": "zh_TW" + }; + return et2_htmlarea; +}(et2_core_editableWidget_1.et2_editableWidget)); +et2_core_widget_1.et2_register_widget(et2_htmlarea, ["htmlarea"]); +//# sourceMappingURL=et2_widget_htmlarea.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_htmlarea.ts b/api/js/etemplate/et2_widget_htmlarea.ts new file mode 100644 index 0000000000..15a96068b1 --- /dev/null +++ b/api/js/etemplate/et2_widget_htmlarea.ts @@ -0,0 +1,525 @@ +/** + * EGroupware eTemplate2 - JS widget for HTML editing + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage api + * @link http://www.egroupware.org + * @author Hadi Nategh + * @copyright Hadi Nategh + * @version $Id$ + */ + +/*egw:uses + jsapi.jsapi; // Needed for egw_seperateJavaScript + /vendor/tinymce/tinymce/tinymce.min.js; + et2_core_editableWidget; +*/ + +import {et2_editableWidget} from "./et2_core_editableWidget"; +import {ClassWithAttributes} from "./et2_core_inheritance"; +import {WidgetConfig, et2_register_widget} from "./et2_core_widget"; + +/** + * @augments et2_inputWidget + */ +class et2_htmlarea extends et2_editableWidget implements et2_IResizeable +{ + static readonly _attributes : any = { + mode: { + 'name': 'Mode', + 'description': 'One of {ascii|simple|extended|advanced}', + 'default': '', + 'type': 'string' + }, + height: { + 'name': 'Height', + 'default': et2_no_init, + 'type': 'string' + }, + width: { + 'name': 'Width', + 'default': et2_no_init, + 'type': 'string' + }, + value: { + name: "Value", + description: "The value of the widget", + type: "html", // "string" would remove html tags by running html_entity_decode + default: et2_no_init + }, + imageUpload: { + name: "imageUpload", + description: "Url to upload images dragged in or id of link_to widget to it's vfs upload. Can also be just a name for which content array contains a path to upload the picture.", + type: "string", + default: null + }, + file_picker_callback: { + name: "File picker callback", + description: "Callback function to get called when file picker is clicked", + type: 'js', + default: et2_no_init + }, + images_upload_handler: { + name: "Images upload handler", + description: "Callback function for handling image upload", + type: 'js', + default: et2_no_init + }, + menubar: { + name: "Menubar", + description: "Display menubar at the top of the editor", + type: "boolean", + default: true + }, + statusbar: { + name: "Status bar", + description: "Enable/disable status bar on the bottom of editor", + type: "boolean", + default: true + }, + valid_children: { + name: "Valid children", + description: "Enables to control what child tag is allowed or not allowed of the present tag. For instance: +body[style], makes style tag allowed inside body", + type: "string", + default: "+body[style]" + } + }; + + /** + * Array of toolbars + * @constant + */ + public static readonly TOOLBAR_LIST : string[] = ['undo', 'redo', 'formatselect', 'fontselect', 'fontsizeselect', + 'bold', 'italic', 'strikethrough', 'forecolor', 'backcolor', 'link', + 'alignleft', 'aligncenter', 'alignright', 'alignjustify', 'numlist', + 'bullist', 'outdent', 'indent', 'ltr', 'rtl', 'removeformat', 'code', 'image', 'searchreplace' + ]; + + /** + * arranged toolbars as simple mode + * @constant + */ + public static readonly TOOLBAR_SIMPLE : string = "undo redo|formatselect fontselect fontsizeselect | bold italic removeformat forecolor backcolor | "+ + "alignleft aligncenter alignright alignjustify | numlist "+ + "bullist outdent indent| link image pastetext"; + + /** + * arranged toolbars as extended mode + * @constant + */ + public static readonly TOOLBAR_EXTENDED : string = "fontselect fontsizeselect | bold italic strikethrough forecolor backcolor | "+ + "link | alignleft aligncenter alignright alignjustify | numlist "+ + "bullist outdent indent | removeformat | image | fullscreen"; + + /** + * arranged toolbars as advanced mode + * @constant + */ + public static readonly TOOLBAR_ADVANCED : string = "undo redo| formatselect | fontselect fontsizeselect | bold italic strikethrough forecolor backcolor | "+ + "link | alignleft aligncenter alignright alignjustify | numlist "+ + "bullist outdent indent ltr rtl | removeformat code| image | searchreplace | fullscreen"; + + /** + * font size formats + * @constant + */ + public static readonly FONT_SIZE_FORMATS : {pt : string, px : string} = { + pt: "8pt 9pt 10pt 11pt 12pt 14pt 16pt 18pt 20pt 22pt 24pt 26pt 28pt 36pt 48pt 72pt", + px: "8px 9px 10px 11px 12px 14px 16px 18px 20px 22px 24px 26px 28px 36px 48px 72px" + }; + + /** + * language code represention for TinyMCE lang code + */ + public static readonly LANGUAGE_CODE : {} = { + bg: "bg_BG", ca: "ca", cs: "cs", da: "da", de: "de", en:"en_CA", + el:"el", "es-es":"es", et: "et", eu: "eu" , fa: "fa_IR", fi: "fi", + fr: "fr_FR", hi:"", hr:"hr", hu:"hu_HU", id: "id", it: "it", iw: "", + ja: "ja", ko: "ko_KR", lo: "", lt: "lt", lv: "lv", nl: "nl", no: "nb_NO", + pl: "pl", pt: "pt_PT", "pt-br": "pt_BR", ru: "ru", sk: "sk", sl: "sl_SI", + sv: "sv_SE", th: "th_TH", tr: "tr_TR", uk: "en_GB", vi: "vi_VN", zh: "zh_CN", + "zh-tw": "zh_TW" + }; + + editor : any = null; + supportedWidgetClasses : any; + htmlNode : JQuery = null; + mode : string; + tinymce : any; + tinymce_container : HTMLElement; + file_picker_callback : Function; + menubar : boolean; + protected value : string; + + /** + * Constructor + */ + constructor(_parent, _attrs? : WidgetConfig, _child? : object) + { + // Call the inherited constructor + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_htmlarea._attributes, _child || {})); + this.editor = null; // TinyMce editor instance + this.supportedWidgetClasses = []; // Allow no child widgets + this.htmlNode = jQuery(document.createElement(this.options.readonly ? "div" : "textarea")) + .addClass('et2_textbox_ro'); + if(this.options.height) + { + this.htmlNode.css('height', this.options.height); + } + this.setDOMNode(this.htmlNode[0]); + } + + /** + * + * @returns {undefined} + */ + doLoadingFinished() + { + super.doLoadingFinished(); + this.init_editor(); + return true; + } + + init_editor() { + if(this.mode == 'ascii' || this.editor != null || this.options.readonly) return; + let imageUpload; + let self = this; + if (this.options.imageUpload && this.options.imageUpload[0] !== '/' && this.options.imageUpload.substr(0, 4) != 'http') + { + imageUpload = egw.ajaxUrl("EGroupware\\Api\\Etemplate\\Widget\\Vfs::ajax_htmlarea_upload")+ + '&request_id='+this.getInstanceManager().etemplate_exec_id+'&widget_id='+this.options.imageUpload+'&type=htmlarea'; + imageUpload = imageUpload.substr(egw.webserverUrl.length+1); + } + else if (imageUpload) + { + imageUpload = this.options.imageUpload.substr(egw.webserverUrl.length+1); + } + else + { + imageUpload = egw.ajaxUrl("EGroupware\\Api\\Etemplate\\Widget\\Vfs::ajax_htmlarea_upload")+ + '&request_id='+this.getInstanceManager().etemplate_exec_id+'&type=htmlarea'; + } + // default settings for initialization + let settings = { + target: this.htmlNode[0], + body_id: this.dom_id + '_htmlarea', + menubar: false, + statusbar: this.options.statusbar, + branding: false, + resize: false, + height: this.options.height, + width: this.options.width, + mobile: { + theme: 'silver' + }, + formats: { + customparagraph: { block: 'p', styles: {"margin-block-start": "0px", "margin-block-end": "0px"}} + }, + min_height: 100, + convert_urls: false, + language: et2_htmlarea.LANGUAGE_CODE[egw.preference('lang', 'common')], + language_url: egw.webserverUrl+'/api/js/tinymce/langs/'+et2_htmlarea.LANGUAGE_CODE[egw.preference('lang', 'common')]+'.js', + paste_data_images: true, + paste_filter_drop: true, + browser_spellcheck: true, + contextmenu: false, + images_upload_url: imageUpload, + file_picker_callback: jQuery.proxy(this._file_picker_callback, this), + images_upload_handler: this.options.images_upload_handler, + init_instance_callback : jQuery.proxy(this._instanceIsReady, this), + auto_focus: false, + valid_children : this.options.valid_children, + plugins: [ + "print searchreplace autolink directionality ", + "visualblocks visualchars image link media template fullscreen", + "codesample table charmap hr pagebreak nonbreaking anchor toc ", + "insertdatetime advlist lists textcolor wordcount imagetools ", + "colorpicker textpattern help paste code searchreplace tabfocus" + ], + toolbar: et2_htmlarea.TOOLBAR_SIMPLE, + block_formats: "Paragraph=p;Heading 1=h1;Heading 2=h2;Heading 3=h3;"+ + "Heading 4=h4;Heading 5=h5;Heading 6=h6;Preformatted=pre;Custom Paragraph=customparagraph", + font_formats: "Andale Mono=andale mono,times;Arial=arial,helvetica,"+ + "sans-serif;Arial Black=arial black,avant garde;Book Antiqua=book "+ + "antiqua,palatino;Comic Sans MS=comic sans ms,sans-serif;"+ + "Courier New=courier new,courier;Georgia=georgia,palatino;"+ + "Helvetica=helvetica;Impact=impact,chicago;Symbol=symbol;"+ + "Tahoma=tahoma,arial,helvetica,sans-serif;Terminal=terminal,"+ + "monaco;Times New Roman=times new roman,times;Trebuchet "+ + "MS=trebuchet ms,geneva;Verdana=verdana,geneva;Webdings=webdings;"+ + "Wingdings=wingdings,zapf dingbats", + fontsize_formats: '8pt 10pt 12pt 14pt 18pt 24pt 36pt', + setup : function(ed) + { + ed.on('init', function() + { + this.getDoc().body.style.fontSize = egw.preference('rte_font_size', 'common') + + egw.preference('rte_font_unit', 'common'); + this.getDoc().body.style.fontFamily = egw.preference('rte_font', 'common'); + }); + } + }; + + // extend default settings with configured options and preferences + jQuery.extend(settings, this._extendedSettings()); + this.tinymce = tinymce.init(settings); + // make sure value gets set in case of widget gets loaded by delay like + // inside an inactive tabs + this.tinymce.then(function() { + self.set_value(self.htmlNode.val()); + if (self.editor && self.editor.editorContainer) + { + self.editor.formatter.toggle(egw.preference('rte_formatblock', 'common')); + jQuery(self.editor.editorContainer).height(self.options.height); + jQuery(self.editor.iframeElement.contentWindow.document).on('dragenter', function(){ + if (jQuery('#dragover-tinymce').length < 1) jQuery("").appendTo('head'); + }); + } + }); + } + + /** + * set disabled + * + * @param {type} _value + * @returns {undefined} + */ + set_disabled(_value) + { + super.set_disabled(_value); + if (_value) + { + jQuery(this.tinymce_container).css('display', 'none'); + } + else + { + jQuery(this.tinymce_container).css('display', 'flex'); + } + } + + set_readonly(_value) + { + if(this.options.readonly === _value) return; + let value = this.get_value(); + this.options.readonly = _value; + if(this.options.readonly) + { + if (this. editor) this.editor.remove(); + this.htmlNode = jQuery(document.createElement(this.options.readonly ? "div" : "textarea")) + .addClass('et2_textbox_ro'); + if(this.options.height) + { + this.htmlNode.css('height', this.options.height) + } + this.editor = null; + this.setDOMNode(this.htmlNode[0]); + this.set_value(value); + } + else + { + if(!this.editor) + { + this.htmlNode = jQuery(document.createElement("textarea")) + .val(value); + if(this.options.height || this.options.editable_height) + { + this.htmlNode.css('height', (this.options.editable_height ? this.options.editable_height : this.options.height)); + } + this.setDOMNode(this.htmlNode[0]); + this.init_editor(); + } + } + } + + /** + * Callback function runs when the filepicker in image dialog is clicked + * + * @param {type} _callback + * @param {type} _value + * @param {type} _meta + */ + private _file_picker_callback(_callback : Function, _value, _meta) + { + if (typeof this.file_picker_callback == 'function') return this.file_picker_callback.call(arguments, this); + let callback = _callback; + + // Don't rely only on app_name to fetch et2 object as app_name may not + // always represent current app of the window, e.g.: mail admin account. + // Try to fetch et2 from its template name. + let etemplate = jQuery('form').data('etemplate'); + let et2; + if (etemplate && etemplate.name && !app[egw(window).app_name()]) + { + et2 = etemplate2.getByTemplate(etemplate.name)[0]['widgetContainer']; + } + else + { + et2 = app[egw(window).app_name()].et2; + } + + let vfsSelect = et2_createWidget('vfs-select', { + id:'upload', + mode: 'open', + name: '', + button_caption:"Link", + button_label:"Link", + dialog_title: "Link file", + method: "download" + }, et2); + jQuery(vfsSelect.getDOMNode()).on('change', function (){ + callback(vfsSelect.get_value(), {alt:vfsSelect.get_value()}); + }); + + // start the file selector dialog + vfsSelect.click(); + } + + /** + * Callback when instance is ready + * + * @param {type} _editor + */ + private _instanceIsReady(_editor) + { + console.log("Editor: " + _editor.id + " is now initialized."); + // try to reserve focus state as running command on editor may steal the + // current focus. + let focusedEl = jQuery(':focus'); + this.editor = _editor; + + this.editor.on('drop', function(e){ + e.preventDefault(); + }); + + if (!this.disabled) jQuery(this.editor.editorContainer).css('display', 'flex'); + this.tinymce_container = this.editor.editorContainer; + // go back to reserved focused element + focusedEl.focus(); + } + + /** + * Takes all relevant preferences into account and set settings accordingly + * + * @returns {object} returns a object including all settings + */ + private _extendedSettings() : object + { + let rte_menubar = egw.preference('rte_menubar', 'common'); + let rte_toolbar = egw.preference('rte_toolbar', 'common'); + // we need to have rte_toolbar values as an array + if (rte_toolbar && typeof rte_toolbar == "object") + { + rte_toolbar = Object.keys(rte_toolbar).map(function(key){return rte_toolbar[key]}); + } + let settings = { + fontsize_formats: et2_htmlarea.FONT_SIZE_FORMATS[egw.preference('rte_font_unit', 'common')], + menubar: parseInt(rte_menubar) && this.menubar ? true : typeof rte_menubar != 'undefined' ? false : this.menubar + }; + + switch (this.mode) + { + case 'simple': + settings['toolbar'] = et2_htmlarea.TOOLBAR_SIMPLE; + break; + case 'extended': + settings['toolbar']= et2_htmlarea.TOOLBAR_EXTENDED; + break; + case 'advanced': + settings['toolbar'] = et2_htmlarea.TOOLBAR_ADVANCED; + break; + default: + this.mode = ''; + } + + // take rte_toolbar into account if no mode restrictly set from template + if (rte_toolbar && !this.mode) + { + let toolbar_diff = et2_htmlarea.TOOLBAR_LIST.filter(function(i){return !((rte_toolbar).indexOf(i) > -1);}); + settings['toolbar'] = et2_htmlarea.TOOLBAR_ADVANCED; + toolbar_diff.forEach(function(a){ + let r = new RegExp(a); + settings['toolbar'] = settings['toolbar'].replace(r, ''); + }); + } + return settings; + } + + destroy() + { + if (this.editor) + { + this.editor.destroy(); + } + this.editor = null; + this.tinymce = null; + this.tinymce_container = null; + this.htmlNode.remove(); + this.htmlNode = null; + super.destroy(); + } + set_value(_value) + { + this._oldValue = _value; + if (this.editor) + { + this.editor.setContent(_value); + } + else + { + if(this.options.readonly) + { + this.htmlNode.empty().append(_value); + } + else + { + this.htmlNode.val(_value); + } + } + this.value = _value; + } + + getValue() + { + return this.editor ? this.editor.getContent() : ( + this.options.readonly ? this.value : this.htmlNode.val() + ); + } + + /** + * Resize htmlNode tag according to window size + * @param {type} _height excess height which comes from window resize + */ + resize(_height) + { + if (_height && this.options.resize_ratio !== '0') + { + // apply the ratio + _height = (this.options.resize_ratio != '')? _height * this.options.resize_ratio: _height; + if (_height != 0) + { + if (this.editor) // TinyMCE HTML + { + let h; + if (typeof this.editor.iframeElement !='undefined' && this.editor.editorContainer.clientHeight > 0) + { + h = (this.editor.editorContainer.clientHeight + _height) > 0 ? + (this.editor.editorContainer.clientHeight) + _height: this.editor.settings.min_height; + } + else // fallback height size + { + h = this.editor.settings.min_height + _height; + } + jQuery(this.editor.editorContainer).height(h); + jQuery(this.editor.iframeElement).height(h - (this.editor.editorContainer.getElementsByClassName('tox-toolbar')[0].clientHeight + + this.editor.editorContainer.getElementsByClassName('tox-statusbar')[0].clientHeight)); + } + else // No TinyMCE + { + this.htmlNode.height(this.htmlNode.height() + _height); + } + } + } + } +} +et2_register_widget(et2_htmlarea, ["htmlarea"]); \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_iframe.js b/api/js/etemplate/et2_widget_iframe.js index 3ad3b51446..27d60f33d2 100644 --- a/api/js/etemplate/et2_widget_iframe.js +++ b/api/js/etemplate/et2_widget_iframe.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - JS widget class for an iframe * @@ -7,154 +8,159 @@ * @link http://www.egroupware.org * @author Nathan Gray * @copyright Nathan Gray 2013 - * @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_valueWidget; */ - +var et2_core_valueWidget_1 = require("./et2_core_valueWidget"); +var et2_core_widget_1 = require("./et2_core_widget"); +var et2_core_inheritance_1 = require("./et2_core_inheritance"); /** * @augments et2_valueWidget */ -var et2_iframe = (function(){ "use strict"; return et2_valueWidget.extend( -{ - attributes: { - 'label': { - 'default': "", - description: "The label is displayed by default in front (for radiobuttons behind) each widget (if not empty). If you want to specify a different position, use a '%s' in the label, which gets replaced by the widget itself. Eg. '%s Name' to have the label Name behind a checkbox. The label can contain variables, as descript for name. If the label starts with a '@' it is replaced by the value of the content-array at this index (with the '@'-removed and after expanding the variables).", - ignore: false, - name: "Label", - translate: true, - type: "string" - }, - "needed": { - "ignore": true - }, - "seamless": { - name: "Seamless", - 'default': true, - description: "Specifies that the iframe should be rendered in a manner that makes it appear to be part of the containing document", - translate: false, - type: "boolean" - }, - "name": { - name: "Name", - "default": "", - description: "Specifies name of frame, to be used as target for links", - type: "string" - }, - fullscreen: { - name: "Fullscreen", - "default": false, - description: "Make the iframe compatible to be a fullscreen video player mode", - type: "boolean" - }, - src: { - name: "Source", - "default": "", - description: "Specifies URL for the iframe", - type: "string" - } - }, - - /** - * Constructor - * - * @memberOf et2_iframe - */ - init: function() { - this._super.apply(this, arguments); - - // Allow no child widgets - this.supportedWidgetClasses = []; - - this.htmlNode = jQuery(document.createElement("iframe")); - if(this.options.label) - { - this.htmlNode.append(''+this.options.label+''); - } - if (this.options.fullscreen) - { - this.htmlNode.attr('allowfullscreen', true); - } - this.setDOMNode(this.htmlNode[0]); - }, - - /** - * Set name of iframe (to be used as target for links) - * - * @param _name - */ - set_name: function(_name) { - this.htmlNode.attr('name', this.htmlNode.name = _name); - }, - - /** - * Make it look like part of the containing document - * - * @param _seamless boolean - */ - set_seamless: function(_seamless) { - this.options.seamless = _seamless; - this.htmlNode.attr("seamless", _seamless); - }, - - set_value: function(_value) { - if(typeof _value == "undefined") _value = ""; - - if(_value.trim().indexOf("http") == 0 || _value.indexOf('about:') == 0 || _value[0] == '/') - { - // Value is a URL - this.set_src(_value); - } - else - { - // Value is content - this.set_srcdoc(_value); - } - }, - - /** - * Set the URL for the iframe - * - * Sets the src attribute to the given value - * - * @param _value String URL - */ - set_src: function(_value) { - if(_value.trim() != "") - { - if(_value.trim() == 'about:blank') - { - this.htmlNode.attr("src", _value); - } - else - { - // Load the new page, but display a loader - var loader = jQuery('
          '); - this.htmlNode - .before(loader); - window.setTimeout(jQuery.proxy(function() { - this.htmlNode.attr("src", _value) - .one('load',function() { - loader.remove(); - }); - },this),0); - - } - } - }, - - /** - * Sets the content of the iframe - * - * Sets the srcdoc attribute to the given value - * - * @param _value String Content of a document - */ - set_srcdoc: function(_value) { - this.htmlNode.attr("srcdoc", _value); - } -});}).call(this); -et2_register_widget(et2_iframe, ["iframe"]); +var et2_iframe = /** @class */ (function (_super) { + __extends(et2_iframe, _super); + /** + * Constructor + * + * @memberOf et2_iframe + */ + function et2_iframe(_parent, _attrs, _child) { + var _this = + // Call the inherited constructor + _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_iframe._attributes, _child || {})) || this; + _this.htmlNode = null; + // Allow no child widgets + _this.supportedWidgetClasses = []; + _this.htmlNode = jQuery(document.createElement("iframe")); + if (_this.options.label) { + _this.htmlNode.append('' + _this.options.label + ''); + } + if (_this.options.fullscreen) { + _this.htmlNode.attr('allowfullscreen', 1); + } + _this.setDOMNode(_this.htmlNode[0]); + return _this; + } + /** + * Set name of iframe (to be used as target for links) + * + * @param _name + */ + et2_iframe.prototype.set_name = function (_name) { + this.options.name = _name; + this.htmlNode.attr('name', _name); + }; + /** + * Make it look like part of the containing document + * + * @param _seamless boolean + */ + et2_iframe.prototype.set_seamless = function (_seamless) { + this.options.seamless = _seamless; + this.htmlNode.attr("seamless", _seamless); + }; + et2_iframe.prototype.set_value = function (_value) { + if (typeof _value == "undefined") + _value = ""; + if (_value.trim().indexOf("http") == 0 || _value.indexOf('about:') == 0 || _value[0] == '/') { + // Value is a URL + this.set_src(_value); + } + else { + // Value is content + this.set_srcdoc(_value); + } + }; + /** + * Set the URL for the iframe + * + * Sets the src attribute to the given value + * + * @param _value String URL + */ + et2_iframe.prototype.set_src = function (_value) { + if (_value.trim() != "") { + if (_value.trim() == 'about:blank') { + this.htmlNode.attr("src", _value); + } + else { + // Load the new page, but display a loader + var loader_1 = jQuery('
          '); + this.htmlNode + .before(loader_1); + window.setTimeout(jQuery.proxy(function () { + this.htmlNode.attr("src", _value) + .one('load', function () { + loader_1.remove(); + }); + }, this), 0); + } + } + }; + /** + * Sets the content of the iframe + * + * Sets the srcdoc attribute to the given value + * + * @param _value String Content of a document + */ + et2_iframe.prototype.set_srcdoc = function (_value) { + this.htmlNode.attr("srcdoc", _value); + }; + et2_iframe._attributes = { + 'label': { + 'default': "", + description: "The label is displayed by default in front (for radiobuttons behind) each widget (if not empty). If you want to specify a different position, use a '%s' in the label, which gets replaced by the widget itself. Eg. '%s Name' to have the label Name behind a checkbox. The label can contain variables, as descript for name. If the label starts with a '@' it is replaced by the value of the content-array at this index (with the '@'-removed and after expanding the variables).", + ignore: false, + name: "Label", + translate: true, + type: "string" + }, + "needed": { + "ignore": true + }, + "seamless": { + name: "Seamless", + 'default': true, + description: "Specifies that the iframe should be rendered in a manner that makes it appear to be part of the containing document", + translate: false, + type: "boolean" + }, + "name": { + name: "Name", + "default": "", + description: "Specifies name of frame, to be used as target for links", + type: "string" + }, + fullscreen: { + name: "Fullscreen", + "default": false, + description: "Make the iframe compatible to be a fullscreen video player mode", + type: "boolean" + }, + src: { + name: "Source", + "default": "", + description: "Specifies URL for the iframe", + type: "string" + } + }; + return et2_iframe; +}(et2_core_valueWidget_1.et2_valueWidget)); +et2_core_widget_1.et2_register_widget(et2_iframe, ["iframe"]); +//# sourceMappingURL=et2_widget_iframe.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_iframe.ts b/api/js/etemplate/et2_widget_iframe.ts new file mode 100644 index 0000000000..550f164f70 --- /dev/null +++ b/api/js/etemplate/et2_widget_iframe.ts @@ -0,0 +1,174 @@ +/** + * EGroupware eTemplate2 - JS widget class for an iframe + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage api + * @link http://www.egroupware.org + * @author Nathan Gray + * @copyright Nathan Gray 2013 + */ + +/*egw:uses + et2_core_valueWidget; +*/ + +import {et2_valueWidget} from "./et2_core_valueWidget"; +import {et2_register_widget, WidgetConfig} from "./et2_core_widget"; +import {ClassWithAttributes} from "./et2_core_inheritance"; + +/** + * @augments et2_valueWidget + */ +class et2_iframe extends et2_valueWidget +{ + static readonly _attributes : any = { + 'label': { + 'default': "", + description: "The label is displayed by default in front (for radiobuttons behind) each widget (if not empty). If you want to specify a different position, use a '%s' in the label, which gets replaced by the widget itself. Eg. '%s Name' to have the label Name behind a checkbox. The label can contain variables, as descript for name. If the label starts with a '@' it is replaced by the value of the content-array at this index (with the '@'-removed and after expanding the variables).", + ignore: false, + name: "Label", + translate: true, + type: "string" + }, + "needed": { + "ignore": true + }, + "seamless": { + name: "Seamless", + 'default': true, + description: "Specifies that the iframe should be rendered in a manner that makes it appear to be part of the containing document", + translate: false, + type: "boolean" + }, + "name": { + name: "Name", + "default": "", + description: "Specifies name of frame, to be used as target for links", + type: "string" + }, + fullscreen: { + name: "Fullscreen", + "default": false, + description: "Make the iframe compatible to be a fullscreen video player mode", + type: "boolean" + }, + src: { + name: "Source", + "default": "", + description: "Specifies URL for the iframe", + type: "string" + } + }; + + protected htmlNode : JQuery = null; + + /** + * Constructor + * + * @memberOf et2_iframe + */ + constructor(_parent, _attrs? : WidgetConfig, _child? : object) + { + // Call the inherited constructor + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_iframe._attributes, _child || {})); + + // Allow no child widgets + this.supportedWidgetClasses = []; + + this.htmlNode = jQuery(document.createElement("iframe")); + if(this.options.label) + { + this.htmlNode.append(''+this.options.label+''); + } + if (this.options.fullscreen) + { + this.htmlNode.attr('allowfullscreen', 1); + } + this.setDOMNode(this.htmlNode[0]); + } + + /** + * Set name of iframe (to be used as target for links) + * + * @param _name + */ + set_name(_name) + { + this.options.name = _name; + this.htmlNode.attr('name', _name); + } + + /** + * Make it look like part of the containing document + * + * @param _seamless boolean + */ + set_seamless(_seamless) + { + this.options.seamless = _seamless; + this.htmlNode.attr("seamless", _seamless); + } + + set_value(_value) + { + if(typeof _value == "undefined") _value = ""; + + if(_value.trim().indexOf("http") == 0 || _value.indexOf('about:') == 0 || _value[0] == '/') + { + // Value is a URL + this.set_src(_value); + } + else + { + // Value is content + this.set_srcdoc(_value); + } + } + + /** + * Set the URL for the iframe + * + * Sets the src attribute to the given value + * + * @param _value String URL + */ + set_src(_value) + { + if(_value.trim() != "") + { + if(_value.trim() == 'about:blank') + { + this.htmlNode.attr("src", _value); + } + else + { + // Load the new page, but display a loader + let loader = jQuery('
          '); + this.htmlNode + .before(loader); + window.setTimeout(jQuery.proxy(function() { + this.htmlNode.attr("src", _value) + .one('load',function() { + loader.remove(); + }); + },this),0); + + } + } + } + + /** + * Sets the content of the iframe + * + * Sets the srcdoc attribute to the given value + * + * @param _value String Content of a document + */ + set_srcdoc(_value) + { + this.htmlNode.attr("srcdoc", _value); + } +} +et2_register_widget(et2_iframe, ["iframe"]); + diff --git a/api/js/etemplate/et2_widget_image.js b/api/js/etemplate/et2_widget_image.js index 4370c1a6d9..8dbf4bd513 100644 --- a/api/js/etemplate/et2_widget_image.js +++ b/api/js/etemplate/et2_widget_image.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - JS Description object * @@ -9,699 +10,602 @@ * @copyright Nathan Gray 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 - /vendor/bower-asset/jquery/dist/jquery.js; - et2_core_interfaces; - et2_core_baseWidget; - expose; - /vendor/bower-asset/cropper/dist/cropper.min.js; + /vendor/bower-asset/jquery/dist/jquery.js; + et2_core_interfaces; + et2_core_baseWidget; + expose; + /vendor/bower-asset/cropper/dist/cropper.min.js; */ - +var et2_core_baseWidget_1 = require("./et2_core_baseWidget"); +var et2_core_widget_1 = require("./et2_core_widget"); +var et2_core_inheritance_1 = require("./et2_core_inheritance"); /** * Class which implements the "image" XET-Tag * * @augments et2_baseWidget */ -var et2_image = (function(){ "use strict"; return expose(et2_baseWidget.extend([et2_IDetachedDOM], -{ - attributes: { - "src": { - "name": "Image", - "type": "string", - "description": "Displayed image" - }, - default_src: { - name: "Default image", - type: "string", - description: "Image to use if src is not found" - }, - "href": { - "name": "Link Target", - "type": "string", - "description": "Link URL, empty if you don't wan't to display a link.", - "default": et2_no_init - }, - "extra_link_target": { - "name": "Link target", - "type": "string", - "default": "_self", - "description": "Link target descriptor" - }, - "extra_link_popup": { - "name": "Popup", - "type": "string", - "description": "widthxheight, if popup should be used, eg. 640x480" - }, - "imagemap":{ - // TODO: Do something with this - "name": "Image map", - "description": "Currently not implemented" - }, - "label": { - "name": "Label", - "type": "string", - "description": "Label for image" - }, - "expose_view":{ - name: "Expose view", - type: "boolean", - default: false, - description: "Clicking on an image with href value would popup an expose view, and will show image referenced by href." - } - }, - legacyOptions: ["href", "extra_link_target", "imagemap", "extra_link_popup", "id"], - - /** - * Constructor - * - * @memberOf et2_image - */ - init: function() { - this._super.apply(this, arguments); - - // Create the image or a/image tag - this.image = jQuery(document.createElement("img")); - if (this.options.label) - { - this.image.attr("alt", this.options.label).attr("title", this.options.label); - } - if (this.options.href) - { - this.image.addClass('et2_clickable'); - } - if(this.options["class"]) - { - this.image.addClass(this.options["class"]); - } - this.setDOMNode(this.image[0]); - }, - - click: function() - { - if(this.options.href) - { - this.egw().open_link(this.options.href, this.options.extra_link_target, this.options.extra_link_popup); - } - else - { - this._super.apply(this,arguments); - } - }, - - transformAttributes: function(_attrs) { - this._super.apply(arguments); - - // Check to expand name - if (typeof _attrs["src"] != "undefined") - { - var manager = this.getArrayMgr("content"); - if(manager) { - var src = manager.getEntry(_attrs["src"]); - if (typeof src != "undefined" && src !== null) - { - if(typeof src == "object") - { - src = egw().link('/index.php', src); - } - _attrs["src"] = src; - } - } - } - }, - - set_label: function(_value) { - this.options.label = _value; - _value = this.egw().lang(_value); - // label is NOT the alt attribute in eTemplate, but the title/tooltip - this.image.attr("alt", _value).attr("title", _value); - }, - - setValue: function(_value) { - // Value is src, images don't get IDs - this.set_src(_value); - }, - - set_href: function (_value) - { - if (!this.isInTree()) - { - return false; - } - - this.options.href = _value; - this.image.wrapAll('"'); - - var href = this.options.href; - var popup = this.options.extra_link_popup; - var target = this.options.extra_link_target; - var self = this; - this.image.click(function(e) - { - if (self.options.expose_view) - { - self._init_blueimp_gallery(e,_value); - e.stopImmediatePropagation(); - } - else - { - egw.open_link(href,target,popup); - } - - e.preventDefault(); - return false; - }); - - return true; - }, - - /** - * Set image src - * - * @param {string} _value image, app/image or url - * @return {boolean} true if image was found, false if not (image is either not displayed or default_src is used) - */ - set_src: function(_value) { - if(!this.isInTree()) - { - return false; - } - - this.options.src = _value; - - // allow url's too - if (_value[0] == '/' || _value.substr(0,4) == 'http' || _value.substr(0,5) == 'data:') - { - this.image.attr('src', _value).show(); - return true; - } - var src = this.egw().image(_value); - if (src) - { - this.image.attr("src", src).show(); - return true; - } - src = null; - if (this.options.default_src) - { - src = this.egw().image(this.options.default_src); - } - if (src) - { - this.image.attr("src", src).show(); - } - else - { - this.image.css("display","none"); - } - return false; - }, - - /** - * Function to get media content to feed the expose - * @param {type} _value - * @returns {Array|Array.getMedia.mediaContent} - */ - getMedia: function (_value) - { - var base_url = egw.webserverUrl.match(/^\//,'ig')?egw(window).window.location.origin + egw.webserverUrl + '/':egw.webserverUrl + '/'; - var mediaContent = []; - if (_value) - { - mediaContent = [{ - title: this.options.label, - href: base_url + _value, - type: this.options.type + "/*", - thumbnail: base_url + _value - }]; - } - return mediaContent; - }, - - /** - * Implementation of "et2_IDetachedDOM" for fast viewing in gridview - * - * @param {array} _attrs - */ - getDetachedAttributes: function(_attrs) { - _attrs.push("src", "label", "href"); - }, - - getDetachedNodes: function() { - return [this.image[0]]; - }, - - setDetachedAttributes: function(_nodes, _values) { - // Set the given DOM-Nodes - this.image = jQuery(_nodes[0]); - - // Set the attributes - if (_values["src"]) - { - this.set_src(_values["src"]); - } - // Not valid, but we'll deal - if (_values["value"]) - { - this.setValue(_values["value"]); - } - - if (_values["label"]) - { - this.set_label(_values["label"]); - } - if(_values["href"]) - { - this.image.addClass('et2_clickable'); - this.set_href(_values["href"]); - } - } -}));}).call(this); - -et2_register_widget(et2_image, ["image"]); - +var et2_image = /** @class */ (function (_super) { + __extends(et2_image, _super); + /** + * Constructor + */ + function et2_image(_parent, _attrs, _child) { + var _this = + // Call the inherited constructor + _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_image._attributes, _child || {})) || this; + _this.legacyOptions = ["href", "extra_link_target", "imagemap", "extra_link_popup", "id"]; + _this.image = null; + // Create the image or a/image tag + _this.image = jQuery(document.createElement("img")); + if (_this.options.label) { + _this.image.attr("alt", _this.options.label).attr("title", _this.options.label); + } + if (_this.options.href) { + _this.image.addClass('et2_clickable'); + } + if (_this.options["class"]) { + _this.image.addClass(_this.options["class"]); + } + _this.setDOMNode(_this.image[0]); + return _this; + } + et2_image.prototype.click = function (_ev) { + if (this.options.href) { + this.egw().open_link(this.options.href, this.options.extra_link_target, this.options.extra_link_popup); + } + else { + _super.prototype.click.call(this, _ev); + } + }; + et2_image.prototype.transformAttributes = function (_attrs) { + _super.prototype.transformAttributes.call(this, _attrs); + // Check to expand name + if (typeof _attrs["src"] != "undefined") { + var manager = this.getArrayMgr("content"); + if (manager) { + var src = manager.getEntry(_attrs["src"]); + if (typeof src != "undefined" && src !== null) { + if (typeof src == "object") { + src = egw().link('/index.php', src); + } + _attrs["src"] = src; + } + } + } + }; + et2_image.prototype.set_label = function (_value) { + this.options.label = _value; + _value = this.egw().lang(_value); + // label is NOT the alt attribute in eTemplate, but the title/tooltip + this.image.attr("alt", _value).attr("title", _value); + }; + et2_image.prototype.setValue = function (_value) { + // Value is src, images don't get IDs + this.set_src(_value); + }; + et2_image.prototype.set_href = function (_value) { + if (!this.isInTree()) { + return false; + } + this.options.href = _value; + this.image.wrapAll('"'); + var href = this.options.href; + var popup = this.options.extra_link_popup; + var target = this.options.extra_link_target; + var self = this; + this.image.click(function (e) { + if (self.options.expose_view) { + /* + TODO: Fix after implementing EXPOSE mixin class + */ + //self._init_blueimp_gallery(e,_value); + e.stopImmediatePropagation(); + } + else { + egw.open_link(href, target, popup); + } + e.preventDefault(); + return false; + }); + return true; + }; + /** + * Set image src + * + * @param {string} _value image, app/image or url + * @return {boolean} true if image was found, false if not (image is either not displayed or default_src is used) + */ + et2_image.prototype.set_src = function (_value) { + if (!this.isInTree()) { + return false; + } + this.options.src = _value; + // allow url's too + if (_value[0] == '/' || _value.substr(0, 4) == 'http' || _value.substr(0, 5) == 'data:') { + this.image.attr('src', _value).show(); + return true; + } + var src = this.egw().image(_value); + if (src) { + this.image.attr("src", src).show(); + return true; + } + src = null; + if (this.options.default_src) { + src = this.egw().image(this.options.default_src); + } + if (src) { + this.image.attr("src", src).show(); + } + else { + this.image.css("display", "none"); + } + return false; + }; + /** + * Function to get media content to feed the expose + * @param {type} _value + */ + et2_image.prototype.getMedia = function (_value) { + var base_url = egw.webserverUrl.match(/^\/ig/) ? egw(window).window.location.origin + egw.webserverUrl + '/' : egw.webserverUrl + '/'; + var mediaContent = []; + if (_value) { + mediaContent = [{ + title: this.options.label, + href: base_url + _value, + type: this.options.type + "/*", + thumbnail: base_url + _value + }]; + } + return mediaContent; + }; + /** + * Implementation of "et2_IDetachedDOM" for fast viewing in gridview + * + * @param {array} _attrs + */ + et2_image.prototype.getDetachedAttributes = function (_attrs) { + _attrs.push("src", "label", "href"); + }; + et2_image.prototype.getDetachedNodes = function () { + return [this.image[0]]; + }; + et2_image.prototype.setDetachedAttributes = function (_nodes, _values) { + // Set the given DOM-Nodes + this.image = jQuery(_nodes[0]); + // Set the attributes + if (_values["src"]) { + this.set_src(_values["src"]); + } + // Not valid, but we'll deal + if (_values["value"]) { + this.setValue(_values["value"]); + } + if (_values["label"]) { + this.set_label(_values["label"]); + } + if (_values["href"]) { + this.image.addClass('et2_clickable'); + this.set_href(_values["href"]); + } + }; + et2_image._attributes = { + "src": { + "name": "Image", + "type": "string", + "description": "Displayed image" + }, + default_src: { + name: "Default image", + type: "string", + description: "Image to use if src is not found" + }, + "href": { + "name": "Link Target", + "type": "string", + "description": "Link URL, empty if you don't wan't to display a link.", + "default": et2_no_init + }, + "extra_link_target": { + "name": "Link target", + "type": "string", + "default": "_self", + "description": "Link target descriptor" + }, + "extra_link_popup": { + "name": "Popup", + "type": "string", + "description": "widthxheight, if popup should be used, eg. 640x480" + }, + "imagemap": { + // TODO: Do something with this + "name": "Image map", + "description": "Currently not implemented" + }, + "label": { + "name": "Label", + "type": "string", + "description": "Label for image" + }, + "expose_view": { + name: "Expose view", + type: "boolean", + default: false, + description: "Clicking on an image with href value would popup an expose view, and will show image referenced by href." + } + }; + return et2_image; +}(et2_core_baseWidget_1.et2_baseWidget)); +et2_core_widget_1.et2_register_widget(et2_image, ["image"]); /** - * Widget displaying an application icon - */ -var et2_appicon = (function(){ "use strict"; return et2_image.extend( -{ - attributes: { - default_src: { - name: "Default image", - type: "string", - default: "nonav", - description: "Image to use if there is no application icon" - } - }, - - set_src: function(_app) - { - if (!_app) _app = this.egw().app_name(); - this.image.addClass('et2_appicon'); - this._super.call(this, _app == 'sitemgr-link' ? 'sitemgr/sitemgr-link' : // got removed from jdots - (this.egw().app(_app, 'icon_app') || _app)+'/'+(this.egw().app(_app, 'icon') || 'navbar')); - } -});}).call(this); -et2_register_widget(et2_appicon, ["appicon"]); - +* Widget displaying an application icon +*/ +var et2_appicon = /** @class */ (function (_super) { + __extends(et2_appicon, _super); + function et2_appicon() { + return _super !== null && _super.apply(this, arguments) || this; + } + et2_appicon.prototype.set_src = function (_app) { + if (!_app) + _app = this.egw().app_name(); + this.image.addClass('et2_appicon'); + return _super.prototype.set_src.call(this, _app == 'sitemgr-link' ? 'sitemgr/sitemgr-link' : // got removed from jdots + (this.egw().app(_app, 'icon_app') || _app) + '/' + (this.egw().app(_app, 'icon') || 'navbar')); + }; + et2_appicon._attributes = { + default_src: { + name: "Default image", + type: "string", + default: "nonav", + description: "Image to use if there is no application icon" + } + }; + return et2_appicon; +}(et2_image)); +et2_core_widget_1.et2_register_widget(et2_appicon, ["appicon"]); /** - * Avatar widget to display user profile picture or - * user letter avatar based on user's firstname lastname. - * - * @augments et2_baseWidget - */ - -var et2_avatar = (function(){ "use strict"; return et2_image.extend( -{ - attributes: { - "contact_id": { - name: "Contact id", - type: "string", - default: "", - description: "Contact id should be either user account_id {account:number} or contact_id {contact:number or number}" - }, - "default_src": { - "ignore": true - }, - "frame": { - name: "Avatar frame", - type: "string", - default: "circle", - description: "Define the shape of frame that avatar will be shown inside it. it can get {circle,rectangle} values which default value is cicle." - }, - editable: { - name: "Edit avatar", - type: "boolean", - default: false, - description: "Make avatar widget editable to be able to crop profile picture or upload a new photo" - }, - crop: { - name: "Crop avatar", - type: "boolean", - default: false, - description: "Create crop container and cropping feature" - } - }, - - init: function () - { - this._super.apply(this,arguments); - if (this.options.frame == 'circle') - { - this.image.attr('style', 'border-radius:50%'); - } - if (this.options.contact_id) this.setValue(this.options.contact_id); - }, - - /** - * Function to set contact id - * contact id could be in one of these formats: - * 'number', will be consider as contact_id - * 'contact:number', similar to above - * 'account:number', will be consider as account id - * @example: contact_id = "account:4" - * - * @param {string} _contact_id contact id could be as above mentioned formats - */ - set_contact_id: function(_contact_id) - { - var params = {}; - var id = 'contact_id'; - - this.image.addClass('et2_avatar'); - - if (!_contact_id) - { - _contact_id = this.egw().user('account_id'); - } - else if(_contact_id.match(/account\:/)) - { - id = 'account_id'; - _contact_id = _contact_id.replace('account:',''); - } - else - { - id = 'contact_id'; - _contact_id = _contact_id.replace('contact:', ''); - } - - // if our src (incl. cache-buster) already includes the correct id, use that one - if (this.options.src && this.options.src.match("(&|\\?)contact_id="+_contact_id+"(&|\\$)")) - { - return; - } - - // we have only the id, so we need to bypass caching with a cache-buster - params[id] = _contact_id; - params._cache = (new Date).getTime(); - - var url = egw.link('/api/avatar.php',params); - this.set_src(url); - }, - - /** - * Function to set value - * @param {string} _value - */ - setValue: function(_value) - { - this.set_contact_id(_value); - }, - - /** - * Implementation of "et2_IDetachedDOM" for fast viewing in gridview - * - * @param {array} _attrs - */ - getDetachedAttributes: function(_attrs) { - _attrs.push("contact_id", "label", "href"); - }, - - setDetachedAttributes: function (_nodes, _values) - { - // Set the given DOM-Nodes - this.image = jQuery(_nodes[0]); - - if (_values["contact_id"]) - { - this.set_contact_id(_values["contact_id"]); - } - - if (_values["label"]) - { - this.set_label(_values["label"]); - } - if(_values["href"]) - { - this.image.addClass('et2_clickable'); - this.set_href(_values["href"]); - } - }, - - /** - * Build Editable Mask Layer (EML) in order to show edit/delete actions - * on top of profile picture. - * @param {boolean} _noDelete disable delete button in initialization - */ - _buildEditableLayer: function (_noDelete) - { - var self = this; - // editable mask layer (eml) - var eml = jQuery(document.createElement('div')) - .addClass('eml') - .insertAfter(this.image); - - // edit button - var edit = jQuery(document.createElement('div')) - .addClass('emlEdit') - .click(function(){ - var buttons = [ - {"button_id": 1,"text": self.egw().lang('save'), id: 'save', image: 'check', "default":true}, - {"button_id": 0,"text": self.egw().lang('cancel'), id: 'cancel', image: 'cancelled'} - ]; - var dialog = function(_title, _value, _buttons, _egw_or_appname) - { - return et2_createWidget("dialog", - { - callback: function(_buttons, _value) - { - if (_buttons == 'save') - { - var canvas = jQuery('#_cropper_image').cropper('getCroppedCanvas'); - self.image.attr('src', canvas.toDataURL("image/jpeg", 1.0)); - self.egw().json('addressbook.addressbook_ui.ajax_update_photo', - [self.getInstanceManager().etemplate_exec_id, canvas.toDataURL('image/jpeg',1.0)], - function(res) - { - if (res) - { - del.show(); - } - }).sendRequest(); - } - }, - title: _title||egw.lang('Input required'), - buttons: _buttons||et2_dialog.BUTTONS_OK_CANCEL, - value: { - content: _value - }, - width: "90%", - height:"450", - resizable: false, - position:"top+10", - template: egw.webserverUrl+'/api/templates/default/avatar_edit.xet?2' - }, et2_dialog._create_parent(_egw_or_appname)); - }; - - dialog(egw.lang('Edit avatar'),{photo:self.options.contact_id},buttons); - }) - .appendTo(eml); - - // delete button - var del = jQuery(document.createElement('div')) - .addClass('emlDelete') - .click(function(){ - et2_dialog.show_dialog(function(_btn){ - if (_btn == et2_dialog.YES_BUTTON) - { - self.egw().json('addressbook.addressbook_ui.ajax_update_photo', - [self.getInstanceManager().etemplate_exec_id, null], - function(res) - { - if (res) - { - self.image.attr('src',''); - del.hide(); - egw.refresh('Avatar Deleted.', egw.app_name()); - } - }).sendRequest(); - } - }, egw.lang('Delete this photo?'), egw.lang('Delete'), null, et2_dialog.BUTTONS_YES_NO); - }) - .appendTo(eml); - if (_noDelete) del.hide(); - // invisible the mask - eml.css('opacity','0'); - - eml.parent().css('position', "relative"); - - // bind handler for activating actions on editable mask - eml.on({ - mouseover:function(){eml.css('opacity','0.9');}, - mouseout: function (){eml.css('opacity','0');} - }); - }, - - /** - * We need to build the Editable Mask Layer after widget gets loaded - */ - doLoadingFinished: function () - { - this._super.apply(this,arguments); - var self = this; - if (this.options.contact_id && this.options.editable) - { - egw(window).json( - 'addressbook.addressbook_ui.ajax_noPhotoExists', - [this.options.contact_id], - function(noPhotoExists) - { - if (noPhotoExists) self.image.attr('src',''); - self._buildEditableLayer(noPhotoExists); - } - ).sendRequest(true); - } - if (this.options.crop) - { - var cropped = jQuery(this.image).cropper({ - aspectRatio: 1/1, - crop: function (e){ - console.log (e); - } - }); - - } - } - -});}).call(this); -et2_register_widget(et2_avatar, ["avatar"]); - +* Avatar widget to display user profile picture or +* user letter avatar based on user's firstname lastname. +* +* @augments et2_baseWidget +*/ +var et2_avatar = /** @class */ (function (_super) { + __extends(et2_avatar, _super); + function et2_avatar(_parent, _attrs, _child) { + var _this = + // Call the inherited constructor + _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_avatar._attributes, _child || {})) || this; + if (_this.options.frame == 'circle') { + _this.image.attr('style', 'border-radius:50%'); + } + if (_this.options.contact_id) + _this.setValue(_this.options.contact_id); + return _this; + } + /** + * Generate letter avatar with given data + * @param {type} _fname + * @param {type} _lname + * @param {type} _id + * @returns {string} return data url + */ + et2_avatar.lavatar = function (_fname, _lname, _id) { + var str = _fname + _lname + _id; + var getBgColor = function (_str) { + var hash = 0; + for (var i = 0; i < _str.length; i++) { + hash = _str[i].charCodeAt(0) + hash; + } + return et2_avatar.LAVATAR_BG_COLORS[hash % et2_avatar.LAVATAR_BG_COLORS.length]; + }; + var bg = getBgColor(str); + var size = et2_avatar.LAVATAR_SIZE * (window.devicePixelRatio ? window.devicePixelRatio : 1); + var text = (_fname ? _fname[0].toUpperCase() : "") + (_lname ? _lname[0].toUpperCase() : ""); + var canvas = document.createElement('canvas'); + canvas.width = size; + canvas.height = size; + var context = canvas.getContext("2d"); + context.fillStyle = bg; + context.fillRect(0, 0, canvas.width, canvas.height); + context.font = Math.round(canvas.width / 2) + "px Arial"; + context.textAlign = "center"; + context.fillStyle = et2_avatar.LAVATAR_TEXT_COLOR; + context.fillText(text, size / 2, size / 1.5); + var dataURL = canvas.toDataURL(); + canvas.remove(); + return dataURL; + }; + /** + * Function runs after uplaod in avatar dialog is finished and it tries to + * update image and cropper container. + * @param {type} e + */ + et2_avatar.uploadAvatar_onFinish = function (e) { + var file = e.data.resumable.files[0].file; + var reader = new FileReader(); + reader.onload = function (e) { + jQuery('#_cropper_image').attr('src', e.target.result); + jQuery('#_cropper_image').cropper('replace', e.target.result); + }; + reader.readAsDataURL(file); + }; + /** + * Function to set contact id + * contact id could be in one of these formats: + * 'number', will be consider as contact_id + * 'contact:number', similar to above + * 'account:number', will be consider as account id + * @example: contact_id = "account:4" + * + * @param {string} _contact_id contact id could be as above mentioned formats + */ + et2_avatar.prototype.set_contact_id = function (_contact_id) { + var params = {}; + var id = 'contact_id'; + this.image.addClass('et2_avatar'); + if (!_contact_id) { + _contact_id = this.egw().user('account_id'); + } + else if (_contact_id.match(/account:/)) { + id = 'account_id'; + _contact_id = _contact_id.replace('account:', ''); + } + else { + id = 'contact_id'; + _contact_id = _contact_id.replace('contact:', ''); + } + // if our src (incl. cache-buster) already includes the correct id, use that one + if (this.options.src && this.options.src.match("(&|\\?)contact_id=" + _contact_id + "(&|\\$)")) { + return; + } + params[id] = _contact_id; + this.set_src(egw.link('/api/avatar.php', params)); + }; + /** + * Function to set value + */ + et2_avatar.prototype.setValue = function (_value) { + this.set_contact_id(_value); + }; + /** + * Implementation of "et2_IDetachedDOM" for fast viewing in gridview + */ + et2_avatar.prototype.getDetachedAttributes = function (_attrs) { + _attrs.push("contact_id", "label", "href"); + }; + et2_avatar.prototype.setDetachedAttributes = function (_nodes, _values) { + // Set the given DOM-Nodes + this.image = jQuery(_nodes[0]); + if (_values["contact_id"]) { + this.set_contact_id(_values["contact_id"]); + } + if (_values["label"]) { + this.set_label(_values["label"]); + } + if (_values["href"]) { + this.image.addClass('et2_clickable'); + this.set_href(_values["href"]); + } + }; + /** + * Build Editable Mask Layer (EML) in order to show edit/delete actions + * on top of profile picture. + * @param {boolean} _noDelete disable delete button in initialization + */ + et2_avatar.prototype._buildEditableLayer = function (_noDelete) { + var self = this; + // editable mask layer (eml) + var eml = jQuery(document.createElement('div')) + .addClass('eml') + .insertAfter(this.image); + // edit button + jQuery(document.createElement('div')) + .addClass('emlEdit') + .click(function () { + var buttons = [ + { "button_id": 1, "text": self.egw().lang('save'), id: 'save', image: 'check', "default": true }, + { "button_id": 0, "text": self.egw().lang('cancel'), id: 'cancel', image: 'cancelled' } + ]; + var dialog = function (_title, _value, _buttons, _egw_or_appname) { + return et2_createWidget("dialog", { + callback: function (_buttons, _value) { + if (_buttons == 'save') { + var canvas = jQuery('#_cropper_image').cropper('getCroppedCanvas'); + self.image.attr('src', canvas.toDataURL("image/jpeg", 1.0)); + self.egw().json('addressbook.addressbook_ui.ajax_update_photo', [self.getInstanceManager().etemplate_exec_id, canvas.toDataURL('image/jpeg', 1.0)], function (res) { + if (res) { + del.show(); + } + }).sendRequest(); + } + }, + title: _title || egw.lang('Input required'), + buttons: _buttons || et2_dialog.BUTTONS_OK_CANCEL, + value: { + content: _value + }, + width: "90%", + height: "450", + resizable: false, + position: "top+10", + template: egw.webserverUrl + '/api/templates/default/avatar_edit.xet?2' + }, et2_dialog._create_parent(_egw_or_appname)); + }; + dialog(egw.lang('Edit avatar'), { photo: self.options.contact_id }, buttons, null); + }) + .appendTo(eml); + // delete button + var del = jQuery(document.createElement('div')) + .addClass('emlDelete') + .click(function () { + et2_dialog.show_dialog(function (_btn) { + if (_btn == et2_dialog.YES_BUTTON) { + self.egw().json('addressbook.addressbook_ui.ajax_update_photo', [self.getInstanceManager().etemplate_exec_id, null], function (res) { + if (res) { + self.image.attr('src', ''); + del.hide(); + egw.refresh('Avatar Deleted.', egw.app_name()); + } + }).sendRequest(); + } + }, egw.lang('Delete this photo?'), egw.lang('Delete'), null, et2_dialog.BUTTONS_YES_NO); + }) + .appendTo(eml); + if (_noDelete) + del.hide(); + // invisible the mask + eml.css('opacity', '0'); + eml.parent().css('position', "relative"); + // bind handler for activating actions on editable mask + eml.on({ + mouseover: function () { eml.css('opacity', '0.9'); }, + mouseout: function () { eml.css('opacity', '0'); } + }); + }; + /** + * We need to build the Editable Mask Layer after widget gets loaded + */ + et2_avatar.prototype.doLoadingFinished = function () { + _super.prototype.doLoadingFinished.call(this); + var self = this; + if (this.options.contact_id && this.options.editable) { + egw(window).json('addressbook.addressbook_ui.ajax_noPhotoExists', [this.options.contact_id], function (noPhotoExists) { + if (noPhotoExists) + self.image.attr('src', ''); + self._buildEditableLayer(noPhotoExists); + }).sendRequest(true); + } + if (this.options.crop) { + jQuery(this.image).cropper({ + aspectRatio: 1 / 1, + crop: function (e) { + console.log(e); + } + }); + } + return true; + }; + et2_avatar._attributes = { + "contact_id": { + name: "Contact id", + type: "string", + default: "", + description: "Contact id should be either user account_id {account:number} or contact_id {contact:number or number}" + }, + "default_src": { + "ignore": true + }, + "frame": { + name: "Avatar frame", + type: "string", + default: "circle", + description: "Define the shape of frame that avatar will be shown inside it. it can get {circle,rectangle} values which default value is cicle." + }, + editable: { + name: "Edit avatar", + type: "boolean", + default: false, + description: "Make avatar widget editable to be able to crop profile picture or upload a new photo" + }, + crop: { + name: "Crop avatar", + type: "boolean", + default: false, + description: "Create crop container and cropping feature" + } + }; + /** + * background oolor codes + */ + et2_avatar.LAVATAR_BG_COLORS = [ + '#5a8770', '#b2b7bb', '#6fa9ab', '#f5af29', + '#0088b9', '#f18636', '#d93a37', '#a6b12e', + '#0088b9', '#f18636', '#d93a37', '#a6b12e', + '#5c9bbc', '#f5888d', '#9a89b5', '#407887', + '#9a89b5', '#5a8770', '#d33f33', '#a2b01f', + '#f0b126', '#0087bf', '#f18636', '#0087bf', + '#b2b7bb', '#72acae', '#9c8ab4', '#5a8770', + '#eeb424', '#407887' + ]; + return et2_avatar; +}(et2_image)); +et2_core_widget_1.et2_register_widget(et2_avatar, ["avatar"]); /** - * Avatar readonly widget to only display user profile picture or - * user letter avatar based on user's firstname lastname. - * - * @augments et2_baseWidget - */ -var et2_avatar_ro = (function(){ "use strict"; return et2_avatar.extend( -{ - init: function () - { - this._super.apply(this,arguments); - this.options.editable = false; - } - -});}).call(this); -et2_register_widget(et2_avatar_ro, ["avatar_ro"]); - +* Avatar readonly widget to only display user profile picture or +* user letter avatar based on user's firstname lastname. +*/ +var et2_avatar_ro = /** @class */ (function (_super) { + __extends(et2_avatar_ro, _super); + function et2_avatar_ro(_parent, _attrs, _child) { + var _this = + // Call the inherited constructor + _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_avatar_ro._attributes, _child || {})) || this; + _this.options.editable = false; + return _this; + } + return et2_avatar_ro; +}(et2_avatar)); +et2_core_widget_1.et2_register_widget(et2_avatar_ro, ["avatar_ro"]); /** - * Letter Avatar widget to display user profile picture (given url) or - * user letter avatar based on user's firstname lastname. - * - * It will use client-side lavatar if all the following conditions are met: - * - contact_id, lname and fname are all set. - * - the given src url includes flag of lavatar=1 which means there's - * no personal avatar set for the contact yet. - * - * @augments et2_baseWidget - */ -var et2_lavatar = (function(){ "use strict"; return et2_image.extend( -{ - attributes: { - lname: { - name: "last name", - type: "string", - default: "", - description:"" - }, - fname: { - name: "first name", - type: "string", - default: "", - description: "" - }, - - contact_id: { - name: "contact id", - type: "string", - default: "", - description: "" - } - }, - - set_src: function(_url){ - if (_url && decodeURIComponent(_url).match("lavatar=1") && (this.options.fname || this.options.lname) && this.options.contact_id) - { - this.set_src(et2_avatar.lavatar(this.options.fname, this.options.lname, this.options.contact_id)); - return false; - } - this._super.apply(this,arguments); - } -});}).call(this); -et2_register_widget(et2_lavatar, ["lavatar"]); - -jQuery.extend(et2_avatar, -{ - /** - * Function runs after uplaod in avatar dialog is finished and it tries to - * update image and cropper container. - * - * @param {type} e - * @returns {undefined} - */ - uploadAvatar_onFinish: function (e){ - var file = e.data.resumable.files[0].file; - var reader = new FileReader(); - reader.onload = function (e) - { - jQuery('#_cropper_image').attr('src', e.target.result); - jQuery('#_cropper_image').cropper('replace',e.target.result); - }; - reader.readAsDataURL(file); - }, - - /** - * background oolor codes - */ - LAVATAR_BG_COLORS: [ - '#5a8770', '#b2b7bb', '#6fa9ab', '#f5af29', - '#0088b9', '#f18636', '#d93a37', '#a6b12e', - '#0088b9', '#f18636', '#d93a37', '#a6b12e', - '#5c9bbc', '#f5888d', '#9a89b5', '#407887', - '#9a89b5', '#5a8770', '#d33f33', '#a2b01f', - '#f0b126', '#0087bf', '#f18636', '#0087bf', - '#b2b7bb', '#72acae', '#9c8ab4', '#5a8770', - '#eeb424', '#407887' - ], - - /** - * Text color - */ - LAVATAR_TEXT_COLOR: '#ffffff', - - LAVATAR_SIZE: 128, - /** - * Generate letter avatar with given data - * @param {type} _fname - * @param {type} _lname - * @param {type} _id - * @returns {string} return data url - */ - lavatar: function(_fname, _lname, _id){ - var str = _fname + _lname + _id; - var getBgColor = function(_str) - { - var hash = 0; - for (var i=0; i< _str.length; i++) - { - hash = _str[i].charCodeAt() + hash; - } - return et2_avatar.LAVATAR_BG_COLORS[hash % et2_avatar.LAVATAR_BG_COLORS.length]; - }; - var bg = getBgColor(str); - var size = et2_avatar.LAVATAR_SIZE * (window.devicePixelRatio ? window.devicePixelRatio : 1); - var text = (_fname ? _fname[0].toUpperCase() : "")+(_lname ? _lname[0].toUpperCase() : ""); - var canvas = document.createElement('canvas'); - canvas.width = size; - canvas.height = size; - var context = canvas.getContext("2d"); - context.fillStyle = bg; - context.fillRect (0, 0, canvas.width, canvas.height); - context.font = Math.round(canvas.width/2)+"px Arial"; - context.textAlign = "center"; - context.fillStyle = et2_avatar.LAVATAR_TEXT_COLOR; - context.fillText(text, size / 2, size / 1.5); - var dataURL = canvas.toDataURL(); - canvas.remove(); - return dataURL; - } -}); \ No newline at end of file +* Letter Avatar widget to display user profile picture (given url) or +* user letter avatar based on user's firstname lastname. +* +* It will use client-side lavatar if all the following conditions are met: +* - contact_id, lname and fname are all set. +* - the given src url includes flag of lavatar=1 which means there's +* no personal avatar set for the contact yet. +* +* @augments et2_baseWidget +*/ +var et2_lavatar = /** @class */ (function (_super) { + __extends(et2_lavatar, _super); + function et2_lavatar(_parent, _attrs, _child) { + // Call the inherited constructor + return _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_lavatar._attributes, _child || {})) || this; + } + et2_lavatar.prototype.set_src = function (_url) { + if (_url && decodeURIComponent(_url).match("lavatar=1") && (this.options.fname || this.options.lname) && this.options.contact_id) { + this.set_src(et2_avatar.lavatar(this.options.fname, this.options.lname, this.options.contact_id)); + return false; + } + _super.prototype.set_src.call(this, _url); + }; + et2_lavatar._attributes = { + lname: { + name: "last name", + type: "string", + default: "", + description: "" + }, + fname: { + name: "first name", + type: "string", + default: "", + description: "" + }, + contact_id: { + name: "contact id", + type: "string", + default: "", + description: "" + } + }; + return et2_lavatar; +}(et2_image)); +et2_core_widget_1.et2_register_widget(et2_lavatar, ["lavatar"]); +//# sourceMappingURL=et2_widget_image.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_image.ts b/api/js/etemplate/et2_widget_image.ts new file mode 100644 index 0000000000..7576538aae --- /dev/null +++ b/api/js/etemplate/et2_widget_image.ts @@ -0,0 +1,717 @@ +/** + * EGroupware eTemplate2 - JS Description object + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage api + * @link http://www.egroupware.org + * @author Nathan Gray + * @copyright Nathan Gray 2011 + * @version $Id$ + */ + +/*egw:uses + /vendor/bower-asset/jquery/dist/jquery.js; + et2_core_interfaces; + et2_core_baseWidget; + expose; + /vendor/bower-asset/cropper/dist/cropper.min.js; +*/ + +import {et2_baseWidget} from './et2_core_baseWidget'; +import {WidgetConfig, et2_register_widget} from "./et2_core_widget"; +import {ClassWithAttributes} from "./et2_core_inheritance"; + +/** + * Class which implements the "image" XET-Tag + * + * @augments et2_baseWidget + */ +class et2_image extends et2_baseWidget implements et2_IDetachedDOM +{ + static readonly _attributes : any = { + "src": { + "name": "Image", + "type": "string", + "description": "Displayed image" + }, + default_src: { + name: "Default image", + type: "string", + description: "Image to use if src is not found" + }, + "href": { + "name": "Link Target", + "type": "string", + "description": "Link URL, empty if you don't wan't to display a link.", + "default": et2_no_init + }, + "extra_link_target": { + "name": "Link target", + "type": "string", + "default": "_self", + "description": "Link target descriptor" + }, + "extra_link_popup": { + "name": "Popup", + "type": "string", + "description": "widthxheight, if popup should be used, eg. 640x480" + }, + "imagemap":{ + // TODO: Do something with this + "name": "Image map", + "description": "Currently not implemented" + }, + "label": { + "name": "Label", + "type": "string", + "description": "Label for image" + }, + "expose_view":{ + name: "Expose view", + type: "boolean", + default: false, + description: "Clicking on an image with href value would popup an expose view, and will show image referenced by href." + } + }; + + legacyOptions : string[] = ["href", "extra_link_target", "imagemap", "extra_link_popup", "id"]; + + image : JQuery = null; + + /** + * Constructor + */ + constructor(_parent, _attrs? : WidgetConfig, _child? : object) + { + // Call the inherited constructor + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_image._attributes, _child || {})); + + // Create the image or a/image tag + this.image = jQuery(document.createElement("img")); + if (this.options.label) + { + this.image.attr("alt", this.options.label).attr("title", this.options.label); + } + if (this.options.href) + { + this.image.addClass('et2_clickable'); + } + if(this.options["class"]) + { + this.image.addClass(this.options["class"]); + } + this.setDOMNode(this.image[0]); + } + + click(_ev : any) + { + if(this.options.href) + { + this.egw().open_link(this.options.href, this.options.extra_link_target, this.options.extra_link_popup); + } + else + { + super.click(_ev); + } + } + + transformAttributes(_attrs : any) + { + super.transformAttributes(_attrs); + + // Check to expand name + if (typeof _attrs["src"] != "undefined") + { + let manager = this.getArrayMgr("content"); + if(manager) { + let src = manager.getEntry(_attrs["src"]); + if (typeof src != "undefined" && src !== null) + { + if(typeof src == "object") + { + src = egw().link('/index.php', src); + } + _attrs["src"] = src; + } + } + } + } + + set_label(_value : string) + { + this.options.label = _value; + _value = this.egw().lang(_value); + // label is NOT the alt attribute in eTemplate, but the title/tooltip + this.image.attr("alt", _value).attr("title", _value); + } + + setValue(_value) + { + // Value is src, images don't get IDs + this.set_src(_value); + } + + set_href(_value) : boolean + { + if (!this.isInTree()) + { + return false; + } + + this.options.href = _value; + this.image.wrapAll('"'); + + let href = this.options.href; + let popup = this.options.extra_link_popup; + let target = this.options.extra_link_target; + let self = this; + this.image.click(function(e) + { + if (self.options.expose_view) + { + /* + TODO: Fix after implementing EXPOSE mixin class + */ + //self._init_blueimp_gallery(e,_value); + e.stopImmediatePropagation(); + } + else + { + egw.open_link(href,target,popup); + } + + e.preventDefault(); + return false; + }); + + return true; + } + + /** + * Set image src + * + * @param {string} _value image, app/image or url + * @return {boolean} true if image was found, false if not (image is either not displayed or default_src is used) + */ + set_src(_value : string) : boolean + { + if(!this.isInTree()) + { + return false; + } + + this.options.src = _value; + + // allow url's too + if (_value[0] == '/' || _value.substr(0,4) == 'http' || _value.substr(0,5) == 'data:') + { + this.image.attr('src', _value).show(); + return true; + } + let src = this.egw().image(_value); + if (src) + { + this.image.attr("src", src).show(); + return true; + } + src = null; + if (this.options.default_src) + { + src = this.egw().image(this.options.default_src); + } + if (src) + { + this.image.attr("src", src).show(); + } + else + { + this.image.css("display","none"); + } + return false; + } + + /** + * Function to get media content to feed the expose + * @param {type} _value + */ + getMedia(_value) : object[] + { + let base_url = egw.webserverUrl.match(/^\/ig/)?egw(window).window.location.origin + egw.webserverUrl + '/':egw.webserverUrl + '/'; + let mediaContent = []; + if (_value) + { + mediaContent = [{ + title: this.options.label, + href: base_url + _value, + type: this.options.type + "/*", + thumbnail: base_url + _value + }]; + } + return mediaContent; + } + + /** + * Implementation of "et2_IDetachedDOM" for fast viewing in gridview + * + * @param {array} _attrs + */ + getDetachedAttributes(_attrs) + { + _attrs.push("src", "label", "href"); + } + + getDetachedNodes() + { + return [this.image[0]]; + } + + setDetachedAttributes(_nodes, _values) + { + // Set the given DOM-Nodes + this.image = jQuery(_nodes[0]); + + // Set the attributes + if (_values["src"]) + { + this.set_src(_values["src"]); + } + // Not valid, but we'll deal + if (_values["value"]) + { + this.setValue(_values["value"]); + } + + if (_values["label"]) + { + this.set_label(_values["label"]); + } + if(_values["href"]) + { + this.image.addClass('et2_clickable'); + this.set_href(_values["href"]); + } + } +} +et2_register_widget(et2_image, ["image"]); + +/** +* Widget displaying an application icon +*/ +class et2_appicon extends et2_image +{ + static readonly _attributes: any = { + default_src: { + name: "Default image", + type: "string", + default: "nonav", + description: "Image to use if there is no application icon" + } + }; + + set_src(_app) : boolean + { + if (!_app) _app = this.egw().app_name(); + this.image.addClass('et2_appicon'); + return super.set_src(_app == 'sitemgr-link' ? 'sitemgr/sitemgr-link' : // got removed from jdots + (this.egw().app(_app, 'icon_app') || _app)+'/'+(this.egw().app(_app, 'icon') || 'navbar')); + } +} +et2_register_widget(et2_appicon, ["appicon"]); + +/** +* Avatar widget to display user profile picture or +* user letter avatar based on user's firstname lastname. +* +* @augments et2_baseWidget +*/ + +class et2_avatar extends et2_image +{ + static readonly _attributes : any = { + "contact_id": { + name: "Contact id", + type: "string", + default: "", + description: "Contact id should be either user account_id {account:number} or contact_id {contact:number or number}" + }, + "default_src": { + "ignore": true + }, + "frame": { + name: "Avatar frame", + type: "string", + default: "circle", + description: "Define the shape of frame that avatar will be shown inside it. it can get {circle,rectangle} values which default value is cicle." + }, + editable: { + name: "Edit avatar", + type: "boolean", + default: false, + description: "Make avatar widget editable to be able to crop profile picture or upload a new photo" + }, + crop: { + name: "Crop avatar", + type: "boolean", + default: false, + description: "Create crop container and cropping feature" + } + }; + + /** + * background oolor codes + */ + static LAVATAR_BG_COLORS : string[] = [ + '#5a8770', '#b2b7bb', '#6fa9ab', '#f5af29', + '#0088b9', '#f18636', '#d93a37', '#a6b12e', + '#0088b9', '#f18636', '#d93a37', '#a6b12e', + '#5c9bbc', '#f5888d', '#9a89b5', '#407887', + '#9a89b5', '#5a8770', '#d33f33', '#a2b01f', + '#f0b126', '#0087bf', '#f18636', '#0087bf', + '#b2b7bb', '#72acae', '#9c8ab4', '#5a8770', + '#eeb424', '#407887' + ]; + + /** + * Text color + */ + static LAVATAR_TEXT_COLOR: '#ffffff'; + + static LAVATAR_SIZE: 128; + + /** + * Generate letter avatar with given data + * @param {type} _fname + * @param {type} _lname + * @param {type} _id + * @returns {string} return data url + */ + static lavatar(_fname, _lname, _id) + { + let str = _fname + _lname + _id; + let getBgColor = function(_str) + { + let hash = 0; + for (let i=0; i< _str.length; i++) + { + hash = _str[i].charCodeAt(0) + hash; + } + return et2_avatar.LAVATAR_BG_COLORS[hash % et2_avatar.LAVATAR_BG_COLORS.length]; + }; + let bg = getBgColor(str); + let size = et2_avatar.LAVATAR_SIZE * (window.devicePixelRatio ? window.devicePixelRatio : 1); + let text = (_fname ? _fname[0].toUpperCase() : "")+(_lname ? _lname[0].toUpperCase() : ""); + let canvas = document.createElement('canvas'); + canvas.width = size; + canvas.height = size; + let context = canvas.getContext("2d"); + context.fillStyle = bg; + context.fillRect (0, 0, canvas.width, canvas.height); + context.font = Math.round(canvas.width/2)+"px Arial"; + context.textAlign = "center"; + context.fillStyle = et2_avatar.LAVATAR_TEXT_COLOR; + context.fillText(text, size / 2, size / 1.5); + let dataURL = canvas.toDataURL(); + canvas.remove(); + return dataURL; + } + + /** + * Function runs after uplaod in avatar dialog is finished and it tries to + * update image and cropper container. + * @param {type} e + */ + static uploadAvatar_onFinish(e) + { + let file = e.data.resumable.files[0].file; + let reader = new FileReader(); + reader.onload = function (e) + { + jQuery('#_cropper_image').attr('src', e.target.result); + jQuery('#_cropper_image').cropper('replace',e.target.result); + }; + reader.readAsDataURL(file); + } + + constructor(_parent, _attrs? : WidgetConfig, _child? : object) + { + // Call the inherited constructor + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_avatar._attributes, _child || {})); + if (this.options.frame == 'circle') + { + this.image.attr('style', 'border-radius:50%'); + } + if (this.options.contact_id) this.setValue(this.options.contact_id); + } + + /** + * Function to set contact id + * contact id could be in one of these formats: + * 'number', will be consider as contact_id + * 'contact:number', similar to above + * 'account:number', will be consider as account id + * @example: contact_id = "account:4" + * + * @param {string} _contact_id contact id could be as above mentioned formats + */ + set_contact_id(_contact_id : string) : void + { + let params = {}; + let id = 'contact_id'; + + this.image.addClass('et2_avatar'); + + if (!_contact_id) + { + _contact_id = this.egw().user('account_id'); + } + else if(_contact_id.match(/account:/)) + { + id = 'account_id'; + _contact_id = _contact_id.replace('account:',''); + } + else + { + id = 'contact_id'; + _contact_id = _contact_id.replace('contact:', ''); + } + + // if our src (incl. cache-buster) already includes the correct id, use that one + if (this.options.src && this.options.src.match("(&|\\?)contact_id="+_contact_id+"(&|\\$)")) + { + return; + } + params[id] = _contact_id; + this.set_src(egw.link('/api/avatar.php',params)); + } + + /** + * Function to set value + */ + setValue(_value : string) + { + this.set_contact_id(_value); + } + + /** + * Implementation of "et2_IDetachedDOM" for fast viewing in gridview + */ + getDetachedAttributes(_attrs : string[]) + { + _attrs.push("contact_id", "label", "href"); + } + + setDetachedAttributes(_nodes, _values) + { + // Set the given DOM-Nodes + this.image = jQuery(_nodes[0]); + + if (_values["contact_id"]) + { + this.set_contact_id(_values["contact_id"]); + } + + if (_values["label"]) + { + this.set_label(_values["label"]); + } + if(_values["href"]) + { + this.image.addClass('et2_clickable'); + this.set_href(_values["href"]); + } + } + + /** + * Build Editable Mask Layer (EML) in order to show edit/delete actions + * on top of profile picture. + * @param {boolean} _noDelete disable delete button in initialization + */ + private _buildEditableLayer(_noDelete : boolean) + { + let self = this; + // editable mask layer (eml) + let eml = jQuery(document.createElement('div')) + .addClass('eml') + .insertAfter(this.image); + + // edit button + jQuery(document.createElement('div')) + .addClass('emlEdit') + .click(function(){ + let buttons = [ + {"button_id": 1,"text": self.egw().lang('save'), id: 'save', image: 'check', "default":true}, + {"button_id": 0,"text": self.egw().lang('cancel'), id: 'cancel', image: 'cancelled'} + ]; + let dialog = function(_title, _value, _buttons, _egw_or_appname) + { + return et2_createWidget("dialog", + { + callback: function(_buttons, _value) + { + if (_buttons == 'save') + { + let canvas = jQuery('#_cropper_image').cropper('getCroppedCanvas'); + self.image.attr('src', canvas.toDataURL("image/jpeg", 1.0)); + self.egw().json('addressbook.addressbook_ui.ajax_update_photo', + [self.getInstanceManager().etemplate_exec_id, canvas.toDataURL('image/jpeg',1.0)], + function(res) + { + if (res) + { + del.show(); + } + }).sendRequest(); + } + }, + title: _title||egw.lang('Input required'), + buttons: _buttons||et2_dialog.BUTTONS_OK_CANCEL, + value: { + content: _value + }, + width: "90%", + height:"450", + resizable: false, + position:"top+10", + template: egw.webserverUrl+'/api/templates/default/avatar_edit.xet?2' + }, et2_dialog._create_parent(_egw_or_appname)); + }; + + dialog(egw.lang('Edit avatar'),{photo:self.options.contact_id}, buttons, null); + }) + .appendTo(eml); + + // delete button + var del = jQuery(document.createElement('div')) + .addClass('emlDelete') + .click(function(){ + et2_dialog.show_dialog(function(_btn){ + if (_btn == et2_dialog.YES_BUTTON) + { + self.egw().json('addressbook.addressbook_ui.ajax_update_photo', + [self.getInstanceManager().etemplate_exec_id, null], + function(res) + { + if (res) + { + self.image.attr('src',''); + del.hide(); + egw.refresh('Avatar Deleted.', egw.app_name()); + } + }).sendRequest(); + } + }, egw.lang('Delete this photo?'), egw.lang('Delete'), null, et2_dialog.BUTTONS_YES_NO); + }) + .appendTo(eml); + if (_noDelete) del.hide(); + // invisible the mask + eml.css('opacity','0'); + + eml.parent().css('position', "relative"); + + // bind handler for activating actions on editable mask + eml.on({ + mouseover:function(){eml.css('opacity','0.9');}, + mouseout: function (){eml.css('opacity','0');} + }); + } + + /** + * We need to build the Editable Mask Layer after widget gets loaded + */ + doLoadingFinished() : boolean + { + super.doLoadingFinished(); + let self = this; + if (this.options.contact_id && this.options.editable) + { + egw(window).json( + 'addressbook.addressbook_ui.ajax_noPhotoExists', + [this.options.contact_id], + function(noPhotoExists) + { + if (noPhotoExists) self.image.attr('src',''); + self._buildEditableLayer(noPhotoExists); + } + ).sendRequest(true); + } + if (this.options.crop) + { + jQuery(this.image).cropper({ + aspectRatio: 1/1, + crop: function (e){ + console.log (e); + } + }); + } + return true; + } +} +et2_register_widget(et2_avatar, ["avatar"]); + +/** +* Avatar readonly widget to only display user profile picture or +* user letter avatar based on user's firstname lastname. +*/ +class et2_avatar_ro extends et2_avatar +{ + constructor(_parent, _attrs? : WidgetConfig, _child? : object) + { + // Call the inherited constructor + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_avatar_ro._attributes, _child || {})); + this.options.editable = false; + } +} +et2_register_widget(et2_avatar_ro, ["avatar_ro"]); + +/** +* Letter Avatar widget to display user profile picture (given url) or +* user letter avatar based on user's firstname lastname. +* +* It will use client-side lavatar if all the following conditions are met: +* - contact_id, lname and fname are all set. +* - the given src url includes flag of lavatar=1 which means there's +* no personal avatar set for the contact yet. +* +* @augments et2_baseWidget +*/ +class et2_lavatar extends et2_image +{ + static readonly _attributes : any = { + lname: { + name: "last name", + type: "string", + default: "", + description:"" + }, + fname: { + name: "first name", + type: "string", + default: "", + description: "" + }, + + contact_id: { + name: "contact id", + type: "string", + default: "", + description: "" + } + }; + + constructor(_parent, _attrs? : WidgetConfig, _child? : object) + { + // Call the inherited constructor + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_lavatar._attributes, _child || {})); + } + + set_src(_url){ + if (_url && decodeURIComponent(_url).match("lavatar=1") && (this.options.fname || this.options.lname) && this.options.contact_id) + { + this.set_src(et2_avatar.lavatar(this.options.fname, this.options.lname, this.options.contact_id)); + return false; + } + super.set_src(_url); + } +} +et2_register_widget(et2_lavatar, ["lavatar"]); \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_itempicker.js b/api/js/etemplate/et2_widget_itempicker.js index dd0f2cdf66..17be9d3cd5 100755 --- a/api/js/etemplate/et2_widget_itempicker.js +++ b/api/js/etemplate/et2_widget_itempicker.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - JS Itempicker object * derived from et2_link_entry widget @copyright 2011 Nathan Gray @@ -10,358 +11,343 @@ * @author Nathan Gray * @copyright 2012 Christian Binder * @copyright 2011 Nathan Gray - * @version $Id: et2_widget_itempicker.js 38623 2012-03-26 23:27:53Z jaytraxx $ */ - +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 - /vendor/bower-asset/jquery/dist/jquery.js; - et2_core_inputWidget; - et2_core_valueWidget; - et2_extension_itempicker_actions; - egw_action.egw_action_common; + /vendor/bower-asset/jquery/dist/jquery.js; + et2_core_inputWidget; + et2_core_valueWidget; + et2_extension_itempicker_actions; + egw_action.egw_action_common; */ - +var et2_core_widget_1 = require("./et2_core_widget"); +var et2_core_inputWidget_1 = require("./et2_core_inputWidget"); +var et2_core_inheritance_1 = require("./et2_core_inheritance"); /** * Class which implements the "itempicker" XET-Tag * * @augments et2_inputWidget */ -var et2_itempicker = (function(){ "use strict"; return et2_inputWidget.extend( -{ - attributes: { - "action": { - "name": "Action callback", - "type": "string", - "default": false, - "description": "Callback for action. Must be a function(context, data)" - }, - "action_label": { - "name": "Action label", - "type": "string", - "default": "Action", - "description": "Label for action button" - }, - "application": { - "name": "Application", - "type": "string", - "default": "", - "description": "Limit to the listed application or applications (comma separated)" - }, - "blur": { - "name": "Placeholder", - "type": "string", - "default": et2_no_init, - "description": "This text get displayed if an input-field is empty and does not have the input-focus (blur). It can be used to show a default value or a kind of help-text." - }, - "value": { - "name": "value", - "type": "any", - "default": "", - "description": "Optional itempicker value(s) - can be used for e.g. environmental information" - }, - "query": { - "name": "Query callback", - "type": "any", - "default": false, - "description": "Callback before query to server. Must return true, or false to abort query." - } - }, - - legacyOptions: ["application"], - search_timeout: 200, //ms after change to send query - minimum_characters: 2, // Don't send query unless there's at least this many chars - last_search: "", // Remember last search value - action: null, // Action function for button - current_app: "", // Remember currently chosen application - - /** - * Constructor - * - * @memberOf et2_itempicker - */ - init: function() { - this._super.apply(this, arguments); - - this.div = null; - this.left = null; - this.right = null; - this.right_container = null; - this.app_select = null; - this.search = null; - this.button_action = null; - this.itemlist = null; - - if(this.options.action !== null && typeof this.options.action == "string") - { - this.action = new egwFnct(this, "javaScript:" + this.options.action); - } - else - { - console.log("itempicker widget: no action provided for button"); - } - - this.createInputWidget(); - }, - - clearSearchResults: function() { - this.search.val(""); - this.itemlist.html(""); - this.search.focus(); - this.clear.hide(); - }, - - createInputWidget: function() { - var _self = this; - - this.div = jQuery(document.createElement("div")); - this.left = jQuery(document.createElement("div")); - this.right = jQuery(document.createElement("div")); - this.right_container = jQuery(document.createElement("div")); - this.app_select = jQuery(document.createElement("ul")); - this.search = jQuery(document.createElement("input")); - this.clear = jQuery(document.createElement("span")); - this.itemlist = jQuery(document.createElement("div")); - - // Container elements - this.div.addClass("et2_itempicker"); - this.left.addClass("et2_itempicker_left"); - this.right.addClass("et2_itempicker_right"); - this.right_container.addClass("et2_itempicker_right_container"); - - // Application select - this.app_select.addClass("et2_itempicker_app_select"); - var item_count = 0; - for(var key in this.options.select_options) { - var img_icon = this.egw().image(key + "/navbar"); - if(img_icon === null) { - continue; - } - var img = jQuery(document.createElement("img")); - img.attr("src", img_icon); - var item = jQuery(document.createElement("li")); - item.attr("id", key) - .click(function() { - _self.selectApplication(jQuery(this)); - }) - .append(img); - if(item_count == 0) { - this.selectApplication(item); // select first item by default - } - this.app_select.append(item); - item_count++; - } - - // Search input field - this.search.addClass("et2_itempicker_search"); - this.search.keyup(function() { - var request = {}; - request.term = jQuery(this).val(); - _self.query(request); - }); - this.set_blur(this.options.blur, this.search); - - // Clear button for search - this.clear - .addClass("et2_itempicker_clear ui-icon ui-icon-close") - .click(function(e){ - _self.clearSearchResults(); - }) - .hide(); - - // Action button - this.button_action = et2_createWidget("button"); - jQuery(this.button_action.getDOMNode()).addClass("et2_itempicker_button_action"); - this.button_action.set_label(this.egw().lang(this.options.action_label)); - this.button_action.click = function() { _self.doAction(); }; - - // Itemlist - this.itemlist.attr("id", "itempicker_itemlist"); - this.itemlist.addClass("et2_itempicker_itemlist"); - - // Put everything together - this.left.append(this.app_select); - this.right_container.append(this.search); - this.right_container.append(this.clear); - this.right_container.append(this.button_action.getDOMNode()); - this.right_container.append(this.itemlist); - this.right.append(this.right_container); - this.div.append(this.right); // right before left to have a natural - this.div.append(this.left); // z-index for left div over right div - - this.setDOMNode(this.div[0]); - }, - - doAction: function() - { - if(this.action !== null) - { - var data = {}; - data.app = this.current_app; - data.value = this.options.value; - data.checked = this.getSelectedItems(); - return this.action.exec(this, data); - } - - return false; - }, - - getSelectedItems: function() - { - var items = []; - jQuery(this.itemlist).children("ul").children("li.selected").each(function(index) { - items[index] = jQuery(this).attr("id"); - }); - return items; - }, - - /** - * Ask server for entries matching selected app/type and filtered by search string - */ - query: function(request) { - if(request.term.length < 3) { - return true; - } - // Remember last search - this.last_search = request.term; - - // Allow hook / tie in - if(this.options.query && typeof this.options.query == 'function') - { - if(!this.options.query(request, response)) return false; - } - - //if(request.term in this.cache) { - // return response(this.cache[request.term]); - //} - - this.itemlist.addClass("loading"); - this.clear.css("display", "inline-block"); - egw._json("EGroupware\\Api\\Etemplate\\Widget\\ItemPicker::ajax_item_search", - [this.current_app, '', request.term, request.options], - this.queryResults, - this,true,this - ).sendRequest(); - }, - - /** - * Server found some results for query - */ - queryResults: function(data) { - this.itemlist.removeClass("loading"); - this.updateItemList(data); - }, - - selectApplication: function(app) { - this.clearSearchResults(); - jQuery(".et2_itempicker_app_select li").removeClass("selected"); - app.addClass("selected"); - this.current_app = app.attr("id"); - return true; - }, - - set_blur: function(_value, input) { - if(typeof input == 'undefined') input = this.search; - - if(_value) { - input.attr("placeholder", _value); // HTML5 - if(!input[0].placeholder) { - // Not HTML5 - if(input.val() == "") input.val(_value); - input.focus(input,function(e) { - var placeholder = _value; - if(e.data.val() == placeholder) e.data.val(""); - }).blur(input, function(e) { - var placeholder = _value; - if(e.data.val() == "") e.data.val(placeholder); - }); - if(input.val() == "") input.val(_value); - } - } else { - this.search.removeAttr("placeholder"); - } - }, - - transformAttributes: function(_attrs) { - this._super.apply(this, arguments); - - _attrs["select_options"] = {}; - if(_attrs["application"]) - { - var apps = et2_csvSplit(_attrs["application"], null, ","); - for(var i = 0; i < apps.length; i++) - { - _attrs["select_options"][apps[i]] = this.egw().lang(apps[i]); - } - } - else - { - _attrs["select_options"] = this.egw().link_app_list('query'); - } - - // Check whether the options entry was found, if not read it from the - // content array. - if (_attrs["select_options"] == null) - { - _attrs["select_options"] = this.getArrayMgr('content') - .getEntry("options-" + this.id); - } - - // Default to an empty object - if (_attrs["select_options"] == null) - { - _attrs["select_options"] = {}; - } - }, - - updateItemList: function(data) { - var list = jQuery(document.createElement("ul")); - var item_count = 0; - var _self = this; - for(var id in data) { - var item = jQuery(document.createElement("li")); - if(item_count%2 == 0) { - item.addClass("row_on"); - } else { - item.addClass("row_off"); - } - item.attr("id", id) - .html(data[id]) - .click(function(e) { - if(e.ctrlKey || e.metaKey) { - // add to selection - jQuery(this).addClass("selected"); - } else if(e.shiftKey) { - // select range - var start = jQuery(this).siblings(".selected").first(); - if(start == 0) { - // no start item - cannot select range - select single item - jQuery(this).addClass("selected"); - return true; - } - var end = jQuery(this); - // swap start and end if start appears after end in dom hierarchy - if(start.index() > end.index()) { - var startOld = start; - start = end; - end = startOld; - } - // select start to end - start.addClass("selected"); - start.nextUntil(end).addClass("selected"); - end.addClass("selected"); - } else { - // select single item - jQuery(this).siblings(".selected").removeClass("selected"); - jQuery(this).addClass("selected"); - } - }); - list.append(item); - item_count++; - } - this.itemlist.html(list); - } - -});}).call(this); - -et2_register_widget(et2_itempicker, ["itempicker"]); - +var et2_itempicker = /** @class */ (function (_super) { + __extends(et2_itempicker, _super); + /** + * Constructor + * + * @memberOf et2_itempicker + */ + function et2_itempicker(_parent, _attrs, _child) { + var _this = + // Call the inherited constructor + _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_itempicker._attributes, _child || {})) || this; + _this.legacyOptions = ["application"]; + _this.last_search = ""; // Remember last search value + _this.action = null; // Action function for button + _this.current_app = ""; // Remember currently chosen application + _this.div = null; + _this.left = null; + _this.right = null; + _this.right_container = null; + _this.app_select = null; + _this.search = null; + _this.button_action = null; + _this.itemlist = null; + _this.div = null; + _this.left = null; + _this.right = null; + _this.right_container = null; + _this.app_select = null; + _this.search = null; + _this.button_action = null; + _this.itemlist = null; + if (_this.options.action !== null && typeof _this.options.action == "string") { + _this.action = new egwFnct(_this, "javaScript:" + _this.options.action); + } + else { + console.log("itempicker widget: no action provided for button"); + } + _this.createInputWidget(); + return _this; + } + et2_itempicker.prototype.clearSearchResults = function () { + this.search.val(""); + this.itemlist.html(""); + this.search.focus(); + this.clear.hide(); + }; + et2_itempicker.prototype.createInputWidget = function () { + var _self = this; + this.div = jQuery(document.createElement("div")); + this.left = jQuery(document.createElement("div")); + this.right = jQuery(document.createElement("div")); + this.right_container = jQuery(document.createElement("div")); + this.app_select = jQuery(document.createElement("ul")); + this.search = jQuery(document.createElement("input")); + this.clear = jQuery(document.createElement("span")); + this.itemlist = jQuery(document.createElement("div")); + // Container elements + this.div.addClass("et2_itempicker"); + this.left.addClass("et2_itempicker_left"); + this.right.addClass("et2_itempicker_right"); + this.right_container.addClass("et2_itempicker_right_container"); + // Application select + this.app_select.addClass("et2_itempicker_app_select"); + var item_count = 0; + for (var key in this.options.select_options) { + var img_icon = this.egw().image(key + "/navbar"); + if (img_icon === null) { + continue; + } + var img = jQuery(document.createElement("img")); + img.attr("src", img_icon); + var item = jQuery(document.createElement("li")); + item.attr("id", key) + .click(function () { + _self.selectApplication(jQuery(this)); + }) + .append(img); + if (item_count == 0) { + this.selectApplication(item); // select first item by default + } + this.app_select.append(item); + item_count++; + } + // Search input field + this.search.addClass("et2_itempicker_search"); + this.search.keyup(function () { + var request = {}; + request.term = jQuery(this).val(); + _self.query(request); + }); + this.set_blur(this.options.blur, this.search); + // Clear button for search + this.clear + .addClass("et2_itempicker_clear ui-icon ui-icon-close") + .click(function () { + _self.clearSearchResults(); + }) + .hide(); + // Action button + this.button_action = et2_createWidget("button"); + jQuery(this.button_action.getDOMNode()).addClass("et2_itempicker_button_action"); + this.button_action.set_label(this.egw().lang(this.options.action_label)); + this.button_action.click = function () { _self.doAction(); }; + // Itemlist + this.itemlist.attr("id", "itempicker_itemlist"); + this.itemlist.addClass("et2_itempicker_itemlist"); + // Put everything together + this.left.append(this.app_select); + this.right_container.append(this.search); + this.right_container.append(this.clear); + this.right_container.append(this.button_action.getDOMNode()); + this.right_container.append(this.itemlist); + this.right.append(this.right_container); + this.div.append(this.right); // right before left to have a natural + this.div.append(this.left); // z-index for left div over right div + this.setDOMNode(this.div[0]); + }; + et2_itempicker.prototype.doAction = function () { + if (this.action !== null) { + var data = {}; + data.app = this.current_app; + data.value = this.options.value; + data.checked = this.getSelectedItems(); + return this.action.exec(this, data); + } + return false; + }; + et2_itempicker.prototype.getSelectedItems = function () { + var items = []; + jQuery(this.itemlist).children("ul").children("li.selected").each(function (index) { + items[index] = jQuery(this).attr("id"); + }); + return items; + }; + /** + * Ask server for entries matching selected app/type and filtered by search string + */ + et2_itempicker.prototype.query = function (request) { + if (request.term.length < 3) { + return true; + } + // Remember last search + this.last_search = request.term; + // Allow hook / tie in + if (this.options.query && typeof this.options.query == 'function') { + if (!this.options.query(request, response)) + return false; + } + //if(request.term in this.cache) { + // return response(this.cache[request.term]); + //} + this.itemlist.addClass("loading"); + this.clear.css("display", "inline-block"); + egw.json("EGroupware\\Api\\Etemplate\\Widget\\ItemPicker::ajax_item_search", [this.current_app, '', request.term, request.options], this.queryResults, this, true, this).sendRequest(); + }; + /** + * Server found some results for query + */ + et2_itempicker.prototype.queryResults = function (data) { + this.itemlist.removeClass("loading"); + this.updateItemList(data); + }; + et2_itempicker.prototype.selectApplication = function (app) { + this.clearSearchResults(); + jQuery(".et2_itempicker_app_select li").removeClass("selected"); + app.addClass("selected"); + this.current_app = app.attr("id"); + return true; + }; + et2_itempicker.prototype.set_blur = function (_value, input) { + if (typeof input == 'undefined') + input = this.search; + if (_value) { + input.attr("placeholder", _value); // HTML5 + if (!input[0].placeholder) { + // Not HTML5 + if (input.val() == "") + input.val(_value); + input.focus(input, function (e) { + var placeholder = _value; + if (e.data.val() == placeholder) + e.data.val(""); + }).blur(input, function (e) { + var placeholder = _value; + if (e.data.val() == "") + e.data.val(placeholder); + }); + if (input.val() == "") + input.val(_value); + } + } + else { + this.search.removeAttr("placeholder"); + } + }; + et2_itempicker.prototype.transformAttributes = function (_attrs) { + _super.prototype.transformAttributes.call(this, _attrs); + _attrs["select_options"] = {}; + if (_attrs["application"]) { + var apps = et2_csvSplit(_attrs["application"], null, ","); + for (var i = 0; i < apps.length; i++) { + _attrs["select_options"][apps[i]] = this.egw().lang(apps[i]); + } + } + else { + _attrs["select_options"] = this.egw().link_app_list('query'); + } + // Check whether the options entry was found, if not read it from the + // content array. + if (_attrs["select_options"] == null) { + _attrs["select_options"] = this.getArrayMgr('content') + .getEntry("options-" + this.id); + } + // Default to an empty object + if (_attrs["select_options"] == null) { + _attrs["select_options"] = {}; + } + }; + et2_itempicker.prototype.updateItemList = function (data) { + var list = jQuery(document.createElement("ul")); + var item_count = 0; + for (var id in data) { + var item = jQuery(document.createElement("li")); + if (item_count % 2 == 0) { + item.addClass("row_on"); + } + else { + item.addClass("row_off"); + } + item.attr("id", id) + .html(data[id]) + .click(function (e) { + var _a; + if (e.ctrlKey || e.metaKey) { + // add to selection + jQuery(this).addClass("selected"); + } + else if (e.shiftKey) { + // select range + var start = jQuery(this).siblings(".selected").first(); + if (((_a = start) === null || _a === void 0 ? void 0 : _a.length) == 0) { + // no start item - cannot select range - select single item + jQuery(this).addClass("selected"); + return true; + } + var end = jQuery(this); + // swap start and end if start appears after end in dom hierarchy + if (start.index() > end.index()) { + var startOld = start; + start = end; + end = startOld; + } + // select start to end + start.addClass("selected"); + start.nextUntil(end).addClass("selected"); + end.addClass("selected"); + } + else { + // select single item + jQuery(this).siblings(".selected").removeClass("selected"); + jQuery(this).addClass("selected"); + } + }); + list.append(item); + item_count++; + } + this.itemlist.html(list); + }; + et2_itempicker._attributes = { + "action": { + "name": "Action callback", + "type": "string", + "default": false, + "description": "Callback for action. Must be a function(context, data)" + }, + "action_label": { + "name": "Action label", + "type": "string", + "default": "Action", + "description": "Label for action button" + }, + "application": { + "name": "Application", + "type": "string", + "default": "", + "description": "Limit to the listed application or applications (comma separated)" + }, + "blur": { + "name": "Placeholder", + "type": "string", + "default": et2_no_init, + "description": "This text get displayed if an input-field is empty and does not have the input-focus (blur). It can be used to show a default value or a kind of help-text." + }, + "value": { + "name": "value", + "type": "any", + "default": "", + "description": "Optional itempicker value(s) - can be used for e.g. environmental information" + }, + "query": { + "name": "Query callback", + "type": "any", + "default": false, + "description": "Callback before query to server. Must return true, or false to abort query." + } + }; + return et2_itempicker; +}(et2_core_inputWidget_1.et2_inputWidget)); +et2_core_widget_1.et2_register_widget(et2_itempicker, ["itempicker"]); +//# sourceMappingURL=et2_widget_itempicker.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_itempicker.ts b/api/js/etemplate/et2_widget_itempicker.ts new file mode 100644 index 0000000000..d6d6340686 --- /dev/null +++ b/api/js/etemplate/et2_widget_itempicker.ts @@ -0,0 +1,400 @@ +/** + * EGroupware eTemplate2 - JS Itempicker object + * derived from et2_link_entry widget @copyright 2011 Nathan Gray + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage api + * @link http://www.egroupware.org + * @author Christian Binder + * @author Nathan Gray + * @copyright 2012 Christian Binder + * @copyright 2011 Nathan Gray + */ + +/*egw:uses + /vendor/bower-asset/jquery/dist/jquery.js; + et2_core_inputWidget; + et2_core_valueWidget; + et2_extension_itempicker_actions; + egw_action.egw_action_common; +*/ + +import {et2_register_widget, WidgetConfig} from "./et2_core_widget"; +import {et2_inputWidget} from "./et2_core_inputWidget"; +import {ClassWithAttributes} from "./et2_core_inheritance"; + +/** + * Class which implements the "itempicker" XET-Tag + * + * @augments et2_inputWidget + */ +class et2_itempicker extends et2_inputWidget +{ + static readonly _attributes : any = { + "action": { + "name": "Action callback", + "type": "string", + "default": false, + "description": "Callback for action. Must be a function(context, data)" + }, + "action_label": { + "name": "Action label", + "type": "string", + "default": "Action", + "description": "Label for action button" + }, + "application": { + "name": "Application", + "type": "string", + "default": "", + "description": "Limit to the listed application or applications (comma separated)" + }, + "blur": { + "name": "Placeholder", + "type": "string", + "default": et2_no_init, + "description": "This text get displayed if an input-field is empty and does not have the input-focus (blur). It can be used to show a default value or a kind of help-text." + }, + "value": { + "name": "value", + "type": "any", + "default": "", + "description": "Optional itempicker value(s) - can be used for e.g. environmental information" + }, + "query": { + "name": "Query callback", + "type": "any", + "default": false, + "description": "Callback before query to server. Must return true, or false to abort query." + } + }; + + legacyOptions : string[] = ["application"]; + private last_search : string = ""; // Remember last search value + private action : egwFnct = null; // Action function for button + private current_app : string = ""; // Remember currently chosen application + private div : JQuery = null; + private left : JQuery = null; + private right : JQuery = null; + private right_container : JQuery = null; + private app_select : JQuery = null; + private search : JQuery = null; + private button_action : any = null; + private itemlist : JQuery = null; + private clear : JQuery; + + /** + * Constructor + * + * @memberOf et2_itempicker + */ + constructor(_parent, _attrs? : WidgetConfig, _child? : object) + { + // Call the inherited constructor + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_itempicker._attributes, _child || {})); + + this.div = null; + this.left = null; + this.right = null; + this.right_container = null; + this.app_select = null; + this.search = null; + this.button_action = null; + this.itemlist = null; + + if(this.options.action !== null && typeof this.options.action == "string") + { + this.action = new egwFnct(this, "javaScript:" + this.options.action); + } + else + { + console.log("itempicker widget: no action provided for button"); + } + + this.createInputWidget(); + } + + clearSearchResults() + { + this.search.val(""); + this.itemlist.html(""); + this.search.focus(); + this.clear.hide(); + } + + createInputWidget() { + let _self = this; + + this.div = jQuery(document.createElement("div")); + this.left = jQuery(document.createElement("div")); + this.right = jQuery(document.createElement("div")); + this.right_container = jQuery(document.createElement("div")); + this.app_select = jQuery(document.createElement("ul")); + this.search = jQuery(document.createElement("input")); + this.clear = jQuery(document.createElement("span")); + this.itemlist = jQuery(document.createElement("div")); + + // Container elements + this.div.addClass("et2_itempicker"); + this.left.addClass("et2_itempicker_left"); + this.right.addClass("et2_itempicker_right"); + this.right_container.addClass("et2_itempicker_right_container"); + + // Application select + this.app_select.addClass("et2_itempicker_app_select"); + let item_count = 0; + for(let key in this.options.select_options) + { + let img_icon = this.egw().image(key + "/navbar"); + if(img_icon === null) + { + continue; + } + let img = jQuery(document.createElement("img")); + img.attr("src", img_icon); + let item = jQuery(document.createElement("li")); + item.attr("id", key) + .click(function() { + _self.selectApplication(jQuery(this)); + }) + .append(img); + if(item_count == 0) { + this.selectApplication(item); // select first item by default + } + this.app_select.append(item); + item_count++; + } + + // Search input field + this.search.addClass("et2_itempicker_search"); + this.search.keyup(function() { + let request : any = {}; + request.term = jQuery(this).val(); + _self.query(request); + }); + this.set_blur(this.options.blur, this.search); + + // Clear button for search + this.clear + .addClass("et2_itempicker_clear ui-icon ui-icon-close") + .click(function(){ + _self.clearSearchResults(); + }) + .hide(); + + // Action button + this.button_action = et2_createWidget("button"); + jQuery(this.button_action.getDOMNode()).addClass("et2_itempicker_button_action"); + this.button_action.set_label(this.egw().lang(this.options.action_label)); + this.button_action.click = function() { _self.doAction(); }; + + // Itemlist + this.itemlist.attr("id", "itempicker_itemlist"); + this.itemlist.addClass("et2_itempicker_itemlist"); + + // Put everything together + this.left.append(this.app_select); + this.right_container.append(this.search); + this.right_container.append(this.clear); + this.right_container.append(this.button_action.getDOMNode()); + this.right_container.append(this.itemlist); + this.right.append(this.right_container); + this.div.append(this.right); // right before left to have a natural + this.div.append(this.left); // z-index for left div over right div + + this.setDOMNode(this.div[0]); + } + + doAction() + { + if(this.action !== null) + { + let data : any = {}; + data.app = this.current_app; + data.value = this.options.value; + data.checked = this.getSelectedItems(); + return this.action.exec(this, data); + } + + return false; + } + + getSelectedItems() + { + let items = []; + jQuery(this.itemlist).children("ul").children("li.selected").each(function(index) { + items[index] = jQuery(this).attr("id"); + }); + return items; + } + + /** + * Ask server for entries matching selected app/type and filtered by search string + */ + query(request) + { + if(request.term.length < 3) { + return true; + } + // Remember last search + this.last_search = request.term; + + // Allow hook / tie in + if(this.options.query && typeof this.options.query == 'function') + { + if(!this.options.query(request, response)) return false; + } + + //if(request.term in this.cache) { + // return response(this.cache[request.term]); + //} + + this.itemlist.addClass("loading"); + this.clear.css("display", "inline-block"); + egw.json("EGroupware\\Api\\Etemplate\\Widget\\ItemPicker::ajax_item_search", + [this.current_app, '', request.term, request.options], + this.queryResults, + this,true,this + ).sendRequest(); + } + + /** + * Server found some results for query + */ + queryResults(data) + { + this.itemlist.removeClass("loading"); + this.updateItemList(data); + } + + selectApplication(app) + { + this.clearSearchResults(); + jQuery(".et2_itempicker_app_select li").removeClass("selected"); + app.addClass("selected"); + this.current_app = app.attr("id"); + return true; + } + + set_blur(_value, input) + { + if(typeof input == 'undefined') input = this.search; + + if(_value) + { + input.attr("placeholder", _value); // HTML5 + if(!input[0].placeholder) + { + // Not HTML5 + if(input.val() == "") input.val(_value); + input.focus(input,function(e) { + let placeholder = _value; + if(e.data.val() == placeholder) e.data.val(""); + }).blur(input, function(e) { + let placeholder = _value; + if(e.data.val() == "") e.data.val(placeholder); + }); + if(input.val() == "") input.val(_value); + } + } + else + { + this.search.removeAttr("placeholder"); + } + } + + transformAttributes(_attrs) + { + super.transformAttributes(_attrs); + + _attrs["select_options"] = {}; + if(_attrs["application"]) + { + let apps = et2_csvSplit(_attrs["application"], null, ","); + for(let i = 0; i < apps.length; i++) + { + _attrs["select_options"][apps[i]] = this.egw().lang(apps[i]); + } + } + else + { + _attrs["select_options"] = this.egw().link_app_list('query'); + } + + // Check whether the options entry was found, if not read it from the + // content array. + if (_attrs["select_options"] == null) + { + _attrs["select_options"] = this.getArrayMgr('content') + .getEntry("options-" + this.id); + } + + // Default to an empty object + if (_attrs["select_options"] == null) + { + _attrs["select_options"] = {}; + } + } + + updateItemList(data) + { + let list = jQuery(document.createElement("ul")); + let item_count = 0; + for(let id in data) { + let item = jQuery(document.createElement("li")); + if(item_count%2 == 0) + { + item.addClass("row_on"); + } + else + { + item.addClass("row_off"); + } + item.attr("id", id) + .html(data[id]) + .click(function(e) { + if(e.ctrlKey || e.metaKey) + { + // add to selection + jQuery(this).addClass("selected"); + } + else if(e.shiftKey) + { + // select range + let start = jQuery(this).siblings(".selected").first(); + if(start?.length == 0) + { + // no start item - cannot select range - select single item + jQuery(this).addClass("selected"); + return true; + } + let end = jQuery(this); + // swap start and end if start appears after end in dom hierarchy + if(start.index() > end.index()) + { + let startOld = start; + start = end; + end = startOld; + } + // select start to end + start.addClass("selected"); + start.nextUntil(end).addClass("selected"); + end.addClass("selected"); + } + else + { + // select single item + jQuery(this).siblings(".selected").removeClass("selected"); + jQuery(this).addClass("selected"); + } + }); + list.append(item); + item_count++; + } + this.itemlist.html(list); + } +} +et2_register_widget(et2_itempicker, ["itempicker"]); + + diff --git a/api/js/etemplate/et2_widget_link.js b/api/js/etemplate/et2_widget_link.js index 791fd068f0..42a35d7606 100644 --- a/api/js/etemplate/et2_widget_link.js +++ b/api/js/etemplate/et2_widget_link.js @@ -1,4 +1,5 @@ - /** +"use strict"; +/** * EGroupware eTemplate2 - JS Link object * * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License @@ -7,2251 +8,1953 @@ * @link http://www.egroupware.org * @author Nathan Gray * @copyright 2011 Nathan Gray - * @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 __()); + }; +})(); +var _a; +Object.defineProperty(exports, "__esModule", { value: true }); /*egw:uses - /vendor/bower-asset/jquery/dist/jquery.js; - /vendor/bower-asset/jquery-ui/jquery-ui.js; - et2_core_inputWidget; - et2_core_valueWidget; + /vendor/bower-asset/jquery/dist/jquery.js; + /vendor/bower-asset/jquery-ui/jquery-ui.js; + et2_core_inputWidget; + et2_core_valueWidget; - // Include menu system for list context menu - egw_action.egw_menu_dhtmlx; + // Include menu system for list context menu + egw_action.egw_menu_dhtmlx; */ - +var et2_core_widget_1 = require("./et2_core_widget"); +var et2_core_inheritance_1 = require("./et2_core_inheritance"); +var et2_core_valueWidget_1 = require("./et2_core_valueWidget"); +var et2_core_inputWidget_1 = require("./et2_core_inputWidget"); +var et2_widget_selectbox_1 = require("./et2_widget_selectbox"); /** - * UI widgets for Egroupware linking system - * - * @augments et2_inputWidget - */ -var et2_link_to = (function(){ "use strict"; return et2_inputWidget.extend( -{ - attributes: { - "only_app": { - "name": "Application", - "type": "string", - "default": "", - "description": "Limit to just this one application - hides app selection" - }, - "application_list": { - "name": "Application list", - "type": "any", - "default": "", - "description": "Limit to the listed application or applications (comma seperated)" - }, - "blur": { - "name": "Placeholder", - "type": "string", - "default": "", - "description": "This text get displayed if an input-field is empty and does not have the input-focus (blur). It can be used to show a default value or a kind of help-text.", - translate:true - }, - "no_files": { - "name": "No files", - "type": "boolean", - "default": false, - "description": "Suppress attach-files" - }, - "search_label": { - "name": "Search label", - "type": "string", - "default": "", - "description": "Label to use for search" - }, - "link_label": { - "name": "Link label", - "type": "string", - "default": "Link", - "description": "Label for the link button" - }, - "value": { - // Could be string or int if application is provided, or an Object - "type": "any" - } - }, - - /** - * Constructor - * - * @memberOf et2_link_to - */ - init: function() { - this._super.apply(this, arguments); - - this.div = jQuery(document.createElement("div")).addClass("et2_link_to et2_toolbar"); - - this.link_button = null; - this.status_span = null; - - this.link_entry = null; - this.file_upload = null; - - this.createInputWidget(); - }, - - destroy: function() { - this.link_button = null; - this.status_span = null; - - if(this.link_entry) - { - this.link_entry.destroy(); - this.link_entry = null; - } - if(this.file_upload) - { - this.file_upload.destroy(); - this.file_upload = null; - } - this.div = null; - - this._super.apply(this, arguments); - }, - - /** - * Override to provide proper node for sub widgets to go in - * - * @param {Object} _sender - */ - getDOMNode: function(_sender) { - if(_sender == this) { - return this.div[0]; - } else if (_sender._type == 'link-entry') { - return this.link_div[0]; - } else if (_sender._type == 'file') { - return this.file_div[0]; - } else if (_sender._type == 'vfs-select') { - return this.filemanager_button[0]; - } - }, - - createInputWidget: function() { - - // Need a div for file upload widget - this.file_div = jQuery(document.createElement("div")).css({display:'inline-block'}).appendTo(this.div); - - // Filemanager link popup - this.filemanager_button = jQuery(document.createElement("div")).css({display:'inline-block'}).appendTo(this.div); - - // Need a div for link-to widget - this.link_div = jQuery(document.createElement("div")) - .addClass('div_link') - // Leave room for link button - .appendTo(this.div); - - if (!this.options.readonly) - { - // One common link button - this.link_button = jQuery(document.createElement("button")) - .text(this.egw().lang(this.options.link_label)) - .appendTo(this.div).hide() - .addClass('link') - .click(this, this.createLink); - - // Span for indicating status - this.status_span = jQuery(document.createElement("span")) - .appendTo(this.div).addClass("status").hide(); - } - - this.setDOMNode(this.div[0]); - }, - - doLoadingFinished: function() { - this._super.apply(this, arguments); - - var self = this; - if(this.link_entry && this.vfs_select && this.file_upload) - { - // Already done - return false; - } - - // Link-to - var link_entry_attrs = { - id: this.id + '_link_entry', - only_app: this.options.only_app, - application_list: this.options.application_list, - blur: this.options.search_label ? this.options.search_label : this.egw().lang('Search...'), - query: function() { self.link_button.hide(); return true;}, - select: function() {self.link_button.show(); return true;}, - readonly: this.options.readonly - }; - this.link_entry = et2_createWidget("link-entry", link_entry_attrs,this); - - // Filemanager select - var select_attrs = { - button_label: egw.lang('Link'), - button_caption: '', - button_icon:'link', - readonly: this.options.readonly, - dialog_title: egw.lang('Link'), - extra_buttons:[{text: egw.lang("copy"), id:"copy", image: "copy"}, - {text: egw.lang("move"), id:"move", image: "move"}], - onchange: function() { - var values = true; - // If entry not yet saved, store for linking on server - if(!self.options.value.to_id || typeof self.options.value.to_id == 'object') - { - values = self.options.value.to_id || {}; - var files = self.vfs_select.getValue(); - if(typeof files !== 'undefined') - { - for(var i = 0; i < files.length; i++) - { - values['link:'+files[i]] = { - app: 'link', - id: files[i], - type: 'unknown', - icon: 'link', - remark: '', - title: files[i] - }; - } - } - } - self._link_result(values); - } - }; - // only set server-side callback, if we have a real application-id (not null or array) - // otherwise it only gives an error on server-side - if (self.options.value && self.options.value.to_id && typeof self.options.value.to_id != 'object') { - select_attrs.method = 'EGroupware\\Api\\Etemplate\\Widget\\Link::ajax_link_existing'; - select_attrs.method_id = self.options.value.to_app + ':' + self.options.value.to_id; - } - this.vfs_select = et2_createWidget("vfs-select", select_attrs,this); - this.vfs_select.set_readonly(this.options.readonly); - - // File upload - var file_attrs = { - multiple: true, - id: this.id + '_file', - label: '', - // Make the whole template a drop target - drop_target: this.getInstanceManager().DOMContainer.getAttribute("id"), - readonly: this.options.readonly, - - // Change to this tab when they drop - onStart: function(event, file_count) { - // Find the tab widget, if there is one - var tabs = self; - do { - tabs = tabs._parent; - } while (tabs != self.getRoot() && tabs._type != 'tabbox'); - if(tabs != self.getRoot()) - { - // Find the tab index - for(var i = 0; i < tabs.tabData.length; i++) - { - // Find the tab - if(tabs.tabData[i].contentDiv.has(self.div).length) - { - tabs.setActiveTab(i); - break; - } - } - } - return true; - }, - onFinish: function(event, file_count) { - event.data = self; - self.filesUploaded(event); - - // Auto-link uploaded files - self.createLink(event); - } - }; - - this.file_upload = et2_createWidget("file", file_attrs,this); - this.file_upload.set_readonly(this.options.readonly); - return true; - }, - - getValue: function() { - return this.options.value; - }, - - filesUploaded: function(event) { - var self = this; - - this.link_button.show(); - }, - - /** - * Create a link using the current internal values - * - * @param {Object} event - */ - createLink: function(event) { - // Disable link button - event.data.link_button.attr("disabled", true); - - var values = event.data.options.value; - var self = event.data; - - var links = []; - - // Links to other entries - event.data = self.link_entry; - self.link_entry.createLink(event,links); - - // Files - if(!self.options.no_files) - { - for(var file in self.file_upload.options.value) { - - links.push({ - app: 'file', - id: file, - name: self.file_upload.options.value[file].name, - type: self.file_upload.options.value[file].type, - remark: jQuery("li[file='"+self.file_upload.options.value[file].name.replace(/'/g, '"')+"'] > input", self.file_upload.progress) - .filter(function() { return jQuery(this).attr("placeholder") != jQuery(this).val();}).val() - }); - } - } - if(links.length == 0) - { - return; - } - - var request = egw.json("EGroupware\\Api\\Etemplate\\Widget\\Link::ajax_link", - [values.to_app, values.to_id, links], - self._link_result, - self, - true, - self - ); - request.sendRequest(); - }, - - /** - * Sent some links, server has a result - * - * @param {Object} success - */ - _link_result: function(success) { - if(success) { - this.link_button.hide().attr("disabled", false); - this.status_span.removeClass("error").addClass("success"); - this.status_span.fadeIn().delay(1000).fadeOut(); - delete this.options.value.app; - delete this.options.value.id; - for(var file in this.file_upload.options.value) { - delete this.file_upload.options.value[file]; - } - this.file_upload.progress.empty(); - - // Server says it's OK, but didn't store - we'll send this again on submit - // This happens if you link to something before it's saved to the DB - if(typeof success == "object") - { - // Save as appropriate in value - if(typeof this.options.value != "object") - { - this.options.value = {}; - } - this.options.value.to_id = success; - for(var link in success) - { - // Icon should be in registry - if(typeof success[link].icon == 'undefined') - { - success[link].icon = egw.link_get_registry(success[link].app,'icon'); - // No icon, try by mime type - different place for un-saved entries - if(success[link].icon == false && success[link].id.type) - { - // Triggers icon by mime type, not thumbnail or app - success[link].type = success[link].id.type; - success[link].icon = true; - } - } - // Special handling for file - if not existing, we can't ask for title - if(success[link].app == 'file' && typeof success[link].title == 'undefined') - { - success[link].title = success[link].id.name || ''; - } - } - } - - // Look for a link-list with the same ID, refresh it - var self = this; - var list_widget = null; - this.getRoot().iterateOver( - function(widget) { - if(widget.id == self.id) { - list_widget = widget; - if(success === true) - { - widget._get_links(); - } - } - }, - this, et2_link_list - ); - - // If there's an array of data (entry is not yet saved), updating the list will - // not work, so add them in explicitly. - if(list_widget && success) - { - // Clear list - list_widget.set_value(null); - - // Add temp links in - for(var link_id in success) - { - var link = success[link_id]; - if(typeof link.title == 'undefined') - { - // Callback to server for title - egw.link_title(link.app, link.id, function(title) { - link.title = title; - list_widget._add_link(link); - }); - } - else - { - // Add direct - list_widget._add_link(link); - } - } - } - } - else - { - this.status_span.removeClass("success").addClass("error") - .fadeIn(); - } - this.div.trigger('link.et2_link_to',success); - }, - - set_no_files: function(no_files) - { - if(this.options.readonly) return; - if(no_files) - { - this.file_div.hide(); - this.filemanager_button.hide(); - } - else - { - this.file_div.show(); - this.filemanager_button.show(); - } - this.options.no_files = no_files; - } -});}).call(this); -et2_register_widget(et2_link_to, ["link-to"]); - +* UI widgets for Egroupware linking system +*/ +var et2_link_to = /** @class */ (function (_super) { + __extends(et2_link_to, _super); + /** + * Constructor + * + * @memberOf et2_link_to + */ + function et2_link_to(_parent, _attrs, _child) { + var _this = _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_link_to._attributes, _child || {})) || this; + _this.div = jQuery(document.createElement("div")).addClass("et2_link_to et2_toolbar"); + _this.link_button = null; + _this.status_span = null; + _this.link_entry = null; + _this.file_upload = null; + _this.createInputWidget(); + return _this; + } + et2_link_to.prototype.destroy = function () { + this.link_button = null; + this.status_span = null; + if (this.link_entry) { + this.link_entry.destroy(); + this.link_entry = null; + } + if (this.file_upload) { + this.file_upload.destroy(); + this.file_upload = null; + } + this.div = null; + _super.prototype.destroy.apply(this, arguments); + }; + /** + * Override to provide proper node for sub widgets to go in + * + * @param {Object} _sender + */ + et2_link_to.prototype.getDOMNode = function (_sender) { + if (_sender == this) { + return this.div[0]; + } + else if (_sender._type == 'link-entry') { + return this.link_div[0]; + } + else if (_sender._type == 'file') { + return this.file_div[0]; + } + else if (_sender._type == 'vfs-select') { + return this.filemanager_button[0]; + } + }; + et2_link_to.prototype.createInputWidget = function () { + // Need a div for file upload widget + this.file_div = jQuery(document.createElement("div")).css({ display: 'inline-block' }).appendTo(this.div); + // Filemanager link popup + this.filemanager_button = jQuery(document.createElement("div")).css({ display: 'inline-block' }).appendTo(this.div); + // Need a div for link-to widget + this.link_div = jQuery(document.createElement("div")) + .addClass('div_link') + // Leave room for link button + .appendTo(this.div); + if (!this.options.readonly) { + // One common link button + this.link_button = jQuery(document.createElement("button")) + .text(this.egw().lang(this.options.link_label)) + .appendTo(this.div).hide() + .addClass('link') + .click(this, this.createLink); + // Span for indicating status + this.status_span = jQuery(document.createElement("span")) + .appendTo(this.div).addClass("status").hide(); + } + this.setDOMNode(this.div[0]); + }; + et2_link_to.prototype.doLoadingFinished = function () { + _super.prototype.doLoadingFinished.apply(this, arguments); + var self = this; + if (this.link_entry && this.vfs_select && this.file_upload) { + // Already done + return false; + } + // Link-to + var link_entry_attrs = { + id: this.id + '_link_entry', + only_app: this.options.only_app, + application_list: this.options.application_list, + blur: this.options.search_label ? this.options.search_label : this.egw().lang('Search...'), + query: function () { self.link_button.hide(); return true; }, + select: function () { self.link_button.show(); return true; }, + readonly: this.options.readonly + }; + this.link_entry = et2_createWidget("link-entry", link_entry_attrs, this); + // Filemanager select + var select_attrs = { + button_label: egw.lang('Link'), + button_caption: '', + button_icon: 'link', + readonly: this.options.readonly, + dialog_title: egw.lang('Link'), + extra_buttons: [{ text: egw.lang("copy"), id: "copy", image: "copy" }, + { text: egw.lang("move"), id: "move", image: "move" }], + onchange: function () { + var values = true; + // If entry not yet saved, store for linking on server + if (!self.options.value.to_id || typeof self.options.value.to_id == 'object') { + values = self.options.value.to_id || {}; + var files = self.vfs_select.getValue(); + if (typeof files !== 'undefined') { + for (var i = 0; i < files.length; i++) { + values['link:' + files[i]] = { + app: 'link', + id: files[i], + type: 'unknown', + icon: 'link', + remark: '', + title: files[i] + }; + } + } + } + self._link_result(values); + } + }; + // only set server-side callback, if we have a real application-id (not null or array) + // otherwise it only gives an error on server-side + if (self.options.value && self.options.value.to_id && typeof self.options.value.to_id != 'object') { + select_attrs.method = 'EGroupware\\Api\\Etemplate\\Widget\\Link::ajax_link_existing'; + select_attrs.method_id = self.options.value.to_app + ':' + self.options.value.to_id; + } + this.vfs_select = et2_createWidget("vfs-select", select_attrs, this); + this.vfs_select.set_readonly(this.options.readonly); + // File upload + var file_attrs = { + multiple: true, + id: this.id + '_file', + label: '', + // Make the whole template a drop target + drop_target: this.getInstanceManager().DOMContainer.getAttribute("id"), + readonly: this.options.readonly, + // Change to this tab when they drop + onStart: function (event, file_count) { + // Find the tab widget, if there is one + var tabs = self; + do { + tabs = tabs.getParent(); + } while (tabs != self.getRoot() && tabs.getType() != 'tabbox'); + if (tabs != self.getRoot()) { + tabs.activateTab(self); + } + return true; + }, + onFinish: function (event, file_count) { + event.data = self; + self.filesUploaded(event); + // Auto-link uploaded files + self.createLink(event); + } + }; + this.file_upload = et2_createWidget("file", file_attrs, this); + this.file_upload.set_readonly(this.options.readonly); + return true; + }; + et2_link_to.prototype.getValue = function () { + return this.options.value; + }; + et2_link_to.prototype.filesUploaded = function (event) { + var self = this; + this.link_button.show(); + }; + /** + * Create a link using the current internal values + * + * @param {Object} event + */ + et2_link_to.prototype.createLink = function (event) { + // Disable link button + event.data.link_button.attr("disabled", true); + var values = event.data.options.value; + var self = event.data; + var links = []; + // Links to other entries + event.data = self.link_entry; + self.link_entry.createLink(event, links); + // Files + if (!self.options.no_files) { + for (var file in self.file_upload.options.value) { + links.push({ + app: 'file', + id: file, + name: self.file_upload.options.value[file].name, + type: self.file_upload.options.value[file].type, + remark: jQuery("li[file='" + self.file_upload.options.value[file].name.replace(/'/g, '"') + "'] > input", self.file_upload.progress) + .filter(function () { return jQuery(this).attr("placeholder") != jQuery(this).val(); }).val() + }); + } + } + if (links.length == 0) { + return; + } + var request = egw.json("EGroupware\\Api\\Etemplate\\Widget\\Link::ajax_link", [values.to_app, values.to_id, links], self._link_result, self, true, self); + request.sendRequest(); + }; + /** + * Sent some links, server has a result + * + * @param {Object} success + */ + et2_link_to.prototype._link_result = function (success) { + if (success) { + this.link_button.hide().attr("disabled", false); + this.status_span.removeClass("error").addClass("success"); + this.status_span.fadeIn().delay(1000).fadeOut(); + delete this.options.value.app; + delete this.options.value.id; + for (var file in this.file_upload.options.value) { + delete this.file_upload.options.value[file]; + } + this.file_upload.progress.empty(); + // Server says it's OK, but didn't store - we'll send this again on submit + // This happens if you link to something before it's saved to the DB + if (typeof success == "object") { + // Save as appropriate in value + if (typeof this.options.value != "object") { + this.options.value = {}; + } + this.options.value.to_id = success; + for (var link in success) { + // Icon should be in registry + if (typeof success[link].icon == 'undefined') { + success[link].icon = egw.link_get_registry(success[link].app, 'icon'); + // No icon, try by mime type - different place for un-saved entries + if (success[link].icon == false && success[link].id.type) { + // Triggers icon by mime type, not thumbnail or app + success[link].type = success[link].id.type; + success[link].icon = true; + } + } + // Special handling for file - if not existing, we can't ask for title + if (success[link].app == 'file' && typeof success[link].title == 'undefined') { + success[link].title = success[link].id.name || ''; + } + } + } + // Look for a link-list with the same ID, refresh it + var self = this; + var list_widget = null; + this.getRoot().iterateOver(function (widget) { + if (widget.id == self.id) { + list_widget = widget; + if (success === true) { + widget._get_links(); + } + } + }, this, et2_link_list); + // If there's an array of data (entry is not yet saved), updating the list will + // not work, so add them in explicitly. + if (list_widget && success) { + // Clear list + list_widget.set_value(null); + var _loop_1 = function () { + var link = success[link_id]; + if (typeof link.title == 'undefined') { + // Callback to server for title + egw.link_title(link.app, link.id, function (title) { + link.title = title; + list_widget._add_link(link); + }); + } + else { + // Add direct + list_widget._add_link(link); + } + }; + // Add temp links in + for (var link_id in success) { + _loop_1(); + } + } + } + else { + this.status_span.removeClass("success").addClass("error") + .fadeIn(); + } + this.div.trigger('link.et2_link_to', success); + }; + et2_link_to.prototype.set_no_files = function (no_files) { + if (this.options.readonly) + return; + if (no_files) { + this.file_div.hide(); + this.filemanager_button.hide(); + } + else { + this.file_div.show(); + this.filemanager_button.show(); + } + this.options.no_files = no_files; + }; + et2_link_to._attributes = { + "only_app": { + "name": "Application", + "type": "string", + "default": "", + "description": "Limit to just this one application - hides app selection" + }, + "application_list": { + "name": "Application list", + "type": "any", + "default": "", + "description": "Limit to the listed application or applications (comma seperated)" + }, + "blur": { + "name": "Placeholder", + "type": "string", + "default": "", + "description": "This text get displayed if an input-field is empty and does not have the input-focus (blur). It can be used to show a default value or a kind of help-text.", + translate: true + }, + "no_files": { + "name": "No files", + "type": "boolean", + "default": false, + "description": "Suppress attach-files" + }, + "search_label": { + "name": "Search label", + "type": "string", + "default": "", + "description": "Label to use for search" + }, + "link_label": { + "name": "Link label", + "type": "string", + "default": "Link", + "description": "Label for the link button" + }, + "value": { + // Could be string or int if application is provided, or an Object + "type": "any" + } + }; + return et2_link_to; +}(et2_core_inputWidget_1.et2_inputWidget)); +exports.et2_link_to = et2_link_to; +et2_core_widget_1.et2_register_widget(et2_link_to, ["link-to"]); /** - * @augments et2_selectbox + * List of applications that support link */ -var et2_link_apps = (function(){ "use strict"; return et2_selectbox.extend( -{ - attributes: { - "only_app": { - "name": "Application", - "type": "string", - "default": "", - "description": "Limit to just this one application - hides app selection" - }, - "application_list": { - "name": "Application list", - "type": "any", - "default": "", - "description": "Limit to the listed application or applications (comma seperated)" - } - }, - - /** - * Constructor - * - * @memberOf et2_link_apps - */ - init: function() { - this._super.apply(this, arguments); - - if (this.options.select_options != null) - { - // Preset to last application - if(!this.options.value) - { - this.set_value(egw.preference('link_app', this.egw().getAppName())); - } - // Register to update preference - var self = this; - this.input.bind("click",function() { - if (typeof self.options.value != 'undefined') var appname = self.options.value.to_app; - egw.set_preference(appname || self.egw().getAppName(),'link_app',self.getValue()); - }); - } - }, - - /** - * We get some minor speedups by overriding parent searching and directly setting select options - * - * @param {Array} _attrs an array of attributes - */ - transformAttributes: function(_attrs) { - var select_options = {}; - - // Limit to one app - if(_attrs.only_app) { - select_options[_attrs.only_app] = this.egw().lang(_attrs.only_app); - } else if (_attrs.application_list) { - select_options = _attrs.application_list; - } else { - select_options = egw.link_app_list('query'); - if(typeof select_options['addressbook-email'] !== 'undefined') - { - delete select_options['addressbook-email']; - } - } - _attrs.select_options = select_options; - this._super.apply(this, arguments); - } -});}).call(this); -et2_register_widget(et2_link_apps, ["link-apps"]); - +var et2_link_apps = /** @class */ (function (_super) { + __extends(et2_link_apps, _super); + /** + * Constructor + * + */ + function et2_link_apps(_parent, _attrs, _child) { + var _this = _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_link_apps._attributes, _child || {})) || this; + if (_this.options.select_options != null) { + // Preset to last application + if (!_this.options.value) { + _this.set_value(egw.preference('link_app', _this.egw().getAppName())); + } + // Register to update preference + var self = _this; + _this.input.bind("click", function () { + if (typeof self.options.value != 'undefined') + var appname = self.options.value.to_app; + egw.set_preference(appname || self.egw().getAppName(), 'link_app', self.getValue()); + }); + } + return _this; + } + /** + * We get some minor speedups by overriding parent searching and directly setting select options + * + * @param {Array} _attrs an array of attributes + */ + et2_link_apps.prototype.transformAttributes = function (_attrs) { + var select_options = {}; + // Limit to one app + if (_attrs.only_app) { + select_options[_attrs.only_app] = this.egw().lang(_attrs.only_app); + } + else if (_attrs.application_list) { + select_options = _attrs.application_list; + } + else { + select_options = egw.link_app_list('query'); + if (typeof select_options['addressbook-email'] !== 'undefined') { + delete select_options['addressbook-email']; + } + } + _attrs.select_options = select_options; + _super.prototype.transformAttributes.call(this, _attrs); + }; + et2_link_apps._attributes = { + "only_app": { + "name": "Application", + "type": "string", + "default": "", + "description": "Limit to just this one application - hides app selection" + }, + "application_list": { + "name": "Application list", + "type": "any", + "default": "", + "description": "Limit to the listed application or applications (comma seperated)" + } + }; + return et2_link_apps; +}(et2_widget_selectbox_1.et2_selectbox)); +exports.et2_link_apps = et2_link_apps; +et2_core_widget_1.et2_register_widget(et2_link_apps, ["link-apps"]); /** - * @augments et2_inputWidget + * Search and select an entry for linking */ -var et2_link_entry = (function(){ "use strict"; return et2_inputWidget.extend( -{ - attributes: { - "value": { - "type": "any", - "default": {} - }, - "only_app": { - "name": "Application", - "type": "string", - "default": "", - "description": "Limit to just this one application - hides app selection" - }, - "application_list": { - "name": "Application list", - "type": "any", - "default": "", - "description": "Limit to the listed applications (comma seperated)" - }, - "app_icons": { - "name": "Application icons", - "type": "boolean", - "default": false, - "description": "Show application icons instead of names" - }, - "blur": { - "name": "Placeholder", - "type": "string", - "default": et2_no_init, - "description": "This text get displayed if an input-field is empty and does not have the input-focus (blur). It can be used to show a default value or a kind of help-text.", - translate:true - }, - "query": { - "name": "Query callback", - "type": "js", - "default": et2_no_init, - "description": "Callback before query to server. It will be passed the request & et2_link_entry objects. Must return true, or false to abort query." - }, - "select": { - "name": "Select callback", - "type": "js", - "default": et2_no_init, - "description": "Callback when user selects an option. Must return true, or false to abort normal action." - } - }, - - legacyOptions: ["only_app", "application_list"], - search_timeout: 500, //ms after change to send query - minimum_characters: 4, // Don't send query unless there's at least this many chars - - /** - * Constructor - * - * @memberOf et2_link_entry - */ - init: function() { - this._super.apply(this, arguments); - - this.search = null; - this.clear = null; - this.app_select = null; - this._oldValue = { - id: null, - app: this.options.value && this.options.value.app ? this.options.value.app : this.options.only_app - }; - - if(typeof this.options.value == 'undefined' || this.options.value == null) - { - this.options.value = {}; - } - this.cache = {}; - this.request = null; - - this.createInputWidget(); - var self = this; - - jQuery(this.getInstanceManager().DOMContainer).on('clear', function(){ - // We need to unbind events to prevent a second triggerd event handler - // (eg. setting a project in infolog edit dialog) when the widget gets cleared. - jQuery(self.getDOMNode()).off(); - }); - }, - - destroy: function() { - this._super.apply(this, arguments); - - this.div = null; - if(this.search.data("ui-autocomplete")) - { - this.search.autocomplete("destroy"); - } - this.search = null; - this.clear = null; - this.app_select = null; - this.request = null; - }, - - createInputWidget: function() { - var self = this; - this.div = jQuery(document.createElement("div")).addClass("et2_link_entry"); - - // Application selection - jQuery.widget( "custom.iconselectmenu", jQuery.ui.selectmenu, { - _setText: function(element, value){ - if(element === this.buttonText){ - this._setButtonText(value); - } else { - this._superApply(element, value); - } - }, - - _setButtonText: function( value ) { - - var _value = this.focusIndex; - - if(typeof this.focusIndex === 'undefined') - { - _value = this.element.find( "option:selected" ).val(); - } - else - { - var selected = this.items[_value] || {}; - _value = selected.value; - } - var url = self.egw().image('navbar', _value); - var buttonItem = jQuery( "", { - "class": "ui-selectmenu-text", - title: value - }); - - jQuery('.ui-selectmenu-text', this.button).replaceWith(buttonItem); - buttonItem.css('background-image', 'url('+url+')'); - }, - _renderItem: function( ul, item ) { - var li = jQuery( "
        • ", {class:"et2_link_entry_app_option"}), - wrapper = jQuery( "
          ", {text: item.label} ); - - if ( item.disabled ) { - li.addClass( "ui-state-disabled" ); - } - ul.addClass(self.div.class); - var url = self.egw().image('navbar', item.value); - jQuery( "", { - style: 'background-image: url("'+url+'");', - "class": "ui-icon " + item.element.attr( "data-class" ), - title: item.label - }) - .appendTo( wrapper ); - - return li.append( wrapper ).appendTo( ul ); - } - }); - - this.app_select = jQuery(document.createElement("select")).appendTo(this.div) - .change(function(e) { - // Clear cache when app changes - self.cache = {}; - - // Update preference with new value - egw.set_preference(self.options.value.to_app || self.egw().getAppName(),'link_app',self.app_select.val()); - - if(typeof self.options.value != 'object') self.options.value = {}; - self.options.value.app = self.app_select.val(); - }); - var opt_count = 0; - for(var key in this.options.select_options) { - opt_count++; - var option = jQuery(document.createElement("option")) - .attr("value", key) - .text(this.options.select_options[key]); - option.appendTo(this.app_select); - } - if(this.options.only_app) - { - this.app_select.val(this.options.only_app); - this.app_select.hide(); - if(this.options.app_icons && this.app_select.iconselectmenu('instance')) - { - this.app_select.iconselectmenu('widget').hide(); - } - this.div.addClass("no_app"); - } - else - { - // Now that options are in, set to last used app - this.app_select.val(this.options.value.app||''); - if(this.app_select.iconselectmenu('instance')) - { - this.app_select.iconselectmenu('update'); - } - } - - // Search input - this.search = jQuery(document.createElement("input")) - // .attr("type", "search") // Fake it for all browsers below - .focus(function(){if(!self.options.only_app) { - // Adjust width, leave room for app select & link button - self.div.removeClass("no_app"); - if(self.options.app_icons) - { - self.app_select.iconselectmenu('widget').show(); - } - else - { - self.app_select.show(); - } - }}) - .blur(function(e) { - if(self.div.has(e.relatedTarget).length) return; - if(self.options.app_icons) - { - // Adjust width, leave room for app select & link button - self.div.addClass("no_app"); - self.app_select.iconselectmenu('widget').hide(); - } - else if (self.search.val()) - { - if(self.options.only_app) { - // Adjust width, leave room for app select & link button - self.div.addClass("no_app"); - } - } - }) - .appendTo(this.div); - - this.set_blur(this.options.blur ? this.options.blur : this.egw().lang("search"), this.search); - - // Autocomplete - this.search.autocomplete({ - source: function(request, response) { - return self.query(request, response); - }, - select: function(event, item) { - event.data = self; - // Correct changed value from server - item.item.value = item.item.value.trim(); - self.select(event,item); - return false; - }, - focus: function(event, item) { - event.stopPropagation(); - self.search.val(item.item.label); - return false; - }, - minLength: self.minimum_characters, - delay: self.search_timeout, - disabled: self.options.disabled, - appendTo: self.div - }); - - // Custom display (colors) - this.search.data("uiAutocomplete")._renderItem = function(ul, item) { - var li = jQuery(document.createElement('li')) - .data("item.autocomplete", item); - var extra = {}; - - // Extra stuff - if(typeof item.label == 'object') { - extra = item.label; - item.label = extra.label ? extra.label : extra; - if(extra['style.backgroundColor'] || extra.color) - { - li.css({'border-left': '5px solid ' + (extra.color ? extra.color : extra['style.backgroundColor'])}); - } - // Careful with this, some browsers may have trouble loading all at once, which can slow display - if(extra.icon) - { - var img = self.egw().image(extra.icon); - if(img) - { - jQuery(document.createElement("img")) - .attr("src", img) - .css("float", "right") - .appendTo(li); - } - } - } - - // Normal stuff - li.append(jQuery( "" ).text( item.label )) - .appendTo(ul); - window.setTimeout(function(){ul.css('max-width', jQuery('.et2_container').width()-ul.offset().left);}, 300); - return li; - }; - - // Bind to enter key to start search early - this.search.keydown(function(e) { - var keycode = (e.keyCode ? e.keyCode : e.which); - if(keycode == '13' && !self.processing) - { - self.search.autocomplete("option","minLength", 0); - self.search.autocomplete("search"); - self.search.autocomplete("option","minLength", self.minimum_characters); - return false; - } - }); - - // Clear / last button - this.clear = jQuery(document.createElement("span")) - .addClass("ui-icon ui-icon-close") - .click(function(e){ - if (!self.search) return; // only gives an error, we should never get into that situation - // No way to tell if the results is open, so if they click the button while open, it clears - if(self.last_search && self.last_search != self.search.val()) - { - // Repeat last search (should be cached) - self.search.val(self.last_search); - self.last_search = ""; - self.search.autocomplete("search"); - } - else - { - // Clear - self.search.autocomplete("close"); - self.set_value(null); - self.search.val(""); - // call trigger, after finishing this handler, not in the middle of it - window.setTimeout(function() - { - self.search.trigger("change"); - }, 0); - } - self.search.focus(); - }) - .appendTo(this.div) - .hide(); - - this.setDOMNode(this.div[0]); - }, - - getDOMNode: function() { - return this.div ? this.div[0] : null; - }, - - transformAttributes: function(_attrs) { - this._super.apply(this, arguments); - - - _attrs["select_options"] = {}; - if(_attrs["application_list"]) - { - var apps = (typeof _attrs["application_list"] == "string") ? et2_csvSplit(_attrs["application_list"], null, ","): _attrs["application_list"]; - for(var i = 0; i < apps.length; i++) - { - _attrs["select_options"][apps[i]] = this.egw().lang(apps[i]); - } - } - else - { - _attrs["select_options"] = this.egw().link_app_list('query'); - if (typeof _attrs["select_options"]["addressbook-email"] != 'undefined') delete _attrs["select_options"]["addressbook-email"]; - } - - // Check whether the options entry was found, if not read it from the - // content array. - if (_attrs["select_options"] == null) - { - _attrs["select_options"] = this.getArrayMgr('content') - .getEntry("options-" + this.id); - } - - // Default to an empty object - if (_attrs["select_options"] == null) - { - _attrs["select_options"] = {}; - } - }, - - doLoadingFinished: function() { - if(typeof this.options.value == 'object' && !this.options.value.app) - { - this.options.value.app = egw.preference('link_app',this.options.value.to_app || this.egw().getAppName()); - // If there's no value set for app, then take the first one from the selectbox - if (typeof this.options.value.app == 'undefined' || !this.options.value.app) - { - this.options.value.app = Object.keys(this.options.select_options)[0]; - } - this.app_select.val(this.options.value.app); - } - - if(this.options.app_icons) - { - var self = this; - this.div.addClass('app_icons'); - this.app_select.iconselectmenu({ - width: 50, - change: function() { - window.setTimeout(function() - { - self.app_select.trigger("change"); - }, 0); - } - }) - .iconselectmenu( "menuWidget" ); - this.app_select.iconselectmenu('widget').hide(); - } - return this._super.apply(this,arguments); - }, - - getValue: function() { - var value = this.options && this.options.only_app ? this.options.value.id : this.options? this.options.value: null; - if(this.options && !this.options.only_app && this.search) - { - value.search = this.search.val(); - } - return value; - }, - - set_value: function(_value) { - if(typeof _value == 'string' || typeof _value == 'number') - { - if(typeof _value == 'string' && _value.indexOf(",") > 0) _value = _value.replace(",",":"); - if(typeof _value == 'string' && _value.indexOf(":") >= 0) - { - var split = _value.split(":"); - - _value = { - app: split.shift(), - id: split.length == 1 ? split[0] : split - }; - } - else if(_value && this.options.only_app) - { - _value = { - app: this.options.only_app, - id: _value - }; - } - } - this._oldValue = this.options.value; - if(!_value || _value.length == 0 || _value == null || jQuery.isEmptyObject(_value)) - { - this.search.val(""); - this.clear.hide(); - this.options.value = _value = {'id':null}; - } - if(!_value.app) _value.app = this.options.only_app || this.app_select.val(); - - if(_value.id) { - // Remove specific display and revert to CSS file - // show() would use inline, should be inline-block - this.clear.css('display',''); - } else { - this.clear.hide(); - return; - } - if(typeof _value != 'object' || (!_value.app && !_value.id)) - { - console.warn("Bad value for link widget. Need an object with keys 'app', 'id', and optionally 'title'", _value); - return; - } - if(!_value.title) { - var title = this.egw().link_title(_value.app, _value.id); - if(title != null) { - _value.title = title; - } - else - { - // Title will be fetched from server and then set - var title = this.egw().link_title(_value.app, _value.id, function(title) { - this.search.removeClass("loading").val(title+""); - // Remove specific display and revert to CSS file - // show() would use inline, should be inline-block - this.clear.css('display',''); - }, this); - this.search.addClass("loading"); - } - } - if(_value.title) - { - this.search.val(_value.title+""); - } - this.options.value = _value; - - jQuery("option[value='"+_value.app+"']",this.app_select).prop("selected",true); - this.app_select.hide(); - if(this.options.app_icons && this.app_select.iconselectmenu('instance')) - { - this.app_select.iconselectmenu('widget').hide(); - } - this.div.addClass("no_app"); - }, - - set_blur: function(_value, input) { - - if(typeof input == 'undefined') input = this.search; - - if(_value) { - input.attr("placeholder", _value); // HTML5 - if(!input[0].placeholder) { - // Not HTML5 - if(input.val() == "") input.val(_value); - input.focus(input,function(e) { - var placeholder = _value; - if(e.data.val() == placeholder) e.data.val(""); - }).blur(input, function(e) { - var placeholder = _value; - if(e.data.val() == "") e.data.val(placeholder); - }); - if(input.val() == "") input.val(_value); - } - } else { - this.search.removeAttr("placeholder"); - } - }, - - /** - * Set the query callback - * - * @param {function} f - */ - set_query: function(f) - { - this.options.query = f; - }, - - /** - * Set the select callback - * - * @param {function} f - */ - set_select: function(f) - { - this.options.select = f; - }, - - /** - * Ask server for entries matching selected app/type and filtered by search string - * - * @param {Object} request - * @param {Object} response - */ - query: function(request, response) { - // If there is a pending request, abort it - if(this.request) - { - this.request.abort(); - this.request = null; - } - - // Remember last search - this.last_search = this.search.val(); - - // Allow hook / tie in - if(this.options.query && typeof this.options.query == 'function') - { - if(!this.options.query(request, this)) return false; - } - - if((typeof request.no_cache == 'undefined' && !request.no_cache) && request.term in this.cache) { - return response(this.cache[request.term]); - } - - // Remember callback - this.response = response; - - this.search.addClass("loading"); - // Remove specific display and revert to CSS file - // show() would use inline, should be inline-block - this.clear.css('display',''); - this.request = egw.json("EGroupware\\Api\\Etemplate\\Widget\\Link::ajax_link_search", - [this.app_select.val(), '', request.term, request.options], - this._results, - this,true,this - ).sendRequest(); - }, - - /** - * User selected a value - * - * @param {Object} event - * @param {Object} selected - * - */ - select: function(event, selected) { - if(selected.item.value !== null && typeof selected.item.value == "string") - { - // Correct changed value from server - selected.item.value = selected.item.value.trim(); - } - if(this.options.select && typeof this.options.select == 'function') - { - if(!this.options.select(event, selected)) return false; - } - if(typeof event.data.options.value != 'object' || event.data.options.value == null) - { - event.data.options.value = {}; - } - event.data.options.value.id = selected.item.value; - - // Set a processing flag to filter some events - event.data.processing = true; - - // Remove specific display and revert to CSS file - // show() would use inline, should be inline-block - this.clear.css('display',''); - event.data.search.val(selected.item.label); - - // Fire change event - this.search.change(); - - // Turn off processing flag when done - window.setTimeout(jQuery.proxy(function() {delete this.processing;},event.data)); - }, - - /** - * Server found some results - * - * @param {Array} data - */ - _results: function(data) { - if(this.request) - { - this.request = null; - } - this.search.removeClass("loading"); - var result = []; - for(var id in data) { - result.push({"value": id, "label":data[id]}); - } - this.cache[this.search.val()] = result; - this.response(result); - }, - - /** - * Create a link using the current internal values - * - * @param {Object} event - * @param {Object} _links - */ - createLink: function(event, _links) { - - var values = event.data.options.value; - var self = event.data; - var links = []; - - if(typeof _links == 'undefined') - { - links = []; - } - else - { - links = _links; - } - - // Links to other entries - if(values.id) { - links.push({ - app: values.app, - id: values.id - }); - self.search.val(""); - } - - // If a link array was passed in, don't make the ajax call - if(typeof _links == 'undefined') - { - var request = egw.json("EGroupware\\Api\\Etemplate\\Widget\\Link::ajax_link", - [values.to_app, values.to_id, links], - self._link_result, - this, - true - ); - request.sendRequest(); - } - }, - - /** - * Sent some links, server has a result - * - * @param {Object} success - * - */ - _link_result: function(success) { - if(success) { - this.link_button.hide().attr("disabled", false); - this.status_span.fadeIn().delay(1000).fadeOut(); - delete this.options.value.app; - delete this.options.value.id; - } - } -});}).call(this); -et2_register_widget(et2_link_entry, ["link-entry"]); - +var et2_link_entry = /** @class */ (function (_super) { + __extends(et2_link_entry, _super); + /** + * Constructor + * + * @memberOf et2_link_entry + */ + function et2_link_entry(_parent, _attrs, _child) { + var _this = _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_link_entry._attributes, _child || {})) || this; + _this.cache = {}; + _this.processing = false; + _this.search = null; + _this.clear = null; + _this.app_select = null; + _this._oldValue = { + id: null, + app: _this.options.value && _this.options.value.app ? _this.options.value.app : _this.options.only_app + }; + if (typeof _this.options.value == 'undefined' || _this.options.value == null) { + _this.options.value = {}; + } + _this.cache = {}; + _this.request = null; + _this.createInputWidget(); + var self = _this; + jQuery(_this.getInstanceManager().DOMContainer).on('clear', function () { + // We need to unbind events to prevent a second triggerd event handler + // (eg. setting a project in infolog edit dialog) when the widget gets cleared. + jQuery(self.getDOMNode()).off(); + }); + return _this; + } + et2_link_entry.prototype.destroy = function () { + _super.prototype.destroy.apply(this, arguments); + this.div = null; + if (this.search.data("ui-autocomplete")) { + this.search.autocomplete("destroy"); + } + this.search = null; + this.clear = null; + this.app_select = null; + this.request = null; + }; + et2_link_entry.prototype.createInputWidget = function () { + var self = this; + this.div = jQuery(document.createElement("div")).addClass("et2_link_entry"); + // Application selection + jQuery.widget("custom.iconselectmenu", jQuery.ui.selectmenu, { + _setText: function (element, value) { + if (element === this.buttonText) { + this._setButtonText(value); + } + else { + this._superApply(element, value); + } + }, + _setButtonText: function (value) { + var _value = this.focusIndex; + if (typeof this.focusIndex === 'undefined') { + _value = this.element.find("option:selected").val(); + } + else { + var selected = this.items[_value] || {}; + _value = selected.value; + } + var url = self.egw().image('navbar', _value); + var buttonItem = jQuery("", { + "class": "ui-selectmenu-text", + title: value + }); + jQuery('.ui-selectmenu-text', this.button).replaceWith(buttonItem); + buttonItem.css('background-image', 'url(' + url + ')'); + }, + _renderItem: function (ul, item) { + var li = jQuery("
        • ", { class: "et2_link_entry_app_option" }), wrapper = jQuery("
          ", { text: item.label }); + if (item.disabled) { + li.addClass("ui-state-disabled"); + } + ul.addClass(self.div.attr("class")); + var url = self.egw().image('navbar', item.value); + jQuery("", { + style: 'background-image: url("' + url + '");', + "class": "ui-icon " + item.element.attr("data-class"), + title: item.label + }) + .appendTo(wrapper); + return li.append(wrapper).appendTo(ul); + } + }); + this.app_select = jQuery(document.createElement("select")).appendTo(this.div) + .change(function (e) { + // Clear cache when app changes + self.cache = {}; + // Update preference with new value + egw.set_preference(self.options.value.to_app || self.egw().getAppName(), 'link_app', self.app_select.val()); + if (typeof self.options.value != 'object') + self.options.value = {}; + self.options.value.app = self.app_select.val(); + }); + var opt_count = 0; + for (var key in this.options.select_options) { + opt_count++; + var option = jQuery(document.createElement("option")) + .attr("value", key) + .text(this.options.select_options[key]); + option.appendTo(this.app_select); + } + if (this.options.only_app) { + this.app_select.val(this.options.only_app); + this.app_select.hide(); + if (this.options.app_icons && this.app_select.iconselectmenu('instance')) { + this.app_select.iconselectmenu('widget').hide(); + } + this.div.addClass("no_app"); + } + else { + // Now that options are in, set to last used app + this.app_select.val(this.options.value.app || ''); + if (this.app_select.iconselectmenu('instance')) { + this.app_select.iconselectmenu('update'); + } + } + // Search input + this.search = jQuery(document.createElement("input")) + // .attr("type", "search") // Fake it for all browsers below + .focus(function () { + if (!self.options.only_app) { + // Adjust width, leave room for app select & link button + self.div.removeClass("no_app"); + if (self.options.app_icons) { + self.app_select.iconselectmenu('widget').show(); + } + else { + self.app_select.show(); + } + } + }) + .blur(function (e) { + if (self.div.has(e.relatedTarget).length) + return; + if (self.options.app_icons) { + // Adjust width, leave room for app select & link button + self.div.addClass("no_app"); + self.app_select.iconselectmenu('widget').hide(); + } + else if (self.search.val()) { + if (self.options.only_app) { + // Adjust width, leave room for app select & link button + self.div.addClass("no_app"); + } + } + }) + .appendTo(this.div); + this.set_blur(this.options.blur ? this.options.blur : this.egw().lang("search"), this.search); + // Autocomplete + this.search.autocomplete({ + source: function (request, response) { + return self.query(request, response); + }, + select: function (event, item) { + event.data = self; + // Correct changed value from server + item.item.value = item.item.value.trim(); + self.select(event, item); + return false; + }, + focus: function (event, item) { + event.stopPropagation(); + self.search.val(item.item.label); + return false; + }, + minLength: et2_link_entry.minimum_characters, + delay: et2_link_entry.search_timeout, + disabled: self.options.disabled, + appendTo: self.div + }); + // Custom display (colors) + this.search.data("uiAutocomplete")._renderItem = function (ul, item) { + var li = jQuery(document.createElement('li')) + .data("item.autocomplete", item); + var extra = {}; + // Extra stuff + if (typeof item.label == 'object') { + extra = item.label; + item.label = extra.label ? extra.label : extra; + if (extra['style.backgroundColor'] || extra.color) { + li.css({ 'border-left': '5px solid ' + (extra.color ? extra.color : extra['style.backgroundColor']) }); + } + // Careful with this, some browsers may have trouble loading all at once, which can slow display + if (extra.icon) { + var img = self.egw().image(extra.icon); + if (img) { + jQuery(document.createElement("img")) + .attr("src", img) + .css("float", "right") + .appendTo(li); + } + } + } + // Normal stuff + li.append(jQuery("").text(item.label)) + .appendTo(ul); + window.setTimeout(function () { ul.css('max-width', jQuery('.et2_container').width() - ul.offset().left); }, 300); + return li; + }; + // Bind to enter key to start search early + this.search.keydown(function (e) { + var keycode = (e.keyCode ? e.keyCode : e.which); + if (keycode == 13 && !self.processing) { + self.search.autocomplete("option", "minLength", 0); + self.search.autocomplete("search"); + self.search.autocomplete("option", "minLength", self.minimum_characters); + return false; + } + }); + // Clear / last button + this.clear = jQuery(document.createElement("span")) + .addClass("ui-icon ui-icon-close") + .click(function (e) { + if (!self.search) + return; // only gives an error, we should never get into that situation + // No way to tell if the results is open, so if they click the button while open, it clears + if (self.last_search && self.last_search != self.search.val()) { + // Repeat last search (should be cached) + self.search.val(self.last_search); + self.last_search = ""; + self.search.autocomplete("search"); + } + else { + // Clear + self.search.autocomplete("close"); + self.set_value(null); + self.search.val(""); + // call trigger, after finishing this handler, not in the middle of it + window.setTimeout(function () { + self.search.trigger("change"); + }, 0); + } + self.search.focus(); + }) + .appendTo(this.div) + .hide(); + this.setDOMNode(this.div[0]); + }; + et2_link_entry.prototype.getDOMNode = function () { + return this.div ? this.div[0] : null; + }; + et2_link_entry.prototype.transformAttributes = function (_attrs) { + _super.prototype.transformAttributes.apply(this, arguments); + _attrs["select_options"] = {}; + if (_attrs["application_list"]) { + var apps = (typeof _attrs["application_list"] == "string") ? et2_csvSplit(_attrs["application_list"], null, ",") : _attrs["application_list"]; + for (var i = 0; i < apps.length; i++) { + _attrs["select_options"][apps[i]] = this.egw().lang(apps[i]); + } + } + else { + _attrs["select_options"] = this.egw().link_app_list('query'); + if (typeof _attrs["select_options"]["addressbook-email"] != 'undefined') + delete _attrs["select_options"]["addressbook-email"]; + } + // Check whether the options entry was found, if not read it from the + // content array. + if (_attrs["select_options"] == null) { + _attrs["select_options"] = this.getArrayMgr('content') + .getEntry("options-" + this.id); + } + // Default to an empty object + if (_attrs["select_options"] == null) { + _attrs["select_options"] = {}; + } + }; + et2_link_entry.prototype.doLoadingFinished = function () { + if (typeof this.options.value == 'object' && !this.options.value.app) { + this.options.value.app = egw.preference('link_app', this.options.value.to_app || this.egw().getAppName()); + // If there's no value set for app, then take the first one from the selectbox + if (typeof this.options.value.app == 'undefined' || !this.options.value.app) { + this.options.value.app = Object.keys(this.options.select_options)[0]; + } + this.app_select.val(this.options.value.app); + } + if (this.options.app_icons) { + var self = this; + this.div.addClass('app_icons'); + this.app_select.iconselectmenu({ + width: 50, + change: function () { + window.setTimeout(function () { + self.app_select.trigger("change"); + }, 0); + } + }) + .iconselectmenu("menuWidget"); + this.app_select.iconselectmenu('widget').hide(); + } + return _super.prototype.doLoadingFinished.apply(this, arguments); + }; + et2_link_entry.prototype.getValue = function () { + var value = this.options && this.options.only_app ? this.options.value.id : this.options ? this.options.value : null; + if (this.options && !this.options.only_app && this.search) { + value.search = this.search.val(); + } + return value; + }; + et2_link_entry.prototype.set_value = function (_value) { + if (typeof _value == 'string' || typeof _value == 'number') { + if (typeof _value == 'string' && _value.indexOf(",") > 0) + _value = _value.replace(",", ":"); + if (typeof _value == 'string' && _value.indexOf(":") >= 0) { + var split = _value.split(":"); + _value = { + app: split.shift(), + id: split.length == 1 ? split[0] : split + }; + } + else if (_value && this.options.only_app) { + _value = { + app: this.options.only_app, + id: _value + }; + } + } + this._oldValue = this.options.value; + if (!_value || _value.length == 0 || _value == null || jQuery.isEmptyObject(_value)) { + this.search.val(""); + this.clear.hide(); + this.options.value = _value = { 'id': null }; + } + if (!_value.app) + _value.app = this.options.only_app || this.app_select.val(); + if (_value.id) { + // Remove specific display and revert to CSS file + // show() would use inline, should be inline-block + this.clear.css('display', ''); + } + else { + this.clear.hide(); + return; + } + if (typeof _value != 'object' || (!_value.app && !_value.id)) { + console.warn("Bad value for link widget. Need an object with keys 'app', 'id', and optionally 'title'", _value); + return; + } + if (!_value.title) { + var title = this.egw().link_title(_value.app, _value.id); + if (title != null) { + _value.title = title; + } + else { + // Title will be fetched from server and then set + var title = this.egw().link_title(_value.app, _value.id, function (title) { + this.search.removeClass("loading").val(title + ""); + // Remove specific display and revert to CSS file + // show() would use inline, should be inline-block + this.clear.css('display', ''); + }, this); + this.search.addClass("loading"); + } + } + if (_value.title) { + this.search.val(_value.title + ""); + } + this.options.value = _value; + jQuery("option[value='" + _value.app + "']", this.app_select).prop("selected", true); + this.app_select.hide(); + if (this.options.app_icons && this.app_select.iconselectmenu('instance')) { + this.app_select.iconselectmenu('widget').hide(); + } + this.div.addClass("no_app"); + }; + et2_link_entry.prototype.set_blur = function (_value, input) { + if (typeof input == 'undefined') + input = this.search; + if (_value) { + input.attr("placeholder", _value); // HTML5 + if (!input[0].placeholder) { + // Not HTML5 + if (input.val() == "") + input.val(_value); + input.focus(input, function (e) { + var placeholder = _value; + if (e.data.val() == placeholder) + e.data.val(""); + }).blur(input, function (e) { + var placeholder = _value; + if (e.data.val() == "") + e.data.val(placeholder); + }); + if (input.val() == "") { + input.val(_value); + } + } + } + else { + this.search.removeAttr("placeholder"); + } + }; + /** + * Set the query callback + * + * @param {function} f + */ + et2_link_entry.prototype.set_query = function (f) { + this.options.query = f; + }; + /** + * Set the select callback + * + * @param {function} f + */ + et2_link_entry.prototype.set_select = function (f) { + this.options.select = f; + }; + /** + * Ask server for entries matching selected app/type and filtered by search string + * + * @param {Object} request + * @param {Object} response + */ + et2_link_entry.prototype.query = function (request, response) { + // If there is a pending request, abort it + if (this.request) { + this.request.abort(); + this.request = null; + } + // Remember last search + this.last_search = this.search.val(); + // Allow hook / tie in + if (this.options.query && typeof this.options.query == 'function') { + if (!this.options.query(request, this)) + return false; + } + if ((typeof request.no_cache == 'undefined' && !request.no_cache) && request.term in this.cache) { + return response(this.cache[request.term]); + } + // Remember callback + this.response = response; + this.search.addClass("loading"); + // Remove specific display and revert to CSS file + // show() would use inline, should be inline-block + this.clear.css('display', ''); + this.request = egw.json("EGroupware\\Api\\Etemplate\\Widget\\Link::ajax_link_search", [this.app_select.val(), '', request.term, request.options], this._results, this, true, this).sendRequest(); + }; + /** + * User selected a value + * + * @param {Object} event + * @param {Object} selected + * + */ + et2_link_entry.prototype.select = function (event, selected) { + if (selected.item.value !== null && typeof selected.item.value == "string") { + // Correct changed value from server + selected.item.value = selected.item.value.trim(); + } + if (this.options.select && typeof this.options.select == 'function') { + if (!this.options.select(event, selected)) + return false; + } + if (typeof event.data.options.value != 'object' || event.data.options.value == null) { + event.data.options.value = {}; + } + event.data.options.value.id = selected.item.value; + // Set a processing flag to filter some events + event.data.processing = true; + // Remove specific display and revert to CSS file + // show() would use inline, should be inline-block + this.clear.css('display', ''); + event.data.search.val(selected.item.label); + // Fire change event + this.search.change(); + // Turn off processing flag when done + window.setTimeout(jQuery.proxy(function () { delete this.processing; }, event.data)); + }; + /** + * Server found some results + * + * @param {Array} data + */ + et2_link_entry.prototype._results = function (data) { + if (this.request) { + this.request = null; + } + this.search.removeClass("loading"); + var result = []; + for (var id in data) { + result.push({ "value": id, "label": data[id] }); + } + this.cache[this.search.val()] = result; + this.response(result); + }; + /** + * Create a link using the current internal values + * + * @param {Object} event + * @param {Object} _links + */ + et2_link_entry.prototype.createLink = function (event, _links) { + var values = event.data.options.value; + var self = event.data; + var links = []; + if (typeof _links == 'undefined') { + links = []; + } + else { + links = _links; + } + // Links to other entries + if (values.id) { + links.push({ + app: values.app, + id: values.id + }); + self.search.val(""); + } + // If a link array was passed in, don't make the ajax call + if (typeof _links == 'undefined') { + var request = egw.json("EGroupware\\Api\\Etemplate\\Widget\\Link::ajax_link", [values.to_app, values.to_id, links], self._link_result, this, true); + request.sendRequest(); + } + }; + /** + * Sent some links, server has a result + * + * @param {Object} success + * + */ + et2_link_entry.prototype._link_result = function (success) { + if (success) { + this.status_span.fadeIn().delay(1000).fadeOut(); + delete this.options.value.app; + delete this.options.value.id; + } + }; + et2_link_entry._attributes = { + "value": { + "type": "any", + "default": {} + }, + "only_app": { + "name": "Application", + "type": "string", + "default": "", + "description": "Limit to just this one application - hides app selection" + }, + "application_list": { + "name": "Application list", + "type": "any", + "default": "", + "description": "Limit to the listed applications (comma seperated)" + }, + "app_icons": { + "name": "Application icons", + "type": "boolean", + "default": false, + "description": "Show application icons instead of names" + }, + "blur": { + "name": "Placeholder", + "type": "string", + "default": et2_no_init, + "description": "This text get displayed if an input-field is empty and does not have the input-focus (blur). It can be used to show a default value or a kind of help-text.", + translate: true + }, + "query": { + "name": "Query callback", + "type": "js", + "default": et2_no_init, + "description": "Callback before query to server. It will be passed the request & et2_link_entry objects. Must return true, or false to abort query." + }, + "select": { + "name": "Select callback", + "type": "js", + "default": et2_no_init, + "description": "Callback when user selects an option. Must return true, or false to abort normal action." + } + }; + et2_link_entry.legacyOptions = ["only_app", "application_list"]; + et2_link_entry.search_timeout = 500; //ms after change to send query + et2_link_entry.minimum_characters = 4; // Don't send query unless there's at least this many chars + return et2_link_entry; +}(et2_core_inputWidget_1.et2_inputWidget)); +exports.et2_link_entry = et2_link_entry; +et2_core_widget_1.et2_register_widget(et2_link_entry, ["link-entry"]); /** * UI widget for a single (read-only) link * - * @augments et2_valueWidget */ -var et2_link = (function(){ "use strict"; return et2_valueWidget.extend([et2_IDetachedDOM], -{ - attributes: { - "only_app": { - "name": "Application", - "type": "string", - "default": "", - "description": "Use the given application, so you can pass just the ID for value" - }, - "value": { - description: "Array with keys app, id, and optionally title", - type: "any" - }, - "needed": { - "ignore": true - }, - "link_hook": { - "name": "Type", - "type": "string", - "default": "view", - "description": "Hook used for displaying link (view/edit/add)" - }, - "target_app": { - "name": "Target application", - "type": "string", - "default": "", - "description": "Optional parameter to be passed to egw().open in order to open links in specified application" - }, - "extra_link_target": { - "name": "Link target", - "type": "string", - "default": null, - "description": "Link target descriptor" - }, - }, - legacyOptions: ["only_app"], - - /** - * Constructor - * - * @memberOf et2_link - */ - init: function() { - this._super.apply(this, arguments); - - this.label_span = jQuery(document.createElement("label")) - .addClass("et2_label"); - this.link = jQuery(document.createElement("span")) - .addClass("et2_link") - .appendTo(this.label_span); - - if(this.options['class']) this.label_span.addClass(this.options['class']); - this.setDOMNode(this.label_span[0]); - }, - destroy: function() { - if(this.link) this.link.unbind(); - this.link = null; - this._super.apply(this, arguments); - }, - set_label: function(label) { - // Remove current label - this.label_span.contents() - .filter(function(){ return this.nodeType == 3; }).remove(); - - var parts = et2_csvSplit(label, 2, "%s"); - this.label_span.prepend(parts[0]); - this.label_span.append(parts[1]); - this.label = label; - - // add class if label is empty - this.label_span.toggleClass('et2_label_empty', !label || !parts[0]); - }, - set_value: function(_value) { - if(typeof _value != 'object' && _value && !this.options.only_app) - { - if(_value.indexOf(':') >= 0) - { - var app = _value.split(':',1); - var id = _value.substr(app[0].length+1); - _value = {'app': app[0], 'id': id}; - } - else - { - console.warn("Bad value for link widget. Need an object with keys 'app', 'id', and optionally 'title'", _value); - return; - } - } - // Application set, just passed ID - else if (typeof _value != "object") - { - _value = { - app: this.options.only_app, - id: _value - }; - } - if(!_value || jQuery.isEmptyObject(_value)) { - this.link.text("").unbind(); - return; - } - var self = this; - this.link.unbind(); - if(_value.id && _value.app) - { - this.link.addClass("et2_link"); - this.link.click( function(e){ - if( !self.options.target_app ){ - self.options.target_app = _value.app; - } - const target = self.options.extra_link_target || _value.app; - self.egw().open(_value, "", self.options.link_hook, _value.extra_args, target, self.options.target_app); - e.stopImmediatePropagation(); - }); - } - else - { - this.link.removeClass("et2_link"); - } - if(!_value.title) { - var self = this; - var node = this.link[0]; - if(_value.app && _value.id) - { - var title = this.egw().link_title(_value.app, _value.id, function(title) {self.set_title(node, title);}, this); - if(title != null) { - _value.title = title; - } - else - { - // Title will be fetched from server and then set - return; - } - } - else - { - _value.title = ""; - } - } - this.set_title(this.link, _value.title); - }, - - /** - * Sets the text to be displayed. - * Used as a callback, so node is provided to make sure we get the right one - * - * @param {Object} node - * @param {String} _value description - */ - set_title: function(node, _value) { - if(_value === false || _value === null) _value = ""; - jQuery(node).text(_value+""); - }, - - /** - * 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 an array of attributes - */ - getDetachedAttributes: function(_attrs) { - _attrs.push("label","value"); - }, - - /** - * 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() { - return [this.node, this.link[0]]; - }, - - /** - * 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) { - this.node = _nodes[0]; - this.label_span = jQuery(_nodes[0]); - this.link = jQuery(_nodes[1]); - if(typeof _values["id"] !== "undefined") this.set_id(_values['id']); - if(typeof _values["label"] !== "undefined") this.set_label(_values['label']); - if(typeof _values["value"] !== "undefined") this.set_value(_values["value"]); - } - -});}).call(this); -et2_register_widget(et2_link, ["link", "link-entry_ro"]); - +var et2_link = /** @class */ (function (_super) { + __extends(et2_link, _super); + /** + * Constructor + * + * @memberOf et2_link + */ + function et2_link(_parent, _attrs, _child) { + var _this = _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_link._attributes, _child || {})) || this; + _this.legacyOptions = ["only_app"]; + _this.label_span = jQuery(document.createElement("label")) + .addClass("et2_label"); + _this.link = jQuery(document.createElement("span")) + .addClass("et2_link") + .appendTo(_this.label_span); + if (_this.options['class']) + _this.label_span.addClass(_this.options['class']); + _this.setDOMNode(_this.label_span[0]); + return _this; + } + et2_link.prototype.destroy = function () { + if (this.link) + this.link.unbind(); + this.link = null; + _super.prototype.destroy.apply(this, arguments); + }; + et2_link.prototype.set_label = function (label) { + // Remove current label + this.label_span.contents() + .filter(function () { return this.nodeType == 3; }).remove(); + var parts = et2_csvSplit(label, 2, "%s"); + this.label_span.prepend(parts[0]); + this.label_span.append(parts[1]); + this.label = label; + // add class if label is empty + this.label_span.toggleClass('et2_label_empty', !label || !parts[0]); + }; + et2_link.prototype.set_value = function (_value) { + if (typeof _value != 'object' && _value && !this.options.only_app) { + if (_value.indexOf(':') >= 0) { + var app = _value.split(':', 1); + var id = _value.substr(app[0].length + 1); + _value = { 'app': app[0], 'id': id }; + } + else { + console.warn("Bad value for link widget. Need an object with keys 'app', 'id', and optionally 'title'", _value); + return; + } + } + // Application set, just passed ID + else if (typeof _value != "object") { + _value = { + app: this.options.only_app, + id: _value + }; + } + if (!_value || jQuery.isEmptyObject(_value)) { + this.link.text("").unbind(); + return; + } + var self = this; + this.link.unbind(); + if (_value.id && _value.app) { + this.link.addClass("et2_link"); + this.link.click(function (e) { + if (!self.options.target_app) { + self.options.target_app = _value.app; + } + var target = self.options.extra_link_target || _value.app; + self.egw().open(_value, "", self.options.link_hook, _value.extra_args, target, self.options.target_app); + e.stopImmediatePropagation(); + }); + } + else { + this.link.removeClass("et2_link"); + } + if (!_value.title) { + var self = this; + var node = this.link[0]; + if (_value.app && _value.id) { + var title = this.egw().link_title(_value.app, _value.id, function (title) { self.set_title(node, title); }, this); + if (title != null) { + _value.title = title; + } + else { + // Title will be fetched from server and then set + return; + } + } + else { + _value.title = ""; + } + } + this.set_title(this.link, _value.title); + }; + /** + * Sets the text to be displayed. + * Used as a callback, so node is provided to make sure we get the right one + * + * @param {Object} node + * @param {String} _value description + */ + et2_link.prototype.set_title = function (node, _value) { + if (_value === false || _value === null) + _value = ""; + jQuery(node).text(_value + ""); + }; + /** + * 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 an array of attributes + */ + et2_link.prototype.getDetachedAttributes = function (_attrs) { + _attrs.push("label", "value"); + }; + /** + * Returns an array of DOM nodes. The (relatively) same DOM-Nodes have to be + * passed to the "setDetachedAttributes" function in the same order. + */ + et2_link.prototype.getDetachedNodes = function () { + return [this.node, this.link[0]]; + }; + /** + * 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. + */ + et2_link.prototype.setDetachedAttributes = function (_nodes, _values) { + this.node = _nodes[0]; + this.label_span = jQuery(_nodes[0]); + this.link = jQuery(_nodes[1]); + if (typeof _values["id"] !== "undefined") + this.set_id(_values['id']); + if (typeof _values["label"] !== "undefined") + this.set_label(_values['label']); + if (typeof _values["value"] !== "undefined") + this.set_value(_values["value"]); + }; + et2_link._attributes = { + "only_app": { + "name": "Application", + "type": "string", + "default": "", + "description": "Use the given application, so you can pass just the ID for value" + }, + "value": { + description: "Array with keys app, id, and optionally title", + type: "any" + }, + "needed": { + "ignore": true + }, + "link_hook": { + "name": "Type", + "type": "string", + "default": "view", + "description": "Hook used for displaying link (view/edit/add)" + }, + "target_app": { + "name": "Target application", + "type": "string", + "default": "", + "description": "Optional parameter to be passed to egw().open in order to open links in specified application" + }, + "extra_link_target": { + "name": "Link target", + "type": "string", + "default": null, + "description": "Optional parameter to be passed to egw().open in order to open links in specified target eg. _blank" + } + }; + return et2_link; +}(et2_core_valueWidget_1.et2_valueWidget)); +exports.et2_link = et2_link; +et2_core_widget_1.et2_register_widget(et2_link, ["link", "link-entry_ro"]); /** * UI widget for one or more links, comma separated * - * @augments et2_valueWidget + * TODO: This one used to have expose */ -var et2_link_string = (function(){ "use strict"; return expose(et2_valueWidget.extend([et2_IDetachedDOM], -{ - attributes: { - "application": { - "name": "Application", - "type": "string", - "default": "", - "description": "Use the given application, so you can pass just the ID for value" - }, - "value": { - "description": "Either an array of link information (see egw_link::link()) or array with keys to_app and to_id", - "type": "any" - }, - "only_app": { - "name": "Application filter", - "type": "string", - "default": "", - "description": "Appname, eg. 'projectmananager' to list only linked projects" - }, - "link_type": { - "name": "Type filter", - "type": "string", - "default":"", - "description": "Sub-type key to list only entries of that type" - }, - "expose_view":{ - name: "Expose view", - type: "boolean", - default: true, - description: "Clicking on description with href value would popup an expose view, and will show content referenced by href." - } - }, - - /** - * Constructor - * - * @memberOf et2_link_string - */ - init: function() { - this._super.apply(this, arguments); - - this.list = jQuery(document.createElement("ul")) - .addClass("et2_link_string"); - - if(this.options['class']) this.list.addClass(this.options['class']); - this.setDOMNode(this.list[0]); - }, - - destroy: function() { - this._super.apply(this, arguments); - if (this.node != null) { - this.node.children().unbind(); - } - }, - - set_value: function(_value) { - // Get data - if(!_value || _value == null) - { - this.list.empty(); - return; - } - if(typeof _value == "string" && _value.indexOf(',') > 0) - { - _value = _value.split(','); - } - if(!_value.to_app && typeof _value == "object" && this.options.application) - { - _value.to_app = this.options.application; - } - - if(typeof _value == 'object' && _value.to_app && _value.to_id) - { - this.value = _value; - this._get_links(); - return; - } - this.list.empty(); - if(typeof _value == 'object' && _value.length > 0) { - // Have full info - // Don't store new value, just update display - - - // Make new links - for(var i = 0; i < _value.length; i++) - { - if(!this.options.only_app || this.options.only_app && _value[i].app == this.options.only_app) - { - this._add_link(_value[i].id ? _value[i] : {id:_value[i], app: _value.to_app}); - } - } - } - else if(this.options.application) - { - this._add_link({id:_value, app: this.options.application}); - } - }, - - _get_links: function() { - var _value = this.value; - // Just IDs - get from server - if(this.options.only_app) - { - _value.only_app = this.options.only_app; - } - this.egw().jsonq('EGroupware\\Api\\Etemplate\\Widget\\Link::ajax_link_list', [_value], this.set_value, this); - return; - }, - /** - * Function to get media content to feed the expose - * @param {type} _value - * @returns {Array|Array.getMedia.mediaContent} - */ - getMedia: function (_value) - { - var base_url = egw.webserverUrl.match(/^\//,'ig')?egw(window).window.location.origin + egw.webserverUrl : egw.webserverUrl; - var mediaContent = []; - if (_value && typeof _value.type !='undefined' && _value.type.match(/video\//,'ig')) - { - mediaContent = [{ - title: _value.id, - type: _value.type, - poster:'', // TODO: Should be changed by correct video thumbnail later - href: base_url + egw().mime_open(_value), - download_href: base_url + egw().mime_open(_value) + '?download' - }]; - } - else if(_value) - { - mediaContent = [{ - title: _value.id, - href: base_url + egw().mime_open(_value).url, - download_href: base_url + egw().mime_open(_value).url + '?download', - type: _value.type - }]; - } - if (mediaContent[0].href && mediaContent[0].href.match(/\/webdav.php/,'ig')) mediaContent[0]["download_href"] = mediaContent[0].href + '?download'; - return mediaContent; - }, - _add_link: function(_link_data) { - var self = this; - var link = jQuery(document.createElement("li")) - .appendTo(this.list) - .addClass("et2_link loading") - .click( function(e){ - var fe = egw_get_file_editor_prefered_mimes(_link_data.type); - if (self.options.expose_view && typeof _link_data.type !='undefined' - && _link_data.type.match(self.mime_regexp,'ig')) - { - self._init_blueimp_gallery(e, _link_data); - } - else if(typeof _link_data.type !='undefined' && fe && fe.mime && fe.mime[_link_data.type]) - { - egw.open_link(egw.link('/index.php', { - menuaction: fe.edit.menuaction, - path: egw().mime_open(_link_data).url.replace('/webdav.php','') - }), '', fe.edit_popup); - } - else - { - self.egw().open(_link_data, "", "view",null,_link_data.app,_link_data.app); - } - e.stopImmediatePropagation(); - }); - - if(_link_data.title) link.text(_link_data.title); - - // Now that link is created, get title from server & update - if(!_link_data.title) { - this.egw().link_title(_link_data.app, _link_data.id, function(title) { - if (title) - this.removeClass("loading").text(title); - else - this.remove(); // no rights or not found - }, link); - } - }, - - /** - * 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 an array of attributes - */ - getDetachedAttributes: function(_attrs) { - // Create the label container if it didn't exist yet - if (this._labelContainer == null) - { - this._labelContainer = jQuery(document.createElement("label")) - .addClass("et2_label"); - this.getSurroundings().insertDOMNode(this._labelContainer[0]); - this.getSurroundings().update(); - } - _attrs.push("value","label"); - }, - - /** - * 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() { - // Create the label container if it didn't exist yet - if (this._labelContainer == null) - { - this._labelContainer = jQuery(document.createElement("label")) - .addClass("et2_label"); - this.getSurroundings().insertDOMNode(this._labelContainer[0]); - } - return [this.list[0], this._labelContainer[0]]; - }, - - /** - * 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) { - this.list = jQuery(_nodes[0]); - - this.set_value(_values["value"]); - - // Special detached, to prevent DOM node modification of the normal method - this._labelContainer = _nodes.length > 1 ? jQuery(_nodes[1]) : null; - if(_values['label']) - { - this.set_label(_values['label']); - } - else if (this._labelContainer) - { - this._labelContainer.contents().not(this.list).remove(); - } - } -}));}).call(this); -et2_register_widget(et2_link_string, ["link-string"]); - +var et2_link_string = /** @class */ (function (_super) { + __extends(et2_link_string, _super); + function et2_link_string() { + return _super !== null && _super.apply(this, arguments) || this; + } + return et2_link_string; +}(expose((_a = /** @class */ (function (_super) { + __extends(et2_link_string, _super); + /** + * Constructor + * + * @memberOf et2_link_string + */ + function et2_link_string(_parent, _attrs, _child) { + var _this = _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_link_string._attributes, _child || {})) || this; + _this.list = jQuery(document.createElement("ul")) + .addClass("et2_link_string"); + if (_this.options['class']) + _this.list.addClass(_this.options['class']); + _this.setDOMNode(_this.list[0]); + return _this; + } + et2_link_string.prototype.destroy = function () { + _super.prototype.destroy.apply(this, arguments); + if (this.node != null) { + jQuery(this.node).children().unbind(); + } + }; + et2_link_string.prototype.set_value = function (_value) { + // Get data + if (!_value || _value == null || !this.list) { + // List can be missing if the AJAX call returns after the form is destroyed + if (this.list) { + this.list.empty(); + } + return; + } + if (typeof _value == "string" && _value.indexOf(',') > 0) { + _value = _value.split(','); + } + if (!_value.to_app && typeof _value == "object" && this.options.application) { + _value.to_app = this.options.application; + } + if (typeof _value == 'object' && _value.to_app && _value.to_id) { + this.value = _value; + this._get_links(); + return; + } + this.list.empty(); + if (typeof _value == 'object' && _value.length > 0) { + // Have full info + // Don't store new value, just update display + // Make new links + for (var i = 0; i < _value.length; i++) { + if (!this.options.only_app || this.options.only_app && _value[i].app == this.options.only_app) { + this._add_link(_value[i].id ? _value[i] : { id: _value[i], app: _value.to_app }); + } + } + } + else if (this.options.application) { + this._add_link({ id: _value, app: this.options.application }); + } + }; + et2_link_string.prototype._get_links = function () { + var _value = this.value; + // Just IDs - get from server + if (this.options.only_app) { + _value.only_app = this.options.only_app; + } + this.egw().jsonq('EGroupware\\Api\\Etemplate\\Widget\\Link::ajax_link_list', [_value], this.set_value, this); + return; + }; + /** + * Function to get media content to feed the expose + * @param {type} _value + * @returns {Array|Array.getMedia.mediaContent} + */ + et2_link_string.prototype.getMedia = function (_value) { + var base_url = egw.webserverUrl.match(/^\//, 'ig') ? egw(window).window.location.origin + egw.webserverUrl : egw.webserverUrl; + var mediaContent = []; + if (_value && typeof _value.type != 'undefined' && _value.type.match(/video\//, 'ig')) { + mediaContent = [{ + title: _value.id, + type: _value.type, + poster: '', + href: base_url + egw().mime_open(_value), + download_href: base_url + egw().mime_open(_value) + '?download' + }]; + } + else if (_value) { + mediaContent = [{ + title: _value.id, + href: base_url + egw().mime_open(_value).url, + download_href: base_url + egw().mime_open(_value).url + '?download', + type: _value.type + }]; + } + if (mediaContent[0].href && mediaContent[0].href.match(/\/webdav.php/, 'ig')) + mediaContent[0]["download_href"] = mediaContent[0].href + '?download'; + return mediaContent; + }; + et2_link_string.prototype._add_link = function (_link_data) { + var self = this; + var link = jQuery(document.createElement("li")) + .appendTo(this.list) + .addClass("et2_link loading") + .click(function (e) { + var fe = egw_get_file_editor_prefered_mimes(_link_data.type); + if (self.options.expose_view && typeof _link_data.type != 'undefined' + && _link_data.type.match(self.mime_regexp, 'ig')) { + self._init_blueimp_gallery(e, _link_data); + } + else if (typeof _link_data.type != 'undefined' && fe && fe.mime && fe.mime[_link_data.type]) { + egw.open_link(egw.link('/index.php', { + menuaction: fe.edit.menuaction, + path: egw().mime_open(_link_data).url.replace('/webdav.php', '') + }), '', fe.edit_popup); + } + else { + self.egw().open(_link_data, "", "view", null, _link_data.app, _link_data.app); + } + e.stopImmediatePropagation(); + }); + if (_link_data.title) + link.text(_link_data.title); + // Now that link is created, get title from server & update + if (!_link_data.title) { + this.egw().link_title(_link_data.app, _link_data.id, function (title) { + if (title) + this.removeClass("loading").text(title); + else + this.remove(); // no rights or not found + }, link); + } + }; + /** + * 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 an array of attributes + */ + et2_link_string.prototype.getDetachedAttributes = function (_attrs) { + // Create the label container if it didn't exist yet + if (this._labelContainer == null) { + this._labelContainer = jQuery(document.createElement("label")) + .addClass("et2_label"); + this.getSurroundings().insertDOMNode(this._labelContainer[0]); + this.getSurroundings().update(); + } + _attrs.push("value", "label"); + }; + /** + * Returns an array of DOM nodes. The (relatively) same DOM-Nodes have to be + * passed to the "setDetachedAttributes" function in the same order. + */ + et2_link_string.prototype.getDetachedNodes = function () { + // Create the label container if it didn't exist yet + if (this._labelContainer == null) { + this._labelContainer = jQuery(document.createElement("label")) + .addClass("et2_label"); + this.getSurroundings().insertDOMNode(this._labelContainer[0]); + } + return [this.list[0], this._labelContainer[0]]; + }; + /** + * 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. + */ + et2_link_string.prototype.setDetachedAttributes = function (_nodes, _values) { + this.list = jQuery(_nodes[0]); + this.set_value(_values["value"]); + // Special detached, to prevent DOM node modification of the normal method + this._labelContainer = _nodes.length > 1 ? jQuery(_nodes[1]) : null; + if (_values['label']) { + this.set_label(_values['label']); + } + else if (this._labelContainer) { + this._labelContainer.contents().not(this.list).remove(); + } + }; + return et2_link_string; + }(et2_core_valueWidget_1.et2_valueWidget)), + _a._attributes = { + "application": { + "name": "Application", + "type": "string", + "default": "", + "description": "Use the given application, so you can pass just the ID for value" + }, + "value": { + "description": "Either an array of link information (see egw_link::link()) or array with keys to_app and to_id", + "type": "any" + }, + "only_app": { + "name": "Application filter", + "type": "string", + "default": "", + "description": "Appname, eg. 'projectmananager' to list only linked projects" + }, + "link_type": { + "name": "Type filter", + "type": "string", + "default": "", + "description": "Sub-type key to list only entries of that type" + }, + "expose_view": { + name: "Expose view", + type: "boolean", + default: true, + description: "Clicking on description with href value would popup an expose view, and will show content referenced by href." + } + }, + _a)))); +exports.et2_link_string = et2_link_string; +; +et2_core_widget_1.et2_register_widget(et2_link_string, ["link-string"]); /** * UI widget for one or more links in a list (table) - * - * @augments et2_link_string */ -var et2_link_list = (function(){ "use strict"; return et2_link_string.extend( -{ - attributes: { - "show_deleted": { - "name": "Show deleted", - "type": "boolean", - "default": false, - "description": "Show links that are marked as deleted, being held for purge" - }, - "onchange": { - "name": "onchange", - "type": "js", - "default": et2_no_init, - "description": "JS code which is executed when the links change." - }, - readonly: { - name: "readonly", - type: "boolean", - "default": false, - description: "Does NOT allow user to enter data, just displays existing data" - }, - "target_app": { - "name": "Target application", - "type": "string", - "default": "", - "description": "Optional parameter to be passed to egw().open in order to open links in specified application " - } - }, - - /** - * Constructor - * - * @memberOf et2_link_list - */ - init: function() { - this._super.apply(this, arguments); - - this.list = jQuery(document.createElement("table")) - .addClass("et2_link_list"); - if(this.options['class']) this.list.addClass(this.options['class']); - this.setDOMNode(this.list[0]); - - // Set up context menu - var self = this; - this.context = new egwMenu(); - this.context.addItem("comment", this.egw().lang("Comment"), "", function() { - var link_id = typeof self.context.data.link_id == 'number' ? self.context.data.link_id : self.context.data.link_id.replace(/[:\.]/g,'_'); - - et2_dialog.show_prompt( - function(button, comment) { - if(button != et2_dialog.OK_BUTTON) return; - var remark = jQuery('#link_'+(self.context.data.dom_id ? self.context.data.dom_id : link_id), self.list).children('.remark'); - if(isNaN(self.context.data.link_id)) // new entry, not yet stored - { - remark.text(comment); - // Look for a link-to with the same ID, refresh it - if(self.context.data.link_id) - { - var _widget = link_id.widget || null; - self.getRoot().iterateOver( - function(widget) { - if(widget.id == self.id) { - _widget = widget; - } - }, - self, et2_link_to - ); - var value = _widget != null ? _widget.getValue() : false; - if(_widget && value && value.to_id) - { - value.to_id[self.context.data.link_id].remark = comment; - } - } - return; - } - remark.addClass("loading"); - var request = egw.json("EGroupware\\Api\\Etemplate\\Widget\\Link::ajax_link_comment", - [link_id, comment], - function() { - if(remark) - { - // Append "" to make sure it's a string, not undefined - remark.removeClass("loading").text(comment+""); - // Update internal data - self.context.data.remark = comment+""; - } - }, - this,true - ).sendRequest(); - }, - '',self.egw().lang("Comment"),self.context.data.remark||'' - ); - - }); - this.context.addItem("file_info", this.egw().lang("File information"), this.egw().image("edit"), function(menu_item) { - var link_data = self.context.data; - if(link_data.app == 'file') - { - // File info is always the same - var url = '/apps/'+link_data.app2+'/'+link_data.id2+'/'+decodeURIComponent(link_data.id); - if(typeof url == 'string' && url.indexOf('webdav.php')) - { - // URL is url to file in webdav, so get rid of that part - url = url.replace('/webdav.php', ''); - } - else if (typeof url == 'object' && url.path) - { - url = url.path; - } - self.egw().open(url, "filemanager", "edit"); - } - }); - this.context.addItem("-", "-"); - this.context.addItem("save", this.egw().lang("Save as"), this.egw().image('save'), function(menu_item) { - var link_data = self.context.data; - // Download file - if(link_data.download_url) - { - var url = link_data.download_url; - if (url[0] == '/') url = egw.link(url); - - var a = document.createElement('a'); - if(typeof a.download == "undefined") - { - window.location = url+"?download"; - return false; - } - - // Multiple file download for those that support it - a = jQuery(a) - .prop('href', url) - .prop('download', link_data.title || "") - .appendTo(self.getInstanceManager().DOMContainer); - - var evt = document.createEvent('MouseEvent'); - evt.initMouseEvent('click', true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 0, null); - a[0].dispatchEvent(evt); - a.remove(); - return false; - } - - self.egw().open(link_data, "", "view",'download',link_data.target ? link_data.target : link_data.app,link_data.app); - }); - this.context.addItem("zip", this.egw().lang("Save as Zip"), this.egw().image('save_zip'), function(menu_item) { - // Highlight files for nice UI indicating what will be in the zip. - // Files have negative IDs. - jQuery('[id^="link_-"]',this.list).effect('highlight',{},2000); - - // Download ZIP - window.location = self.egw().link('/index.php',{ - menuaction: 'api.EGroupware\\Api\\Etemplate\\Widget\\Link.download_zip', - app: self.value.to_app, - id: self.value.to_id - }); - }); - this.context.addItem("-", "-"); - this.context.addItem("delete", this.egw().lang("Delete link"), this.egw().image("delete"), function(menu_item) { - var link_id = isNaN(self.context.data.link_id) ? self.context.data : self.context.data.link_id; - var row = jQuery('#link_'+(self.context.data.dom_id ? self.context.data.dom_id : self.context.data.link_id), self.list); - et2_dialog.show_dialog( - function(button) { if(button == et2_dialog.YES_BUTTON) self._delete_link(link_id,row);}, - egw.lang('Delete link?') - ); - }); - - // Native DnD - Doesn't play nice with jQueryUI Sortable - // Tell jQuery to include this property - jQuery.event.props.push('dataTransfer'); - - }, - - destroy: function() { - - this._super.apply(this, arguments); - if(this.context) - { - this.context.clear(); - delete this.context; - } - }, - - set_value: function(_value) - { - this.list.empty(); - // Handle server passed a list of links that aren't ready yet - if(_value && typeof _value == "object") - { - var list = []; - if(_value.to_id && typeof _value.to_id == "object") - { - list = _value.to_id; - } - else if (_value.length) - { - list = _value; - } - if(list.length > 0) - { - for(var id in list) - { - var link = list[id]; - if(link.app) - { - // Temp IDs can cause problems since the ID includes the file name or : - if(link.link_id && typeof link.link_id != 'number') - { - link.dom_id = 'temp_'+egw.uid(); - } - // Icon should be in registry - if(!link.icon) - { - link.icon = egw.link_get_registry(link.app,'icon'); - // No icon, try by mime type - different place for un-saved entries - if(link.icon == false && link.id.type) - { - // Triggers icon by mime type, not thumbnail or app - link.type = link.id.type; - link.icon = true; - } - } - // Special handling for file - if not existing, we can't ask for title - if(typeof link.id =='object' && !link.title) - { - link.title = link.id.name || ''; - } - this._add_link(link); - } - } - } - else - { - this._super.apply(this,arguments); - } - } - }, - - _add_link: function(_link_data) { - var row = jQuery(document.createElement("tr")) - .attr("id", "link_"+(_link_data.dom_id ? _link_data.dom_id : (typeof _link_data.link_id == "string" ? _link_data.link_id.replace(/[:\.]/g,'_'):_link_data.link_id ||_link_data.id))) - .attr("draggable", _link_data.app == 'file' ? "true" : "") - .appendTo(this.list); - if(!_link_data.link_id) - { - for(var k in _link_data) - { - row[0].dataset[k] = _link_data[k]; - } - } - - // Icon - var icon = jQuery(document.createElement("td")) - .appendTo(row) - .addClass("icon"); - if(_link_data.icon) - { - var icon_widget = et2_createWidget("image"); - var src = ''; - // Creat a mime widget if the link has type - if(_link_data.type) - { - // VFS - file - var vfs_widget = et2_createWidget('vfs-mime'); - vfs_widget.set_value({ - download_url:_link_data.download_url, - name:_link_data.title, - mime:_link_data.type, - path:_link_data.icon - }); - icon.append(vfs_widget.getDOMNode()); - } - else - { - src = this.egw().image(_link_data.icon); - if(src) icon_widget.set_src(src); - icon.append(icon_widget.getDOMNode()); - } - } - - var columns = ['title','remark']; - - var self = this; - for(var i = 0; i < columns.length; i++) { - var $td = jQuery(document.createElement("td")) - .appendTo(row) - .addClass(columns[i]) - .text(_link_data[columns[i]] ? _link_data[columns[i]]+"" : ""); - - var dirs = _link_data[columns[i]] ? _link_data[columns[i]].split('/') : []; - if(columns[i] == 'title' && _link_data.type && dirs.length > 1) - { - this._format_vfs($td, dirs, _link_data); - } - //Bind the click handler if there is download_url - if (_link_data && (typeof _link_data.download_url != 'undefined' || _link_data.app !='egw-data')) - { - $td.click( function(){ - var fe_mime = egw_get_file_editor_prefered_mimes(_link_data.type); - // Check if the link entry is mime with media type, in order to open it in expose view - if (typeof _link_data.type != 'undefined' && - (_link_data.type.match(self.mime_regexp,'ig') || (fe_mime && fe_mime.mime[_link_data.type]))) - { - var $vfs_img_node = jQuery(this).parent().find('.vfsMimeIcon'); - if ($vfs_img_node.length > 0) $vfs_img_node.click(); - } - else - { - if( !self.options.target_app ){ - self.options.target_app = _link_data.app; - } - self.egw().open(_link_data, "", "view",null,_link_data.target ? _link_data.target : _link_data.app,self.options.target_app); - } - }); - } - } - - if (typeof _link_data.title == 'undefined') - { - // Title will be fetched from server and then set - jQuery('td.title',row).addClass("loading"); - var title = this.egw().link_title(_link_data.app, _link_data.id, function(title) { - jQuery('td.title',this).removeClass("loading").text(title+""); - }, row); - } - // Date - /* - var date_row = jQuery(document.createElement("td")) - .appendTo(row); - if(_link_data.lastmod) - { - var date_widget = et2_createWidget("date-since"); - date_widget.set_value(_link_data.lastmod); - date_row.append(date_widget.getDOMNode()); - } - */ - - // Delete - // build delete button if the link is not readonly - if (!this.options.readonly) - { - var delete_button = jQuery(document.createElement("td")) - .appendTo(row); - jQuery("
          ") - .appendTo(delete_button) - // We don't use ui-icon because it assigns a bg image - .addClass("delete icon") - .bind( 'click', function() { - et2_dialog.show_dialog( - function(button) { - if(button == et2_dialog.YES_BUTTON) - { - self._delete_link( - self.value && typeof self.value.to_id != 'object' && _link_data.link_id ? _link_data.link_id:_link_data, - row - ); - } - }, - egw.lang('Delete link?') - ); - }); - } - // Context menu - row.bind("contextmenu", function(e) { - // Comment only available if link_id is there and not readonly - self.context.getItem("comment").set_enabled(typeof _link_data.link_id != 'undefined' && !self.options.readonly); - // File info only available for existing files - self.context.getItem("file_info").set_enabled(typeof _link_data.id != 'object' && _link_data.app == 'file'); - self.context.getItem("save").set_enabled(typeof _link_data.id != 'object' && _link_data.app == 'file'); - // Zip download only offered if there are at least 2 files - self.context.getItem("zip").set_enabled(jQuery('[id^="link_-"]',this.list).length >= 2); - // Show delete item only if the widget is not readonly - self.context.getItem("delete").set_enabled(!self.options.readonly); - - self.context.data = _link_data; - self.context.showAt(e.pageX, e.pageY, true); - e.preventDefault(); - }); - - - // Drag - adapted from egw_action_dragdrop, sidestepping action system - // so all linked files get it - // // Unfortunately, dragging files is currently only supported by Chrome - if(navigator && navigator.userAgent.indexOf('Chrome') >= 0) - { - row.on("dragstart", _link_data, function(event) { - if(event.dataTransfer == null) { - return; - } - var data = event.data || {}; - if(data && data.type && data.download_url) - { - event.dataTransfer.dropEffect="copy"; - event.dataTransfer.effectAllowed="copy"; - - var url = data.download_url; - - // NEED an absolute URL - if (url[0] == '/') url = egw.link(url); - // egw.link adds the webserver, but that might not be an absolute URL - try again - if (url[0] == '/') url = window.location.origin+url; - - // Unfortunately, dragging files is currently only supported by Chrome - if(navigator && navigator.userAgent.indexOf('Chrome')) - { - event.dataTransfer.setData("DownloadURL", data.type+':'+data.title+':'+url); - } - - // Include URL as a fallback - event.dataTransfer.setData("text/uri-list", url); - } - - if(event.dataTransfer.types.length == 0) - { - // No file data? Abort: drag does nothing - event.preventDefault(); - return; - } - //event.dataTransfer.setDragImage(event.delegate.target,0,0); - var div = jQuery(document.createElement("div")) - .attr('id', 'drag_helper') - .css({ - position: 'absolute', - top: '0px', - left: '0px', - width: '300px' - }); - div.append(event.target.cloneNode(true)); - - self.list.append(div); - - event.dataTransfer.setDragImage(div.get(0),0,0); - }) - .on('drag', function() { - jQuery('#drag_helper',self.list).remove(); - }); - } - }, - _delete_link: function(link_id, row) { - if(row) - { - var delete_button = jQuery('.delete',row); - delete_button.removeClass("delete").addClass("loading"); - row.off(); - } - if(this.onchange) - { - this.onchange(this,link_id,row); - } - if(typeof link_id != "object") - { - egw.json("EGroupware\\Api\\Etemplate\\Widget\\Link::ajax_delete", [link_id], - function(data) { if(data) {row.slideUp(row.remove);}} - ).sendRequest(); - } - else if (row) - { - // No link ID means a link on an unsaved entry. - // Just remove the row, but need to adjust the link_to value also - row.slideUp(row.remove); - - // Look for a link-to with the same ID, refresh it - if(link_id.link_id) - { - var self = this; - var _widget = link_id.widget || null; - this.getRoot().iterateOver( - function(widget) { - if(widget.id == self.id) { - _widget = widget; - } - }, - this, et2_link_to - ); - var value = _widget != null ? _widget.getValue() : false; - if(_widget && value && value.to_id) - { - delete value.to_id[link_id.link_id]; - _widget.set_value(value); - } - } - } - }, - - /** - * When the link is to a VFS file, we do some special formatting. - * - * Instead of listing the full path, we use - * Path: - filename - * When multiple files from the same directory are linked, we exclude - * the directory name from all but the first link to that directory - * - * @param {JQuery} $td Current table data cell for the title - * @param {String[]} dirs List of directories in the linked file's path - * @param {String[]} _link_data Data for the egw_link - * @returns {undefined} - */ - _format_vfs: function($td, dirs, _link_data) - { - // Keep it here for matching next row - $td.attr('data-title', _link_data['title']); - - // VFS link - check for same dir as above, and hide dir - var reformat = false; - var span_size = 1; - var prev = jQuery('td.title',$td.parent().prev('tr')); - if(prev.length === 1) - { - var prev_dirs = (prev.attr('data-title') || '').split('/'); - if(prev_dirs.length > 1 && prev_dirs.length == dirs.length) - { - for(var i = 0; i < dirs.length; i++) - { - // Current is same as prev, blank it - if(dirs[i] === prev_dirs[i]) - { - reformat = true; - span_size += dirs[i].length+1; - dirs[i] = ''; - } - else - { - break; - } - } - } - } - var filename = dirs.pop(); - if(reformat && (dirs.length - i) === 0) - { - $td.html(' - '+filename); - } - else - { - // Different format for directory - span_size += dirs.join('/').length+1; - $td.html(''+dirs.join('/')+': - ' + filename); - } - } -});}).call(this); -et2_register_widget(et2_link_list, ["link-list"]); - - +var et2_link_list = /** @class */ (function (_super) { + __extends(et2_link_list, _super); + /** + * Constructor + * + */ + function et2_link_list(_parent, _attrs, _child) { + var _this = _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_link_list._attributes, _child || {})) || this; + _this.list = jQuery(document.createElement("table")) + .addClass("et2_link_list"); + if (_this.options['class']) + _this.list.addClass(_this.options['class']); + _this.setDOMNode(_this.list[0]); + // Set up context menu + var self = _this; + _this.context = new egwMenu(); + _this.context.addItem("comment", _this.egw().lang("Comment"), "", function () { + var link_id = typeof self.context.data.link_id == 'number' ? self.context.data.link_id : self.context.data.link_id.replace(/[:\.]/g, '_'); + et2_dialog.show_prompt(function (button, comment) { + if (button != et2_dialog.OK_BUTTON) + return; + var remark = jQuery('#link_' + (self.context.data.dom_id ? self.context.data.dom_id : link_id), self.list).children('.remark'); + if (isNaN(self.context.data.link_id)) // new entry, not yet stored + { + remark.text(comment); + // Look for a link-to with the same ID, refresh it + if (self.context.data.link_id) { + var _widget = link_id.widget || null; + self.getRoot().iterateOver(function (widget) { + if (widget.id == self.id) { + _widget = widget; + } + }, self, et2_link_to); + var value = _widget != null ? _widget.getValue() : false; + if (_widget && value && value.to_id) { + value.to_id[self.context.data.link_id].remark = comment; + } + } + return; + } + remark.addClass("loading"); + var request = egw.json("EGroupware\\Api\\Etemplate\\Widget\\Link::ajax_link_comment", [link_id, comment], function () { + if (remark) { + // Append "" to make sure it's a string, not undefined + remark.removeClass("loading").text(comment + ""); + // Update internal data + self.context.data.remark = comment + ""; + } + }, this, true).sendRequest(); + }, '', self.egw().lang("Comment"), self.context.data.remark || ''); + }); + _this.context.addItem("file_info", _this.egw().lang("File information"), _this.egw().image("edit"), function (menu_item) { + var link_data = self.context.data; + if (link_data.app == 'file') { + // File info is always the same + var url = '/apps/' + link_data.app2 + '/' + link_data.id2 + '/' + decodeURIComponent(link_data.id); + if (typeof url == 'string' && url.indexOf('webdav.php')) { + // URL is url to file in webdav, so get rid of that part + url = url.replace('/webdav.php', ''); + } + self.egw().open(url, "filemanager", "edit"); + } + }); + _this.context.addItem("-", "-"); + _this.context.addItem("save", _this.egw().lang("Save as"), _this.egw().image('save'), function (menu_item) { + var link_data = self.context.data; + // Download file + if (link_data.download_url) { + var url = link_data.download_url; + if (url[0] == '/') + url = egw.link(url); + var a = document.createElement('a'); + if (typeof a.download == "undefined") { + window.location.href = url + "?download"; + return false; + } + // Multiple file download for those that support it + a = jQuery(a) + .prop('href', url) + .prop('download', link_data.title || "") + .appendTo(self.getInstanceManager().DOMContainer); + var evt = document.createEvent('MouseEvent'); + evt.initMouseEvent('click', true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 0, null); + a[0].dispatchEvent(evt); + a.remove(); + return false; + } + self.egw().open(link_data, "", "view", 'download', link_data.target ? link_data.target : link_data.app, link_data.app); + }); + _this.context.addItem("zip", _this.egw().lang("Save as Zip"), _this.egw().image('save_zip'), function (menu_item) { + // Highlight files for nice UI indicating what will be in the zip. + // Files have negative IDs. + jQuery('[id^="link_-"]', this.list).effect('highlight', {}, 2000); + // Download ZIP + window.location = self.egw().link('/index.php', { + menuaction: 'api.EGroupware\\Api\\Etemplate\\Widget\\Link.download_zip', + app: self.value.to_app, + id: self.value.to_id + }); + }); + _this.context.addItem("-", "-"); + _this.context.addItem("delete", _this.egw().lang("Delete link"), _this.egw().image("delete"), function (menu_item) { + var link_id = isNaN(self.context.data.link_id) ? self.context.data : self.context.data.link_id; + var row = jQuery('#link_' + (self.context.data.dom_id ? self.context.data.dom_id : self.context.data.link_id), self.list); + et2_dialog.show_dialog(function (button) { if (button == et2_dialog.YES_BUTTON) + self._delete_link(link_id, row); }, egw.lang('Delete link?')); + }); + // Native DnD - Doesn't play nice with jQueryUI Sortable + // Tell jQuery to include this property + jQuery.event.props.push('dataTransfer'); + return _this; + } + et2_link_list.prototype.destroy = function () { + _super.prototype.destroy.apply(this, arguments); + if (this.context) { + this.context.clear(); + delete this.context; + } + }; + et2_link_list.prototype.set_value = function (_value) { + this.list.empty(); + // Handle server passed a list of links that aren't ready yet + if (_value && typeof _value == "object") { + var list = []; + if (_value.to_id && typeof _value.to_id == "object") { + list = _value.to_id; + } + else if (_value.length) { + list = _value; + } + if (list.length > 0) { + for (var id in list) { + var link = list[id]; + if (link.app) { + // Temp IDs can cause problems since the ID includes the file name or : + if (link.link_id && typeof link.link_id != 'number') { + link.dom_id = 'temp_' + egw.uid(); + } + // Icon should be in registry + if (!link.icon) { + link.icon = egw.link_get_registry(link.app, 'icon'); + // No icon, try by mime type - different place for un-saved entries + if (link.icon == false && link.id.type) { + // Triggers icon by mime type, not thumbnail or app + link.type = link.id.type; + link.icon = true; + } + } + // Special handling for file - if not existing, we can't ask for title + if (typeof link.id == 'object' && !link.title) { + link.title = link.id.name || ''; + } + this._add_link(link); + } + } + } + else { + _super.prototype.set_value.call(this, _value); + } + } + }; + et2_link_list.prototype._add_link = function (_link_data) { + var row = jQuery(document.createElement("tr")) + .attr("id", "link_" + (_link_data.dom_id ? _link_data.dom_id : (typeof _link_data.link_id == "string" ? _link_data.link_id.replace(/[:\.]/g, '_') : _link_data.link_id || _link_data.id))) + .attr("draggable", _link_data.app == 'file' ? "true" : "") + .appendTo(this.list); + if (!_link_data.link_id) { + for (var k in _link_data) { + row[0].dataset[k] = _link_data[k]; + } + } + // Icon + var icon = jQuery(document.createElement("td")) + .appendTo(row) + .addClass("icon"); + if (_link_data.icon) { + var icon_widget = et2_createWidget("image", {}); + var src = ''; + // Creat a mime widget if the link has type + if (_link_data.type) { + // VFS - file + var vfs_widget = et2_createWidget('vfs-mime', {}); + vfs_widget.set_value({ + download_url: _link_data.download_url, + name: _link_data.title, + mime: _link_data.type, + path: _link_data.icon + }); + icon.append(vfs_widget.getDOMNode()); + } + else { + src = this.egw().image(_link_data.icon); + if (src) + icon_widget.set_src(src); + icon.append(icon_widget.getDOMNode()); + } + } + var columns = ['title', 'remark']; + var self = this; + for (var i = 0; i < columns.length; i++) { + var $td = jQuery(document.createElement("td")) + .appendTo(row) + .addClass(columns[i]) + .text(_link_data[columns[i]] ? _link_data[columns[i]] + "" : ""); + var dirs = _link_data[columns[i]] ? _link_data[columns[i]].split('/') : []; + if (columns[i] == 'title' && _link_data.type && dirs.length > 1) { + this._format_vfs($td, dirs, _link_data); + } + //Bind the click handler if there is download_url + if (_link_data && (typeof _link_data.download_url != 'undefined' || _link_data.app != 'egw-data')) { + $td.click(function () { + var fe_mime = egw_get_file_editor_prefered_mimes(_link_data.type); + // Check if the link entry is mime with media type, in order to open it in expose view + if (typeof _link_data.type != 'undefined' && + (_link_data.type.match(self.mime_regexp, 'ig') || (fe_mime && fe_mime.mime[_link_data.type]))) { + var $vfs_img_node = jQuery(this).parent().find('.vfsMimeIcon'); + if ($vfs_img_node.length > 0) + $vfs_img_node.click(); + } + else { + if (!self.options.target_app) { + self.options.target_app = _link_data.app; + } + self.egw().open(_link_data, "", "view", null, _link_data.target ? _link_data.target : _link_data.app, self.options.target_app); + } + }); + } + } + if (typeof _link_data.title == 'undefined') { + // Title will be fetched from server and then set + jQuery('td.title', row).addClass("loading"); + var title = this.egw().link_title(_link_data.app, _link_data.id, function (title) { + jQuery('td.title', this).removeClass("loading").text(title + ""); + }, row); + } + // Date + /* + var date_row = jQuery(document.createElement("td")) + .appendTo(row); + if(_link_data.lastmod) + { + var date_widget = et2_createWidget("date-since"); + date_widget.set_value(_link_data.lastmod); + date_row.append(date_widget.getDOMNode()); + } + */ + // Delete + // build delete button if the link is not readonly + if (!this.options.readonly) { + var delete_button = jQuery(document.createElement("td")) + .appendTo(row); + jQuery("
          ") + .appendTo(delete_button) + // We don't use ui-icon because it assigns a bg image + .addClass("delete icon") + .bind('click', function () { + et2_dialog.show_dialog(function (button) { + if (button == et2_dialog.YES_BUTTON) { + self._delete_link(self.value && typeof self.value.to_id != 'object' && _link_data.link_id ? _link_data.link_id : _link_data, row); + } + }, egw.lang('Delete link?')); + }); + } + // Context menu + row.bind("contextmenu", function (e) { + // Comment only available if link_id is there and not readonly + self.context.getItem("comment").set_enabled(typeof _link_data.link_id != 'undefined' && !self.options.readonly); + // File info only available for existing files + self.context.getItem("file_info").set_enabled(typeof _link_data.id != 'object' && _link_data.app == 'file'); + self.context.getItem("save").set_enabled(typeof _link_data.id != 'object' && _link_data.app == 'file'); + // Zip download only offered if there are at least 2 files + self.context.getItem("zip").set_enabled(jQuery('[id^="link_-"]', this.list).length >= 2); + // Show delete item only if the widget is not readonly + self.context.getItem("delete").set_enabled(!self.options.readonly); + self.context.data = _link_data; + self.context.showAt(e.pageX, e.pageY, true); + e.preventDefault(); + }); + // Drag - adapted from egw_action_dragdrop, sidestepping action system + // so all linked files get it + // // Unfortunately, dragging files is currently only supported by Chrome + if (navigator && navigator.userAgent.indexOf('Chrome') >= 0) { + row.on("dragstart", _link_data, function (event) { + if (event.dataTransfer == null) { + return; + } + var data = event.data || {}; + if (data && data.type && data.download_url) { + event.dataTransfer.dropEffect = "copy"; + event.dataTransfer.effectAllowed = "copy"; + var url = data.download_url; + // NEED an absolute URL + if (url[0] == '/') + url = egw.link(url); + // egw.link adds the webserver, but that might not be an absolute URL - try again + if (url[0] == '/') + url = window.location.origin + url; + // Unfortunately, dragging files is currently only supported by Chrome + if (navigator && navigator.userAgent.indexOf('Chrome')) { + event.dataTransfer.setData("DownloadURL", data.type + ':' + data.title + ':' + url); + } + // Include URL as a fallback + event.dataTransfer.setData("text/uri-list", url); + } + if (event.dataTransfer.types.length == 0) { + // No file data? Abort: drag does nothing + event.preventDefault(); + return; + } + //event.dataTransfer.setDragImage(event.delegate.target,0,0); + var div = jQuery(document.createElement("div")) + .attr('id', 'drag_helper') + .css({ + position: 'absolute', + top: '0px', + left: '0px', + width: '300px' + }); + div.append(event.target.cloneNode(true)); + self.list.append(div); + event.dataTransfer.setDragImage(div.get(0), 0, 0); + }) + .on('drag', function () { + jQuery('#drag_helper', self.list).remove(); + }); + } + }; + et2_link_list.prototype._delete_link = function (link_id, row) { + if (row) { + var delete_button = jQuery('.delete', row); + delete_button.removeClass("delete").addClass("loading"); + row.off(); + } + if (this.onchange) { + this.onchange(this, link_id, row); + } + if (typeof link_id != "object") { + egw.json("EGroupware\\Api\\Etemplate\\Widget\\Link::ajax_delete", [link_id], function (data) { if (data) { + row.slideUp(row.remove); + } }).sendRequest(); + } + else if (row) { + // No link ID means a link on an unsaved entry. + // Just remove the row, but need to adjust the link_to value also + row.slideUp(row.remove); + // Look for a link-to with the same ID, refresh it + if (link_id.link_id) { + var self = this; + var _widget = link_id.widget || null; + this.getRoot().iterateOver(function (widget) { + if (widget.id == self.id) { + _widget = widget; + } + }, this, et2_link_to); + var value = _widget != null ? _widget.getValue() : false; + if (_widget && value && value.to_id) { + delete value.to_id[link_id.link_id]; + _widget.set_value(value); + } + } + } + }; + /** + * When the link is to a VFS file, we do some special formatting. + * + * Instead of listing the full path, we use + * Path: - filename + * When multiple files from the same directory are linked, we exclude + * the directory name from all but the first link to that directory + * + * @param {JQuery} $td Current table data cell for the title + * @param {String[]} dirs List of directories in the linked file's path + * @param {String[]} _link_data Data for the egw_link + * @returns {undefined} + */ + et2_link_list.prototype._format_vfs = function ($td, dirs, _link_data) { + // Keep it here for matching next row + $td.attr('data-title', _link_data['title']); + // VFS link - check for same dir as above, and hide dir + var reformat = false; + var span_size = 1; + var prev = jQuery('td.title', $td.parent().prev('tr')); + if (prev.length === 1) { + var prev_dirs = (prev.attr('data-title') || '').split('/'); + if (prev_dirs.length > 1 && prev_dirs.length == dirs.length) { + for (var i = 0; i < dirs.length; i++) { + // Current is same as prev, blank it + if (dirs[i] === prev_dirs[i]) { + reformat = true; + span_size += dirs[i].length + 1; + dirs[i] = ''; + } + else { + break; + } + } + } + } + var filename = dirs.pop(); + if (reformat && (dirs.length - i) === 0) { + $td.html(' - ' + filename); + } + else { + // Different format for directory + span_size += dirs.join('/').length + 1; + $td.html('' + dirs.join('/') + ': - ' + filename); + } + }; + et2_link_list._attributes = { + "show_deleted": { + "name": "Show deleted", + "type": "boolean", + "default": false, + "description": "Show links that are marked as deleted, being held for purge" + }, + "onchange": { + "name": "onchange", + "type": "js", + "default": et2_no_init, + "description": "JS code which is executed when the links change." + }, + readonly: { + name: "readonly", + type: "boolean", + "default": false, + description: "Does NOT allow user to enter data, just displays existing data" + }, + "target_app": { + "name": "Target application", + "type": "string", + "default": "", + "description": "Optional parameter to be passed to egw().open in order to open links in specified application " + } + }; + return et2_link_list; +}(et2_link_string)); +exports.et2_link_list = et2_link_list; +et2_core_widget_1.et2_register_widget(et2_link_list, ["link-list"]); /** - * UI widget for one or more links in a list (table) * - * @augments et2_inputWidget + * */ -var et2_link_add = (function(){ "use strict"; return et2_inputWidget.extend( -{ - attributes: { - "value": { - "description": "Either an array of link information (see egw_link::link()) or array with keys to_app and to_id", - "type": "any" - }, - "application": { - "name": "Application", - "type": "string", - "default": "", - "description": "Limit to the listed application or applications (comma seperated)" - } - }, - /** - * Constructor - * - * @memberOf et2_link_add - */ - init: function() { - this._super.apply(this, arguments); - - this.span = jQuery(document.createElement("span")) - .text(this.egw().lang("Add new")) - .addClass('et2_link_add_span'); - this.div = jQuery(document.createElement("div")).append(this.span); - this.setDOMNode(this.div[0]); - }, - doLoadingFinished: function() { - this._super.apply(this, arguments); - if(this.app_select && this.button) - { - // Already done - return false; - } - this.app_select = et2_createWidget("link-apps", jQuery.extend({},this.options,{ - 'id': this.options.id + 'app', - value: this.options.application ? this.options.application : this.options.value && this.options.value.add_app ? this.options.value.add_app : null, - application_list: this.options.application ? this.options.application : null - }) ,this); - this.div.append(this.app_select.getDOMNode()); - this.button = et2_createWidget("button", {id:this.options.id+"_add",label: this.egw().lang("add")}, this); - this.button.set_label(this.egw().lang("add")); - var self = this; - this.button.click = function() { - self.egw().open(self.options.value.to_app + ":" + self.options.value.to_id, self.app_select.get_value(), 'add'); - }; - this.div.append(this.button.getDOMNode()); - - return true; - }, - /** - * Should be handled client side. - * Return null to avoid overwriting other link values, in case designer used the same ID for multiple widgets - */ - getValue: function() { - return null; - } -});}).call(this); -et2_register_widget(et2_link_add, ["link-add"]); +var et2_link_add = /** @class */ (function (_super) { + __extends(et2_link_add, _super); + /** + * Constructor + */ + function et2_link_add(_parent, _attrs, _child) { + var _this = _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_link_add._attributes, _child || {})) || this; + _this.span = jQuery(document.createElement("span")) + .text(_this.egw().lang("Add new")) + .addClass('et2_link_add_span'); + _this.div = jQuery(document.createElement("div")).append(_this.span); + _this.setDOMNode(_this.div[0]); + return _this; + } + et2_link_add.prototype.doLoadingFinished = function () { + _super.prototype.doLoadingFinished.apply(this, arguments); + if (this.app_select && this.button) { + // Already done + return false; + } + this.app_select = et2_createWidget("link-apps", jQuery.extend({}, this.options, { + 'id': this.options.id + 'app', + value: this.options.application ? this.options.application : this.options.value && this.options.value.add_app ? this.options.value.add_app : null, + application_list: this.options.application ? this.options.application : null + }), this); + this.div.append(this.app_select.getDOMNode()); + this.button = et2_createWidget("button", { id: this.options.id + "_add", label: this.egw().lang("add") }, this); + this.button.set_label(this.egw().lang("add")); + var self = this; + this.button.click = function () { + self.egw().open(self.options.value.to_app + ":" + self.options.value.to_id, self.app_select.get_value(), 'add'); + return false; + }; + this.div.append(this.button.getDOMNode()); + return true; + }; + /** + * Should be handled client side. + * Return null to avoid overwriting other link values, in case designer used the same ID for multiple widgets + */ + et2_link_add.prototype.getValue = function () { + return null; + }; + et2_link_add._attributes = { + "value": { + "description": "Either an array of link information (see egw_link::link()) or array with keys to_app and to_id", + "type": "any" + }, + "application": { + "name": "Application", + "type": "string", + "default": "", + "description": "Limit to the listed application or applications (comma seperated)" + } + }; + return et2_link_add; +}(et2_core_inputWidget_1.et2_inputWidget)); +exports.et2_link_add = et2_link_add; +et2_core_widget_1.et2_register_widget(et2_link_add, ["link-add"]); +//# sourceMappingURL=et2_widget_link.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_link.ts b/api/js/etemplate/et2_widget_link.ts new file mode 100644 index 0000000000..8c6054458f --- /dev/null +++ b/api/js/etemplate/et2_widget_link.ts @@ -0,0 +1,2389 @@ +/** + * EGroupware eTemplate2 - JS Link object + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage api + * @link http://www.egroupware.org + * @author Nathan Gray + * @copyright 2011 Nathan Gray + */ + + /*egw:uses + /vendor/bower-asset/jquery/dist/jquery.js; + /vendor/bower-asset/jquery-ui/jquery-ui.js; + et2_core_inputWidget; + et2_core_valueWidget; + + // Include menu system for list context menu + egw_action.egw_menu_dhtmlx; + */ + + import {et2_register_widget, et2_widget, WidgetConfig} from "./et2_core_widget"; + import {ClassWithAttributes} from "./et2_core_inheritance"; + import {et2_valueWidget} from "./et2_core_valueWidget"; + import {et2_inputWidget} from "./et2_core_inputWidget"; + import {et2_selectbox} from "./et2_widget_selectbox"; + import {et2_button} from "./et2_widget_button"; + import {et2_vfs_select} from "./et2_widget_vfs"; + + /** + * UI widgets for Egroupware linking system + */ +export class et2_link_to extends et2_inputWidget +{ + static readonly _attributes: any = { + "only_app": { + "name": "Application", + "type": "string", + "default": "", + "description": "Limit to just this one application - hides app selection" + }, + "application_list": { + "name": "Application list", + "type": "any", + "default": "", + "description": "Limit to the listed application or applications (comma seperated)" + }, + "blur": { + "name": "Placeholder", + "type": "string", + "default": "", + "description": "This text get displayed if an input-field is empty and does not have the input-focus (blur). It can be used to show a default value or a kind of help-text.", + translate:true + }, + "no_files": { + "name": "No files", + "type": "boolean", + "default": false, + "description": "Suppress attach-files" + }, + "search_label": { + "name": "Search label", + "type": "string", + "default": "", + "description": "Label to use for search" + }, + "link_label": { + "name": "Link label", + "type": "string", + "default": "Link", + "description": "Label for the link button" + }, + "value": { + // Could be string or int if application is provided, or an Object + "type": "any" + } + }; + + private div: JQuery; + private link_button: JQuery; + private link_div: JQuery; + private filemanager_button: JQuery; + private file_div: JQuery; + private status_span: JQuery; + private link_entry: et2_link_entry; + private vfs_select: et2_vfs_select; + private file_upload: et2_file; + + + /** + * Constructor + * + * @memberOf et2_link_to + */ + constructor(_parent : et2_widget, _attrs? : WidgetConfig, _child? : object) { + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_link_to._attributes, _child || {})); + + this.div = jQuery(document.createElement("div")).addClass("et2_link_to et2_toolbar"); + + this.link_button = null; + this.status_span = null; + + this.link_entry = null; + this.file_upload = null; + + this.createInputWidget(); + } + + destroy( ) + { + this.link_button = null; + this.status_span = null; + + if(this.link_entry) + { + this.link_entry.destroy(); + this.link_entry = null; + } + if(this.file_upload) + { + this.file_upload.destroy(); + this.file_upload = null; + } + this.div = null; + + super.destroy.apply(this, arguments); + } + + /** + * Override to provide proper node for sub widgets to go in + * + * @param {Object} _sender + */ + getDOMNode( _sender) + { + if(_sender == this) { + return this.div[0]; + } else if (_sender._type == 'link-entry') { + return this.link_div[0]; + } else if (_sender._type == 'file') { + return this.file_div[0]; + } else if (_sender._type == 'vfs-select') { + return this.filemanager_button[0]; + } + } + + createInputWidget( ) + { + + // Need a div for file upload widget + this.file_div = jQuery(document.createElement("div")).css({display:'inline-block'}).appendTo(this.div); + + // Filemanager link popup + this.filemanager_button = jQuery(document.createElement("div")).css({display:'inline-block'}).appendTo(this.div); + + // Need a div for link-to widget + this.link_div = jQuery(document.createElement("div")) + .addClass('div_link') + // Leave room for link button + .appendTo(this.div); + + if (!this.options.readonly) + { + // One common link button + this.link_button = jQuery(document.createElement("button")) + .text(this.egw().lang(this.options.link_label)) + .appendTo(this.div).hide() + .addClass('link') + .click(this, this.createLink); + + // Span for indicating status + this.status_span = jQuery(document.createElement("span")) + .appendTo(this.div).addClass("status").hide(); + } + + this.setDOMNode(this.div[0]); + } + + doLoadingFinished( ) + { + super.doLoadingFinished.apply(this, arguments); + + var self = this; + if(this.link_entry && this.vfs_select && this.file_upload) + { + // Already done + return false; + } + + // Link-to + var link_entry_attrs = { + id: this.id + '_link_entry', + only_app: this.options.only_app, + application_list: this.options.application_list, + blur: this.options.search_label ? this.options.search_label : this.egw().lang('Search...'), + query: function() { self.link_button.hide(); return true;}, + select: function() {self.link_button.show(); return true;}, + readonly: this.options.readonly + }; + this.link_entry = et2_createWidget("link-entry", link_entry_attrs,this); + + // Filemanager select + var select_attrs : any = { + button_label: egw.lang('Link'), + button_caption: '', + button_icon:'link', + readonly: this.options.readonly, + dialog_title: egw.lang('Link'), + extra_buttons:[{text: egw.lang("copy"), id:"copy", image: "copy"}, + {text: egw.lang("move"), id:"move", image: "move"}], + onchange: function() + { + var values = true; + // If entry not yet saved, store for linking on server + if(!self.options.value.to_id || typeof self.options.value.to_id == 'object') + { + values = self.options.value.to_id || {}; + var files = self.vfs_select.getValue(); + if(typeof files !== 'undefined') + { + for(var i = 0; i < files.length; i++) + { + values['link:'+files[i]] = { + app: 'link', + id: files[i], + type: 'unknown', + icon: 'link', + remark: '', + title: files[i] + }; + } + } + } + self._link_result(values); + } + }; + // only set server-side callback, if we have a real application-id (not null or array) + // otherwise it only gives an error on server-side + if (self.options.value && self.options.value.to_id && typeof self.options.value.to_id != 'object') { + select_attrs.method = 'EGroupware\\Api\\Etemplate\\Widget\\Link::ajax_link_existing'; + select_attrs.method_id = self.options.value.to_app + ':' + self.options.value.to_id; + } + this.vfs_select = et2_createWidget("vfs-select", select_attrs,this); + this.vfs_select.set_readonly(this.options.readonly); + + // File upload + var file_attrs = { + multiple: true, + id: this.id + '_file', + label: '', + // Make the whole template a drop target + drop_target: this.getInstanceManager().DOMContainer.getAttribute("id"), + readonly: this.options.readonly, + + // Change to this tab when they drop + onStart: function( event, file_count) + { + // Find the tab widget, if there is one + var tabs : et2_widget = self; + do { + tabs = tabs.getParent(); + } while (tabs != self.getRoot() && tabs.getType() != 'tabbox'); + if(tabs != self.getRoot()) + { + (tabs).activateTab(self); + } + return true; + }, + onFinish: function( event, file_count) + { + event.data = self; + self.filesUploaded(event); + + // Auto-link uploaded files + self.createLink(event); + } + }; + + this.file_upload = et2_createWidget("file", file_attrs,this); + this.file_upload.set_readonly(this.options.readonly); + return true; + } + + getValue( ) + { + return this.options.value; + } + + filesUploaded( event) + { + var self = this; + + this.link_button.show(); + } + + /** + * Create a link using the current internal values + * + * @param {Object} event + */ + createLink( event) + { + // Disable link button + event.data.link_button.attr("disabled", true); + + var values = event.data.options.value; + var self = event.data; + + var links = []; + + // Links to other entries + event.data = self.link_entry; + self.link_entry.createLink(event,links); + + // Files + if(!self.options.no_files) + { + for(var file in self.file_upload.options.value) { + + links.push({ + app: 'file', + id: file, + name: self.file_upload.options.value[file].name, + type: self.file_upload.options.value[file].type, + remark: jQuery("li[file='"+self.file_upload.options.value[file].name.replace(/'/g, '"')+"'] > input", self.file_upload.progress) + .filter(function() { return jQuery(this).attr("placeholder") != jQuery(this).val();}).val() + }); + } + } + if(links.length == 0) + { + return; + } + + var request = egw.json("EGroupware\\Api\\Etemplate\\Widget\\Link::ajax_link", + [values.to_app, values.to_id, links], + self._link_result, + self, + true, + self + ); + request.sendRequest(); + } + + /** + * Sent some links, server has a result + * + * @param {Object} success + */ + _link_result( success) + { + if(success) + { + this.link_button.hide().attr("disabled", false); + this.status_span.removeClass("error").addClass("success"); + this.status_span.fadeIn().delay(1000).fadeOut(); + delete this.options.value.app; + delete this.options.value.id; + for(var file in this.file_upload.options.value) { + delete this.file_upload.options.value[file]; + } + this.file_upload.progress.empty(); + + // Server says it's OK, but didn't store - we'll send this again on submit + // This happens if you link to something before it's saved to the DB + if(typeof success == "object") + { + // Save as appropriate in value + if(typeof this.options.value != "object") + { + this.options.value = {}; + } + this.options.value.to_id = success; + for(let link in success) + { + // Icon should be in registry + if(typeof success[link].icon == 'undefined') + { + success[link].icon = egw.link_get_registry(success[link].app,'icon'); + // No icon, try by mime type - different place for un-saved entries + if(success[link].icon == false && success[link].id.type) + { + // Triggers icon by mime type, not thumbnail or app + success[link].type = success[link].id.type; + success[link].icon = true; + } + } + // Special handling for file - if not existing, we can't ask for title + if(success[link].app == 'file' && typeof success[link].title == 'undefined') + { + success[link].title = success[link].id.name || ''; + } + } + } + + // Look for a link-list with the same ID, refresh it + var self = this; + var list_widget = null; + this.getRoot().iterateOver( + function(widget) { + if(widget.id == self.id) { + list_widget = widget; + if(success === true) + { + widget._get_links(); + } + } + }, + this, et2_link_list + ); + + // If there's an array of data (entry is not yet saved), updating the list will + // not work, so add them in explicitly. + if(list_widget && success) + { + // Clear list + list_widget.set_value(null); + + // Add temp links in + for(var link_id in success) + { + let link = success[link_id]; + if(typeof link.title == 'undefined') + { + // Callback to server for title + egw.link_title(link.app, link.id, function(title) { + link.title = title; + list_widget._add_link(link); + }); + } + else + { + // Add direct + list_widget._add_link(link); + } + } + } + } + else + { + this.status_span.removeClass("success").addClass("error") + .fadeIn(); + } + this.div.trigger('link.et2_link_to',success); + } + + set_no_files(no_files) + { + if(this.options.readonly) return; + if(no_files) + { + this.file_div.hide(); + this.filemanager_button.hide(); + } + else + { + this.file_div.show(); + this.filemanager_button.show(); + } + this.options.no_files = no_files; + } +} +et2_register_widget(et2_link_to, ["link-to"]); + +/** + * List of applications that support link + */ +export class et2_link_apps extends et2_selectbox +{ + static readonly _attributes: any = { + "only_app": { + "name": "Application", + "type": "string", + "default": "", + "description": "Limit to just this one application - hides app selection" + }, + "application_list": { + "name": "Application list", + "type": "any", + "default": "", + "description": "Limit to the listed application or applications (comma seperated)" + } + }; + + /** + * Constructor + * + */ + constructor(_parent : et2_widget, _attrs? : WidgetConfig, _child? : object) + { + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_link_apps._attributes, _child || {})); + + + if (this.options.select_options != null) + { + // Preset to last application + if(!this.options.value) + { + this.set_value(egw.preference('link_app', this.egw().getAppName())); + } + // Register to update preference + var self = this; + this.input.bind("click",function() { + if (typeof self.options.value != 'undefined') var appname = self.options.value.to_app; + egw.set_preference(appname || self.egw().getAppName(),'link_app',self.getValue()); + }); + } + } + + /** + * We get some minor speedups by overriding parent searching and directly setting select options + * + * @param {Array} _attrs an array of attributes + */ + transformAttributes( _attrs) + { + var select_options = {}; + + // Limit to one app + if(_attrs.only_app) + { + select_options[_attrs.only_app] = this.egw().lang(_attrs.only_app); + } + else if (_attrs.application_list) + { + select_options = _attrs.application_list; + } + else + { + select_options = egw.link_app_list('query'); + if(typeof select_options['addressbook-email'] !== 'undefined') + { + delete select_options['addressbook-email']; + } + } + _attrs.select_options = select_options; + super.transformAttributes(_attrs); + } +} +et2_register_widget(et2_link_apps, ["link-apps"]); + +/** + * Search and select an entry for linking + */ +export class et2_link_entry extends et2_inputWidget +{ + static readonly _attributes : any = { + "value": { + "type": "any", + "default": {} + }, + "only_app": { + "name": "Application", + "type": "string", + "default": "", + "description": "Limit to just this one application - hides app selection" + }, + "application_list": { + "name": "Application list", + "type": "any", + "default": "", + "description": "Limit to the listed applications (comma seperated)" + }, + "app_icons": { + "name": "Application icons", + "type": "boolean", + "default": false, + "description": "Show application icons instead of names" + }, + "blur": { + "name": "Placeholder", + "type": "string", + "default": et2_no_init, + "description": "This text get displayed if an input-field is empty and does not have the input-focus (blur). It can be used to show a default value or a kind of help-text.", + translate:true + }, + "query": { + "name": "Query callback", + "type": "js", + "default": et2_no_init, + "description": "Callback before query to server. It will be passed the request & et2_link_entry objects. Must return true, or false to abort query." + }, + "select": { + "name": "Select callback", + "type": "js", + "default": et2_no_init, + "description": "Callback when user selects an option. Must return true, or false to abort normal action." + } + }; + + protected static readonly legacyOptions = ["only_app", "application_list"]; + protected static readonly search_timeout = 500; //ms after change to send query + protected static readonly minimum_characters = 4; // Don't send query unless there's at least this many chars + + private cache: any = {}; + + private div: JQuery; + private app_select: JQuery; + private search: JQuery; + private clear: JQuery; + private link_button: JQuery; + private response: any; + private request: any; + private last_search: string; + processing: boolean = false; + + /** + * Constructor + * + * @memberOf et2_link_entry + */ + constructor(_parent : et2_widget, _attrs? : WidgetConfig, _child? : object) + { + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_link_entry._attributes, _child || {})); + + this.search = null; + this.clear = null; + this.app_select = null; + this._oldValue = { + id: null, + app: this.options.value && this.options.value.app ? this.options.value.app : this.options.only_app + }; + + if(typeof this.options.value == 'undefined' || this.options.value == null) + { + this.options.value = {}; + } + this.cache = {}; + this.request = null; + + this.createInputWidget(); + var self = this; + + jQuery(this.getInstanceManager().DOMContainer).on('clear', function(){ + // We need to unbind events to prevent a second triggerd event handler + // (eg. setting a project in infolog edit dialog) when the widget gets cleared. + jQuery(self.getDOMNode()).off(); + }); + } + + destroy( ) + { + super.destroy.apply(this, arguments); + + this.div = null; + if(this.search.data("ui-autocomplete")) + { + this.search.autocomplete("destroy"); + } + this.search = null; + this.clear = null; + this.app_select = null; + this.request = null; + } + + createInputWidget( ) + { + var self = this; + this.div = jQuery(document.createElement("div")).addClass("et2_link_entry"); + + // Application selection + jQuery.widget( "custom.iconselectmenu", jQuery.ui.selectmenu, { + _setText: function(element, value) + { + if(element === this.buttonText) + { + this._setButtonText(value); + } + else + { + this._superApply(element, value); + } + }, + + _setButtonText: function( value ) + { + + var _value = this.focusIndex; + + if(typeof this.focusIndex === 'undefined') + { + _value = this.element.find( "option:selected" ).val(); + } + else + { + var selected = this.items[_value] || {}; + _value = selected.value; + } + var url = self.egw().image('navbar', _value); + var buttonItem = jQuery( "", { + "class": "ui-selectmenu-text", + title: value + }); + + jQuery('.ui-selectmenu-text', this.button).replaceWith(buttonItem); + buttonItem.css('background-image', 'url('+url+')'); + }, + _renderItem: function( ul, item ) + { + var li = jQuery( "
        • ", {class:"et2_link_entry_app_option"}), + wrapper = jQuery( "
          ", {text: item.label} ); + + if ( item.disabled ) + { + li.addClass( "ui-state-disabled" ); + } + ul.addClass(self.div.attr("class")); + var url = self.egw().image('navbar', item.value); + jQuery( "", { + style: 'background-image: url("'+url+'");', + "class": "ui-icon " + item.element.attr( "data-class" ), + title: item.label + }) + .appendTo( wrapper ); + + return li.append( wrapper ).appendTo( ul ); + } + }); + + this.app_select = jQuery(document.createElement("select")).appendTo(this.div) + .change(function(e) + { + // Clear cache when app changes + self.cache = {}; + + // Update preference with new value + egw.set_preference(self.options.value.to_app || self.egw().getAppName(),'link_app',self.app_select.val()); + + if(typeof self.options.value != 'object') self.options.value = {}; + self.options.value.app = self.app_select.val(); + }); + var opt_count = 0; + for(var key in this.options.select_options) + { + opt_count++; + var option = jQuery(document.createElement("option")) + .attr("value", key) + .text(this.options.select_options[key]); + option.appendTo(this.app_select); + } + if(this.options.only_app) + { + this.app_select.val(this.options.only_app); + this.app_select.hide(); + if(this.options.app_icons && this.app_select.iconselectmenu('instance')) + { + this.app_select.iconselectmenu('widget').hide(); + } + this.div.addClass("no_app"); + } + else + { + // Now that options are in, set to last used app + this.app_select.val(this.options.value.app||''); + if(this.app_select.iconselectmenu('instance')) + { + this.app_select.iconselectmenu('update'); + } + } + + // Search input + this.search = jQuery(document.createElement("input")) + // .attr("type", "search") // Fake it for all browsers below + .focus(function(){if(!self.options.only_app) { + // Adjust width, leave room for app select & link button + self.div.removeClass("no_app"); + if(self.options.app_icons) + { + self.app_select.iconselectmenu('widget').show(); + } + else + { + self.app_select.show(); + } + }}) + .blur(function(e) { + if(self.div.has(e.relatedTarget).length) return; + if(self.options.app_icons) + { + // Adjust width, leave room for app select & link button + self.div.addClass("no_app"); + self.app_select.iconselectmenu('widget').hide(); + } + else if (self.search.val()) + { + if(self.options.only_app) { + // Adjust width, leave room for app select & link button + self.div.addClass("no_app"); + } + } + }) + .appendTo(this.div); + + this.set_blur(this.options.blur ? this.options.blur : this.egw().lang("search"), this.search); + + // Autocomplete + this.search.autocomplete({ + source: function( request, response) + { + return self.query(request, response); + }, + select: function( event, item) + { + event.data = self; + // Correct changed value from server + item.item.value = item.item.value.trim(); + self.select(event,item); + return false; + }, + focus: function( event, item) + { + event.stopPropagation(); + self.search.val(item.item.label); + return false; + }, + minLength: et2_link_entry.minimum_characters, + delay: et2_link_entry.search_timeout, + disabled: self.options.disabled, + appendTo: self.div + }); + + // Custom display (colors) + this.search.data("uiAutocomplete")._renderItem = function(ul, item) + { + var li = jQuery(document.createElement('li')) + .data("item.autocomplete", item); + var extra : any = {}; + + // Extra stuff + if(typeof item.label == 'object') { + extra = item.label; + item.label = extra.label ? extra.label : extra; + if(extra['style.backgroundColor'] || extra.color) + { + li.css({'border-left': '5px solid ' + (extra.color ? extra.color : extra['style.backgroundColor'])}); + } + // Careful with this, some browsers may have trouble loading all at once, which can slow display + if(extra.icon) + { + var img = self.egw().image(extra.icon); + if(img) + { + jQuery(document.createElement("img")) + .attr("src", img) + .css("float", "right") + .appendTo(li); + } + } + } + + // Normal stuff + li.append(jQuery( "" ).text( item.label )) + .appendTo(ul); + window.setTimeout(function(){ul.css('max-width', jQuery('.et2_container').width()-ul.offset().left);}, 300); + return li; + }; + + // Bind to enter key to start search early + this.search.keydown(function(e) + { + var keycode = (e.keyCode ? e.keyCode : e.which); + if(keycode == 13 && !self.processing) + { + self.search.autocomplete("option","minLength", 0); + self.search.autocomplete("search"); + self.search.autocomplete("option","minLength", self.minimum_characters); + return false; + } + }); + + // Clear / last button + this.clear = jQuery(document.createElement("span")) + .addClass("ui-icon ui-icon-close") + .click(function(e) + { + if (!self.search) return; // only gives an error, we should never get into that situation + // No way to tell if the results is open, so if they click the button while open, it clears + if(self.last_search && self.last_search != self.search.val()) + { + // Repeat last search (should be cached) + self.search.val(self.last_search); + self.last_search = ""; + self.search.autocomplete("search"); + } + else + { + // Clear + self.search.autocomplete("close"); + self.set_value(null); + self.search.val(""); + // call trigger, after finishing this handler, not in the middle of it + window.setTimeout(function() + { + self.search.trigger("change"); + }, 0); + } + self.search.focus(); + }) + .appendTo(this.div) + .hide(); + + this.setDOMNode(this.div[0]); + } + + getDOMNode( ) + { + return this.div ? this.div[0] : null; + } + + transformAttributes( _attrs) + { + super.transformAttributes.apply(this, arguments); + + + _attrs["select_options"] = {}; + if(_attrs["application_list"]) + { + var apps = (typeof _attrs["application_list"] == "string") ? et2_csvSplit(_attrs["application_list"], null, ","): _attrs["application_list"]; + for(var i = 0; i < apps.length; i++) + { + _attrs["select_options"][apps[i]] = this.egw().lang(apps[i]); + } + } + else + { + _attrs["select_options"] = this.egw().link_app_list('query'); + if (typeof _attrs["select_options"]["addressbook-email"] != 'undefined') delete _attrs["select_options"]["addressbook-email"]; + } + + // Check whether the options entry was found, if not read it from the + // content array. + if (_attrs["select_options"] == null) + { + _attrs["select_options"] = this.getArrayMgr('content') + .getEntry("options-" + this.id); + } + + // Default to an empty object + if (_attrs["select_options"] == null) + { + _attrs["select_options"] = {}; + } + } + + doLoadingFinished( ) + { + if(typeof this.options.value == 'object' && !this.options.value.app) + { + this.options.value.app = egw.preference('link_app',this.options.value.to_app || this.egw().getAppName()); + // If there's no value set for app, then take the first one from the selectbox + if (typeof this.options.value.app == 'undefined' || !this.options.value.app) + { + this.options.value.app = Object.keys(this.options.select_options)[0]; + } + this.app_select.val(this.options.value.app); + } + + if(this.options.app_icons) + { + var self = this; + this.div.addClass('app_icons'); + this.app_select.iconselectmenu({ + width: 50, + change: function( ) + { + window.setTimeout(function() + { + self.app_select.trigger("change"); + }, 0); + } + }) + .iconselectmenu( "menuWidget" ); + this.app_select.iconselectmenu('widget').hide(); + } + return super.doLoadingFinished.apply(this,arguments); + } + + getValue( ) + { + var value = this.options && this.options.only_app ? this.options.value.id : this.options? this.options.value: null; + if(this.options && !this.options.only_app && this.search) + { + value.search = this.search.val(); + } + return value; + } + + set_value( _value) + { + if(typeof _value == 'string' || typeof _value == 'number') + { + if(typeof _value == 'string' && _value.indexOf(",") > 0) _value = _value.replace(",",":"); + if(typeof _value == 'string' && _value.indexOf(":") >= 0) + { + var split = _value.split(":"); + + _value = { + app: split.shift(), + id: split.length == 1 ? split[0] : split + }; + } + else if(_value && this.options.only_app) + { + _value = { + app: this.options.only_app, + id: _value + }; + } + } + this._oldValue = this.options.value; + if(!_value || _value.length == 0 || _value == null || jQuery.isEmptyObject(_value)) + { + this.search.val(""); + this.clear.hide(); + this.options.value = _value = {'id':null}; + } + if(!_value.app) _value.app = this.options.only_app || this.app_select.val(); + + if(_value.id) { + // Remove specific display and revert to CSS file + // show() would use inline, should be inline-block + this.clear.css('display',''); + } else { + this.clear.hide(); + return; + } + if(typeof _value != 'object' || (!_value.app && !_value.id)) + { + console.warn("Bad value for link widget. Need an object with keys 'app', 'id', and optionally 'title'", _value); + return; + } + if(!_value.title) { + var title = this.egw().link_title(_value.app, _value.id); + if(title != null) + { + _value.title = title; + } + else + { + // Title will be fetched from server and then set + var title = this.egw().link_title(_value.app, _value.id, function(title) + { + this.search.removeClass("loading").val(title+""); + // Remove specific display and revert to CSS file + // show() would use inline, should be inline-block + this.clear.css('display',''); + }, this); + this.search.addClass("loading"); + } + } + if(_value.title) + { + this.search.val(_value.title+""); + } + this.options.value = _value; + + jQuery("option[value='"+_value.app+"']",this.app_select).prop("selected",true); + this.app_select.hide(); + if(this.options.app_icons && this.app_select.iconselectmenu('instance')) + { + this.app_select.iconselectmenu('widget').hide(); + } + this.div.addClass("no_app"); + } + + set_blur( _value, input) + { + + if(typeof input == 'undefined') input = this.search; + + if(_value) + { + input.attr("placeholder", _value); // HTML5 + if(!input[0].placeholder) + { + // Not HTML5 + if(input.val() == "") input.val(_value); + input.focus(input,function(e) + { + var placeholder = _value; + if(e.data.val() == placeholder) e.data.val(""); + }).blur(input, function(e) + { + var placeholder = _value; + if(e.data.val() == "") e.data.val(placeholder); + }); + if(input.val() == "") + { + input.val(_value); + } + } + } + else + { + this.search.removeAttr("placeholder"); + } + } + + /** + * Set the query callback + * + * @param {function} f + */ + set_query(f) + { + this.options.query = f; + } + + /** + * Set the select callback + * + * @param {function} f + */ + set_select(f) + { + this.options.select = f; + } + + /** + * Ask server for entries matching selected app/type and filtered by search string + * + * @param {Object} request + * @param {Object} response + */ + query( request, response) + { + // If there is a pending request, abort it + if(this.request) + { + this.request.abort(); + this.request = null; + } + + // Remember last search + this.last_search = this.search.val(); + + // Allow hook / tie in + if(this.options.query && typeof this.options.query == 'function') + { + if(!this.options.query(request, this)) return false; + } + + if((typeof request.no_cache == 'undefined' && !request.no_cache) && request.term in this.cache) { + return response(this.cache[request.term]); + } + + // Remember callback + this.response = response; + + this.search.addClass("loading"); + // Remove specific display and revert to CSS file + // show() would use inline, should be inline-block + this.clear.css('display',''); + this.request = egw.json("EGroupware\\Api\\Etemplate\\Widget\\Link::ajax_link_search", + [this.app_select.val(), '', request.term, request.options], + this._results, + this,true,this + ).sendRequest(); + } + + /** + * User selected a value + * + * @param {Object} event + * @param {Object} selected + * + */ + select( event, selected) + { + if(selected.item.value !== null && typeof selected.item.value == "string") + { + // Correct changed value from server + selected.item.value = selected.item.value.trim(); + } + if(this.options.select && typeof this.options.select == 'function') + { + if(!this.options.select(event, selected)) return false; + } + if(typeof event.data.options.value != 'object' || event.data.options.value == null) + { + event.data.options.value = {}; + } + event.data.options.value.id = selected.item.value; + + // Set a processing flag to filter some events + event.data.processing = true; + + // Remove specific display and revert to CSS file + // show() would use inline, should be inline-block + this.clear.css('display',''); + event.data.search.val(selected.item.label); + + // Fire change event + this.search.change(); + + // Turn off processing flag when done + window.setTimeout(jQuery.proxy(function() {delete this.processing;},event.data)); + } + + /** + * Server found some results + * + * @param {Array} data + */ + _results( data) + { + if(this.request) + { + this.request = null; + } + this.search.removeClass("loading"); + var result = []; + for(var id in data) { + result.push({"value": id, "label":data[id]}); + } + this.cache[this.search.val()] = result; + this.response(result); + } + + /** + * Create a link using the current internal values + * + * @param {Object} event + * @param {Object} _links + */ + createLink( event, _links) + { + + var values = event.data.options.value; + var self = event.data; + var links = []; + + if(typeof _links == 'undefined') + { + links = []; + } + else + { + links = _links; + } + + // Links to other entries + if(values.id) { + links.push({ + app: values.app, + id: values.id + }); + self.search.val(""); + } + + // If a link array was passed in, don't make the ajax call + if(typeof _links == 'undefined') + { + var request = egw.json("EGroupware\\Api\\Etemplate\\Widget\\Link::ajax_link", + [values.to_app, values.to_id, links], + self._link_result, + this, + true + ); + request.sendRequest(); + } + } + + /** + * Sent some links, server has a result + * + * @param {Object} success + * + */ + _link_result( success) + { + if(success) { + this.status_span.fadeIn().delay(1000).fadeOut(); + delete this.options.value.app; + delete this.options.value.id; + } + } +} +et2_register_widget(et2_link_entry, ["link-entry"]); + +/** + * UI widget for a single (read-only) link + * + */ +export class et2_link extends et2_valueWidget implements et2_IDetachedDOM +{ + static readonly _attributes : any = { + "only_app": { + "name": "Application", + "type": "string", + "default": "", + "description": "Use the given application, so you can pass just the ID for value" + }, + "value": { + description: "Array with keys app, id, and optionally title", + type: "any" + }, + "needed": { + "ignore": true + }, + "link_hook": { + "name": "Type", + "type": "string", + "default": "view", + "description": "Hook used for displaying link (view/edit/add)" + }, + "target_app": { + "name": "Target application", + "type": "string", + "default": "", + "description": "Optional parameter to be passed to egw().open in order to open links in specified application" + }, + "extra_link_target": { + "name": "Link target", + "type": "string", + "default": null, + "description": "Optional parameter to be passed to egw().open in order to open links in specified target eg. _blank" + } + }; + legacyOptions = ["only_app"]; + + private label_span: JQuery; + private link: JQuery; + + /** + * Constructor + * + * @memberOf et2_link + */ + constructor(_parent : et2_widget, _attrs? : WidgetConfig, _child? : object) + { + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_link._attributes, _child || {})); + + + this.label_span = jQuery(document.createElement("label")) + .addClass("et2_label"); + this.link = jQuery(document.createElement("span")) + .addClass("et2_link") + .appendTo(this.label_span); + + if(this.options['class']) this.label_span.addClass(this.options['class']); + this.setDOMNode(this.label_span[0]); + } + destroy( ) + { + if(this.link) this.link.unbind(); + this.link = null; + super.destroy.apply(this, arguments); + } + set_label( label) + { + // Remove current label + this.label_span.contents() + .filter(function(){ return this.nodeType == 3; }).remove(); + + var parts = et2_csvSplit(label, 2, "%s"); + this.label_span.prepend(parts[0]); + this.label_span.append(parts[1]); + this.label = label; + + // add class if label is empty + this.label_span.toggleClass('et2_label_empty', !label || !parts[0]); + } + set_value( _value) + { + if(typeof _value != 'object' && _value && !this.options.only_app) + { + if(_value.indexOf(':') >= 0) + { + var app = _value.split(':',1); + var id = _value.substr(app[0].length+1); + _value = {'app': app[0], 'id': id}; + } + else + { + console.warn("Bad value for link widget. Need an object with keys 'app', 'id', and optionally 'title'", _value); + return; + } + } + // Application set, just passed ID + else if (typeof _value != "object") + { + _value = { + app: this.options.only_app, + id: _value + }; + } + if(!_value || jQuery.isEmptyObject(_value)) + { + this.link.text("").unbind(); + return; + } + var self = this; + this.link.unbind(); + if(_value.id && _value.app) + { + this.link.addClass("et2_link"); + this.link.click( function(e) + { + if( !self.options.target_app ){ + self.options.target_app = _value.app; + } + const target = self.options.extra_link_target || _value.app; + self.egw().open(_value, "", self.options.link_hook, _value.extra_args, target, self.options.target_app); + e.stopImmediatePropagation(); + }); + } + else + { + this.link.removeClass("et2_link"); + } + if(!_value.title) + { + var self = this; + var node = this.link[0]; + if(_value.app && _value.id) + { + var title = this.egw().link_title(_value.app, _value.id, function(title) {self.set_title(node, title);}, this); + if(title != null) + { + _value.title = title; + } + else + { + // Title will be fetched from server and then set + return; + } + } + else + { + _value.title = ""; + } + } + this.set_title(this.link, _value.title); + } + + /** + * Sets the text to be displayed. + * Used as a callback, so node is provided to make sure we get the right one + * + * @param {Object} node + * @param {String} _value description + */ + set_title( node, _value) + { + if(_value === false || _value === null) _value = ""; + jQuery(node).text(_value+""); + } + + /** + * 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 an array of attributes + */ + getDetachedAttributes( _attrs) + { + _attrs.push("label","value"); + } + + /** + * Returns an array of DOM nodes. The (relatively) same DOM-Nodes have to be + * passed to the "setDetachedAttributes" function in the same order. + */ + getDetachedNodes( ) + { + return [this.node, this.link[0]]; + } + + /** + * 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, _values) + { + this.node = _nodes[0]; + this.label_span = jQuery(_nodes[0]); + this.link = jQuery(_nodes[1]); + if(typeof _values["id"] !== "undefined") this.set_id(_values['id']); + if(typeof _values["label"] !== "undefined") this.set_label(_values['label']); + if(typeof _values["value"] !== "undefined") this.set_value(_values["value"]); + } + +} +et2_register_widget(et2_link, ["link", "link-entry_ro"]); + +/** + * UI widget for one or more links, comma separated + * + * TODO: This one used to have expose + */ +export class et2_link_string extends expose(class et2_link_string extends et2_valueWidget implements et2_IDetachedDOM, et2_IExposable +{ + static readonly _attributes : any = { + "application": { + "name": "Application", + "type": "string", + "default": "", + "description": "Use the given application, so you can pass just the ID for value" + }, + "value": { + "description": "Either an array of link information (see egw_link::link()) or array with keys to_app and to_id", + "type": "any" + }, + "only_app": { + "name": "Application filter", + "type": "string", + "default": "", + "description": "Appname, eg. 'projectmananager' to list only linked projects" + }, + "link_type": { + "name": "Type filter", + "type": "string", + "default":"", + "description": "Sub-type key to list only entries of that type" + }, + "expose_view":{ + name: "Expose view", + type: "boolean", + default: true, + description: "Clicking on description with href value would popup an expose view, and will show content referenced by href." + } + }; + protected list: JQuery; + protected value: any; + + /** + * Constructor + * + * @memberOf et2_link_string + */ + constructor(_parent : et2_widget, _attrs? : WidgetConfig, _child? : object) + { + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_link_string._attributes, _child || {})); + + + this.list = jQuery(document.createElement("ul")) + .addClass("et2_link_string"); + + if(this.options['class']) this.list.addClass(this.options['class']); + this.setDOMNode(this.list[0]); + } + + destroy( ) + { + super.destroy.apply(this, arguments); + if (this.node != null) { + jQuery(this.node).children().unbind(); + } + } + + set_value( _value) + { + // Get data + if(!_value || _value == null || !this.list) + { + // List can be missing if the AJAX call returns after the form is destroyed + if (this.list) + { + this.list.empty(); + } + return; + } + if(typeof _value == "string" && _value.indexOf(',') > 0) + { + _value = _value.split(','); + } + if(!_value.to_app && typeof _value == "object" && this.options.application) + { + _value.to_app = this.options.application; + } + + if(typeof _value == 'object' && _value.to_app && _value.to_id) + { + this.value = _value; + this._get_links(); + return; + } + this.list.empty(); + if(typeof _value == 'object' && _value.length > 0) + { + // Have full info + // Don't store new value, just update display + + + // Make new links + for(var i = 0; i < _value.length; i++) + { + if(!this.options.only_app || this.options.only_app && _value[i].app == this.options.only_app) + { + this._add_link(_value[i].id ? _value[i] : {id:_value[i], app: _value.to_app}); + } + } + } + else if(this.options.application) + { + this._add_link({id:_value, app: this.options.application}); + } + } + + _get_links( ) + { + var _value = this.value; + // Just IDs - get from server + if(this.options.only_app) + { + _value.only_app = this.options.only_app; + } + this.egw().jsonq('EGroupware\\Api\\Etemplate\\Widget\\Link::ajax_link_list', [_value], this.set_value, this); + return; + } + /** + * Function to get media content to feed the expose + * @param {type} _value + * @returns {Array|Array.getMedia.mediaContent} + */ + getMedia(_value) + { + let base_url = egw.webserverUrl.match(/^\//,'ig')?egw(window).window.location.origin + egw.webserverUrl : egw.webserverUrl; + let mediaContent = []; + if (_value && typeof _value.type !='undefined' && _value.type.match(/video\//,'ig')) + { + mediaContent = [{ + title: _value.id, + type: _value.type, + poster:'', // TODO: Should be changed by correct video thumbnail later + href: base_url + egw().mime_open(_value), + download_href: base_url + egw().mime_open(_value) + '?download' + }]; + } + else if(_value) + { + mediaContent = [{ + title: _value.id, + href: base_url + egw().mime_open(_value).url, + download_href: base_url + egw().mime_open(_value).url + '?download', + type: _value.type + }]; + } + if (mediaContent[0].href && mediaContent[0].href.match(/\/webdav.php/,'ig')) mediaContent[0]["download_href"] = mediaContent[0].href + '?download'; + return mediaContent; + } + _add_link( _link_data) + { + var self = this; + var link = jQuery(document.createElement("li")) + .appendTo(this.list) + .addClass("et2_link loading") + .click( function(e){ + var fe = egw_get_file_editor_prefered_mimes(_link_data.type); + if (self.options.expose_view && typeof _link_data.type !='undefined' + && _link_data.type.match(self.mime_regexp,'ig')) + { + self._init_blueimp_gallery(e, _link_data); + } + else if(typeof _link_data.type !='undefined' && fe && fe.mime && fe.mime[_link_data.type]) + { + egw.open_link(egw.link('/index.php', { + menuaction: fe.edit.menuaction, + path: egw().mime_open(_link_data).url.replace('/webdav.php','') + }), '', fe.edit_popup); + } + else + { + self.egw().open(_link_data, "", "view",null,_link_data.app,_link_data.app); + } + e.stopImmediatePropagation(); + }); + + if(_link_data.title) link.text(_link_data.title); + + // Now that link is created, get title from server & update + if(!_link_data.title) + { + this.egw().link_title(_link_data.app, _link_data.id, function(title) { + if (title) + this.removeClass("loading").text(title); + else + this.remove(); // no rights or not found + }, link); + } + } + + /** + * 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 an array of attributes + */ + getDetachedAttributes( _attrs) + { + // Create the label container if it didn't exist yet + if (this._labelContainer == null) + { + this._labelContainer = jQuery(document.createElement("label")) + .addClass("et2_label"); + this.getSurroundings().insertDOMNode(this._labelContainer[0]); + this.getSurroundings().update(); + } + _attrs.push("value","label"); + } + + /** + * Returns an array of DOM nodes. The (relatively) same DOM-Nodes have to be + * passed to the "setDetachedAttributes" function in the same order. + */ + getDetachedNodes( ) + { + // Create the label container if it didn't exist yet + if (this._labelContainer == null) + { + this._labelContainer = jQuery(document.createElement("label")) + .addClass("et2_label"); + this.getSurroundings().insertDOMNode(this._labelContainer[0]); + } + return [this.list[0], this._labelContainer[0]]; + } + + /** + * 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, _values) + { + this.list = jQuery(_nodes[0]); + + this.set_value(_values["value"]); + + // Special detached, to prevent DOM node modification of the normal method + this._labelContainer = _nodes.length > 1 ? jQuery(_nodes[1]) : null; + if(_values['label']) + { + this.set_label(_values['label']); + } + else if (this._labelContainer) + { + this._labelContainer.contents().not(this.list).remove(); + } + } +}){}; +et2_register_widget(et2_link_string, ["link-string"]); + +/** + * UI widget for one or more links in a list (table) + */ +export class et2_link_list extends et2_link_string +{ + static readonly _attributes : any = { + "show_deleted": { + "name": "Show deleted", + "type": "boolean", + "default": false, + "description": "Show links that are marked as deleted, being held for purge" + }, + "onchange": { + "name": "onchange", + "type": "js", + "default": et2_no_init, + "description": "JS code which is executed when the links change." + }, + readonly: { + name: "readonly", + type: "boolean", + "default": false, + description: "Does NOT allow user to enter data, just displays existing data" + }, + "target_app": { + "name": "Target application", + "type": "string", + "default": "", + "description": "Optional parameter to be passed to egw().open in order to open links in specified application " + } + }; + private context: egwMenu; + + /** + * Constructor + * + */ + constructor(_parent : et2_widget, _attrs? : WidgetConfig, _child? : object) + { + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_link_list._attributes, _child || {})); + + + this.list = jQuery(document.createElement("table")) + .addClass("et2_link_list"); + if(this.options['class']) this.list.addClass(this.options['class']); + this.setDOMNode(this.list[0]); + + // Set up context menu + var self = this; + this.context = new egwMenu(); + this.context.addItem("comment", this.egw().lang("Comment"), "", function() + { + var link_id = typeof self.context.data.link_id == 'number' ? self.context.data.link_id : self.context.data.link_id.replace(/[:\.]/g,'_'); + + et2_dialog.show_prompt( + function(button, comment) + { + if(button != et2_dialog.OK_BUTTON) return; + var remark = jQuery('#link_'+(self.context.data.dom_id ? self.context.data.dom_id : link_id), self.list).children('.remark'); + if(isNaN(self.context.data.link_id)) // new entry, not yet stored + { + remark.text(comment); + // Look for a link-to with the same ID, refresh it + if(self.context.data.link_id) + { + var _widget = link_id.widget || null; + self.getRoot().iterateOver( + function(widget) + { + if(widget.id == self.id) + { + _widget = widget; + } + }, + self, et2_link_to + ); + var value = _widget != null ? _widget.getValue() : false; + if(_widget && value && value.to_id) + { + value.to_id[self.context.data.link_id].remark = comment; + } + } + return; + } + remark.addClass("loading"); + var request = egw.json("EGroupware\\Api\\Etemplate\\Widget\\Link::ajax_link_comment", + [link_id, comment], + function() + { + if(remark) + { + // Append "" to make sure it's a string, not undefined + remark.removeClass("loading").text(comment+""); + // Update internal data + self.context.data.remark = comment+""; + } + }, + this,true + ).sendRequest(); + }, + '',self.egw().lang("Comment"),self.context.data.remark||'' + ); + + }); + this.context.addItem("file_info", this.egw().lang("File information"), this.egw().image("edit"), function(menu_item) + { + var link_data = self.context.data; + if(link_data.app == 'file') + { + // File info is always the same + var url = '/apps/'+link_data.app2+'/'+link_data.id2+'/'+decodeURIComponent(link_data.id); + if(typeof url == 'string' && url.indexOf('webdav.php')) + { + // URL is url to file in webdav, so get rid of that part + url = url.replace('/webdav.php', ''); + } + self.egw().open(url, "filemanager", "edit"); + } + }); + this.context.addItem("-", "-"); + this.context.addItem("save", this.egw().lang("Save as"), this.egw().image('save'), function(menu_item) + { + var link_data = self.context.data; + // Download file + if(link_data.download_url) + { + var url = link_data.download_url; + if (url[0] == '/') url = egw.link(url); + + let a = document.createElement('a'); + if(typeof a.download == "undefined") + { + window.location.href = url+"?download"; + return false; + } + + // Multiple file download for those that support it + a = jQuery(a) + .prop('href', url) + .prop('download', link_data.title || "") + .appendTo(self.getInstanceManager().DOMContainer); + + var evt = document.createEvent('MouseEvent'); + evt.initMouseEvent('click', true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 0, null); + a[0].dispatchEvent(evt); + a.remove(); + return false; + } + + self.egw().open(link_data, "", "view",'download',link_data.target ? link_data.target : link_data.app,link_data.app); + }); + this.context.addItem("zip", this.egw().lang("Save as Zip"), this.egw().image('save_zip'), function(menu_item) + { + // Highlight files for nice UI indicating what will be in the zip. + // Files have negative IDs. + jQuery('[id^="link_-"]',this.list).effect('highlight',{},2000); + + // Download ZIP + window.location = self.egw().link('/index.php',{ + menuaction: 'api.EGroupware\\Api\\Etemplate\\Widget\\Link.download_zip', + app: self.value.to_app, + id: self.value.to_id + }); + }); + this.context.addItem("-", "-"); + this.context.addItem("delete", this.egw().lang("Delete link"), this.egw().image("delete"), function(menu_item) + { + var link_id = isNaN(self.context.data.link_id) ? self.context.data : self.context.data.link_id; + var row = jQuery('#link_'+(self.context.data.dom_id ? self.context.data.dom_id : self.context.data.link_id), self.list); + et2_dialog.show_dialog( + function(button) { if(button == et2_dialog.YES_BUTTON) self._delete_link(link_id,row);}, + egw.lang('Delete link?') + ); + }); + + // Native DnD - Doesn't play nice with jQueryUI Sortable + // Tell jQuery to include this property + jQuery.event.props.push('dataTransfer'); + + } + + destroy( ) + { + super.destroy.apply(this, arguments); + if(this.context) + { + this.context.clear(); + delete this.context; + } + } + + set_value(_value) + { + this.list.empty(); + // Handle server passed a list of links that aren't ready yet + if(_value && typeof _value == "object") + { + var list = []; + if(_value.to_id && typeof _value.to_id == "object") + { + list = _value.to_id; + } + else if (_value.length) + { + list = _value; + } + if(list.length > 0) + { + for(var id in list) + { + var link = list[id]; + if(link.app) + { + // Temp IDs can cause problems since the ID includes the file name or : + if(link.link_id && typeof link.link_id != 'number') + { + link.dom_id = 'temp_'+egw.uid(); + } + // Icon should be in registry + if(!link.icon) + { + link.icon = egw.link_get_registry(link.app,'icon'); + // No icon, try by mime type - different place for un-saved entries + if(link.icon == false && link.id.type) + { + // Triggers icon by mime type, not thumbnail or app + link.type = link.id.type; + link.icon = true; + } + } + // Special handling for file - if not existing, we can't ask for title + if(typeof link.id =='object' && !link.title) + { + link.title = link.id.name || ''; + } + this._add_link(link); + } + } + } + else + { + super.set_value(_value); + } + } + } + + _add_link( _link_data) + { + var row = jQuery(document.createElement("tr")) + .attr("id", "link_"+(_link_data.dom_id ? _link_data.dom_id : (typeof _link_data.link_id == "string" ? _link_data.link_id.replace(/[:\.]/g,'_'):_link_data.link_id ||_link_data.id))) + .attr("draggable", _link_data.app == 'file' ? "true" : "") + .appendTo(this.list); + if(!_link_data.link_id) + { + for(var k in _link_data) + { + row[0].dataset[k] = _link_data[k]; + } + } + + // Icon + var icon = jQuery(document.createElement("td")) + .appendTo(row) + .addClass("icon"); + if(_link_data.icon) + { + var icon_widget = et2_createWidget("image", {}); + var src = ''; + // Creat a mime widget if the link has type + if(_link_data.type) + { + // VFS - file + var vfs_widget = et2_createWidget('vfs-mime', {}); + vfs_widget.set_value({ + download_url:_link_data.download_url, + name:_link_data.title, + mime:_link_data.type, + path:_link_data.icon + }); + icon.append(vfs_widget.getDOMNode()); + } + else + { + src = this.egw().image(_link_data.icon); + if(src) icon_widget.set_src(src); + icon.append(icon_widget.getDOMNode()); + } + } + + var columns = ['title','remark']; + + var self = this; + for(var i = 0; i < columns.length; i++) + { + var $td = jQuery(document.createElement("td")) + .appendTo(row) + .addClass(columns[i]) + .text(_link_data[columns[i]] ? _link_data[columns[i]]+"" : ""); + + var dirs = _link_data[columns[i]] ? _link_data[columns[i]].split('/') : []; + if(columns[i] == 'title' && _link_data.type && dirs.length > 1) + { + this._format_vfs($td, dirs, _link_data); + } + //Bind the click handler if there is download_url + if (_link_data && (typeof _link_data.download_url != 'undefined' || _link_data.app !='egw-data')) + { + $td.click( function(){ + var fe_mime = egw_get_file_editor_prefered_mimes(_link_data.type); + // Check if the link entry is mime with media type, in order to open it in expose view + if (typeof _link_data.type != 'undefined' && + (_link_data.type.match(self.mime_regexp,'ig') || (fe_mime && fe_mime.mime[_link_data.type]))) + { + var $vfs_img_node = jQuery(this).parent().find('.vfsMimeIcon'); + if ($vfs_img_node.length > 0) $vfs_img_node.click(); + } + else + { + if( !self.options.target_app ) + { + self.options.target_app = _link_data.app; + } + self.egw().open(_link_data, "", "view",null,_link_data.target ? _link_data.target : _link_data.app,self.options.target_app); + } + }); + } + } + + if (typeof _link_data.title == 'undefined') + { + // Title will be fetched from server and then set + jQuery('td.title',row).addClass("loading"); + var title = this.egw().link_title(_link_data.app, _link_data.id, function(title) + { + jQuery('td.title',this).removeClass("loading").text(title+""); + }, row); + } + // Date + /* + var date_row = jQuery(document.createElement("td")) + .appendTo(row); + if(_link_data.lastmod) + { + var date_widget = et2_createWidget("date-since"); + date_widget.set_value(_link_data.lastmod); + date_row.append(date_widget.getDOMNode()); + } + */ + + // Delete + // build delete button if the link is not readonly + if (!this.options.readonly) + { + var delete_button = jQuery(document.createElement("td")) + .appendTo(row); + jQuery("
          ") + .appendTo(delete_button) + // We don't use ui-icon because it assigns a bg image + .addClass("delete icon") + .bind( 'click', function() + { + et2_dialog.show_dialog( + function(button) { + if(button == et2_dialog.YES_BUTTON) + { + self._delete_link( + self.value && typeof self.value.to_id != 'object' && _link_data.link_id ? _link_data.link_id:_link_data, + row + ); + } + }, + egw.lang('Delete link?') + ); + }); + } + // Context menu + row.bind("contextmenu", function(e) + { + // Comment only available if link_id is there and not readonly + self.context.getItem("comment").set_enabled(typeof _link_data.link_id != 'undefined' && !self.options.readonly); + // File info only available for existing files + self.context.getItem("file_info").set_enabled(typeof _link_data.id != 'object' && _link_data.app == 'file'); + self.context.getItem("save").set_enabled(typeof _link_data.id != 'object' && _link_data.app == 'file'); + // Zip download only offered if there are at least 2 files + self.context.getItem("zip").set_enabled(jQuery('[id^="link_-"]',this.list).length >= 2); + // Show delete item only if the widget is not readonly + self.context.getItem("delete").set_enabled(!self.options.readonly); + + self.context.data = _link_data; + self.context.showAt(e.pageX, e.pageY, true); + e.preventDefault(); + }); + + + // Drag - adapted from egw_action_dragdrop, sidestepping action system + // so all linked files get it + // // Unfortunately, dragging files is currently only supported by Chrome + if(navigator && navigator.userAgent.indexOf('Chrome') >= 0) + { + row.on("dragstart", _link_data, function(event) + { + if(event.dataTransfer == null) { + return; + } + var data = event.data || {}; + if(data && data.type && data.download_url) + { + event.dataTransfer.dropEffect="copy"; + event.dataTransfer.effectAllowed="copy"; + + var url = data.download_url; + + // NEED an absolute URL + if (url[0] == '/') url = egw.link(url); + // egw.link adds the webserver, but that might not be an absolute URL - try again + if (url[0] == '/') url = window.location.origin+url; + + // Unfortunately, dragging files is currently only supported by Chrome + if(navigator && navigator.userAgent.indexOf('Chrome')) + { + event.dataTransfer.setData("DownloadURL", data.type+':'+data.title+':'+url); + } + + // Include URL as a fallback + event.dataTransfer.setData("text/uri-list", url); + } + + if(event.dataTransfer.types.length == 0) + { + // No file data? Abort: drag does nothing + event.preventDefault(); + return; + } + //event.dataTransfer.setDragImage(event.delegate.target,0,0); + var div = jQuery(document.createElement("div")) + .attr('id', 'drag_helper') + .css({ + position: 'absolute', + top: '0px', + left: '0px', + width: '300px' + }); + div.append(event.target.cloneNode(true)); + + self.list.append(div); + + event.dataTransfer.setDragImage(div.get(0),0,0); + }) + .on('drag', function() + { + jQuery('#drag_helper',self.list).remove(); + }); + } + } + _delete_link( link_id, row) + { + if(row) + { + var delete_button = jQuery('.delete',row); + delete_button.removeClass("delete").addClass("loading"); + row.off(); + } + if(this.onchange) + { + this.onchange(this,link_id,row); + } + if(typeof link_id != "object") + { + egw.json("EGroupware\\Api\\Etemplate\\Widget\\Link::ajax_delete", [link_id], + function(data) { if(data) {row.slideUp(row.remove);}} + ).sendRequest(); + } + else if (row) + { + // No link ID means a link on an unsaved entry. + // Just remove the row, but need to adjust the link_to value also + row.slideUp(row.remove); + + // Look for a link-to with the same ID, refresh it + if(link_id.link_id) + { + var self = this; + var _widget = link_id.widget || null; + this.getRoot().iterateOver( + function(widget) { + if(widget.id == self.id) + { + _widget = widget; + } + }, + this, et2_link_to + ); + var value = _widget != null ? _widget.getValue() : false; + if(_widget && value && value.to_id) + { + delete value.to_id[link_id.link_id]; + _widget.set_value(value); + } + } + } + } + + /** + * When the link is to a VFS file, we do some special formatting. + * + * Instead of listing the full path, we use + * Path: - filename + * When multiple files from the same directory are linked, we exclude + * the directory name from all but the first link to that directory + * + * @param {JQuery} $td Current table data cell for the title + * @param {String[]} dirs List of directories in the linked file's path + * @param {String[]} _link_data Data for the egw_link + * @returns {undefined} + */ + _format_vfs($td, dirs, _link_data) + { + // Keep it here for matching next row + $td.attr('data-title', _link_data['title']); + + // VFS link - check for same dir as above, and hide dir + var reformat = false; + var span_size = 1; + var prev = jQuery('td.title',$td.parent().prev('tr')); + if(prev.length === 1) + { + var prev_dirs = (prev.attr('data-title') || '').split('/'); + if(prev_dirs.length > 1 && prev_dirs.length == dirs.length) + { + for(var i = 0; i < dirs.length; i++) + { + // Current is same as prev, blank it + if(dirs[i] === prev_dirs[i]) + { + reformat = true; + span_size += dirs[i].length+1; + dirs[i] = ''; + } + else + { + break; + } + } + } + } + var filename = dirs.pop(); + if(reformat && (dirs.length - i) === 0) + { + $td.html(' - '+filename); + } + else + { + // Different format for directory + span_size += dirs.join('/').length+1; + $td.html(''+dirs.join('/')+': - ' + filename); + } + } +} +et2_register_widget(et2_link_list, ["link-list"]); + + +/** + * + * + */ +export class et2_link_add extends et2_inputWidget +{ + static readonly _attributes : any = { + "value": { + "description": "Either an array of link information (see egw_link::link()) or array with keys to_app and to_id", + "type": "any" + }, + "application": { + "name": "Application", + "type": "string", + "default": "", + "description": "Limit to the listed application or applications (comma seperated)" + } + }; + private span: JQuery; + private div: JQuery; + private app_select: et2_link_apps; + private button: et2_button; + /** + * Constructor + */ + constructor(_parent : et2_widget, _attrs? : WidgetConfig, _child? : object) + { + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_link_add._attributes, _child || {})); + + + this.span = jQuery(document.createElement("span")) + .text(this.egw().lang("Add new")) + .addClass('et2_link_add_span'); + this.div = jQuery(document.createElement("div")).append(this.span); + this.setDOMNode(this.div[0]); + } + doLoadingFinished( ) + { + super.doLoadingFinished.apply(this, arguments); + if(this.app_select && this.button) + { + // Already done + return false; + } + this.app_select = et2_createWidget("link-apps", jQuery.extend({},this.options,{ + 'id': this.options.id + 'app', + value: this.options.application ? this.options.application : this.options.value && this.options.value.add_app ? this.options.value.add_app : null, + application_list: this.options.application ? this.options.application : null + }) ,this); + this.div.append(this.app_select.getDOMNode()); + this.button = et2_createWidget("button", {id:this.options.id+"_add",label: this.egw().lang("add")}, this); + this.button.set_label(this.egw().lang("add")); + var self = this; + this.button.click = function() + { + self.egw().open(self.options.value.to_app + ":" + self.options.value.to_id, self.app_select.get_value(), 'add'); + return false; + }; + this.div.append(this.button.getDOMNode()); + + return true; + } + /** + * Should be handled client side. + * Return null to avoid overwriting other link values, in case designer used the same ID for multiple widgets + */ + getValue( ) + { + return null; + } +} +et2_register_widget(et2_link_add, ["link-add"]); diff --git a/api/js/etemplate/et2_widget_number.js b/api/js/etemplate/et2_widget_number.js index 5a3358510e..26603414d6 100644 --- a/api/js/etemplate/et2_widget_number.js +++ b/api/js/etemplate/et2_widget_number.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - JS Number object * @@ -6,170 +7,176 @@ * @subpackage api * @link http://www.egroupware.org * @author Nathan Gray - * @copyright Nathan Gray 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_widget_textbox; + et2_widget_textbox; */ - +var et2_widget_textbox_1 = require("./et2_widget_textbox"); +var et2_core_widget_1 = require("./et2_core_widget"); +var et2_core_inheritance_1 = require("./et2_core_inheritance"); /** * Class which implements the "int" and textbox type=float XET-Tags * * @augments et2_textbox */ -var et2_number = (function(){ "use strict"; return et2_textbox.extend( -{ - attributes: { - "value": { - "type": "float" - }, - // Override default width, numbers are usually shorter - "size": { - "default": 5 - }, - "min": { - "name": "Minimum", - "type": "integer", - "default": et2_no_init, - "description": "Minimum allowed value" - }, - "max": { - "name": "Maximum", - "type": "integer", - "default": et2_no_init, - "description": "Maximum allowed value" - }, - "precision": { - // TODO: Implement this in some nice way other than HTML5's step attribute - "name": "Precision", - "type": "integer", - "default": et2_no_init, - "description": "Allowed precision - # of decimal places", - "ignore": true - } - }, - - /** - * Constructor - * - * @memberOf et2_number - */ - init: function() { - this._super.apply(this, arguments); - }, - - transformAttributes: function(_attrs) { - this._super.apply(this, arguments); - - if (typeof _attrs.validator == 'undefined') - { - _attrs.validator = _attrs.type == 'float' ? '/^-?[0-9]*[,.]?[0-9]*$/' : '/^-?[0-9]*$/'; - } - }, - - /** - * Clientside validation using regular expression in "validator" attribute - * - * @param {array} _messages - */ - isValid: function(_messages) - { - var ok = true; - // if we have a html5 validation error, show it, as this.input.val() will be empty! - if (this.input && this.input[0] && this.input[0].validationMessage && !this.input[0].validity.stepMismatch) - { - _messages.push(this.input[0].validationMessage); - ok = false; - } - return this._super.apply(this, arguments) && ok; - }, - - createInputWidget: function() { - this.input = jQuery(document.createElement("input")); - this.input.attr("type", "number"); - this.input.addClass("et2_textbox"); - // bind invalid event to change, to trigger our validation - this.input.on('invalid', jQuery.proxy(this.change, this)); - if (this.options.onkeypress && typeof this.options.onkeypress == 'function') - { - var self = this; - this.input.keypress(function(_ev) - { - return self.options.onkeypress.call(this, _ev, self); - }); - } - this.setDOMNode(this.input[0]); - }, - - /** - * Set input widget size - * - * Overwritten from et2_textbox as input type=number seems to ignore size, - * therefore we set width in em instead, if not et2_fullWidth given. - * - * @param _size Rather arbitrary size units, approximately characters - */ - set_size: function(_size) { - if (typeof _size != 'undefined' && _size != this.input.attr("size")) - { - this.size = _size; - this.input.attr("size", this.size); - - if (typeof this.options.class == 'undefined' || this.options.class.search('et2_fullWidth') == -1) - { - this.input.css('width', _size+'em'); - } - } - }, - - set_min: function(_value) { - this.min = _value; - if(this.min == null) { - this.input.removeAttr("min"); - } else { - this.input.attr("min",this.min); - } - }, - set_max: function(_value) { - this.max = _value; - if(this.max == null) { - this.input.removeAttr("max"); - } else { - this.input.attr("max",this.max); - } - } -});}).call(this); -et2_register_widget(et2_number, ["int", "integer", "float"]); - +var et2_number = /** @class */ (function (_super) { + __extends(et2_number, _super); + /** + * Constructor + * + * @memberOf et2_number + */ + function et2_number(_parent, _attrs, _child) { + var _this = _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_number._attributes, _child || {})) || this; + _this.min = null; + _this.max = null; + return _this; + } + et2_number.prototype.transformAttributes = function (_attrs) { + _super.prototype.transformAttributes.call(this, _attrs); + if (typeof _attrs.validator == 'undefined') { + _attrs.validator = _attrs.type == 'float' ? '/^-?[0-9]*[,.]?[0-9]*$/' : '/^-?[0-9]*$/'; + } + }; + /** + * Clientside validation using regular expression in "validator" attribute + * + * @param {array} _messages + */ + et2_number.prototype.isValid = function (_messages) { + var ok = true; + // if we have a html5 validation error, show it, as this.input.val() will be empty! + if (this.input && this.input[0] && this.input[0].validationMessage && !this.input[0].validity.stepMismatch) { + _messages.push(this.input[0].validationMessage); + ok = false; + } + return _super.prototype.isValid.call(this, _messages) && ok; + }; + et2_number.prototype.createInputWidget = function () { + this.input = jQuery(document.createElement("input")); + this.input.attr("type", "number"); + this.input.addClass("et2_textbox"); + // bind invalid event to change, to trigger our validation + this.input.on('invalid', jQuery.proxy(this.change, this)); + if (this.options.onkeypress && typeof this.options.onkeypress == 'function') { + var self = this; + this.input.keypress(function (_ev) { + return self.options.onkeypress.call(this, _ev, self); + }); + } + this.setDOMNode(this.input[0]); + }; + /** + * Set input widget size + * + * Overwritten from et2_textbox as input type=number seems to ignore size, + * therefore we set width in em instead, if not et2_fullWidth given. + * + * @param _size Rather arbitrary size units, approximately characters + */ + et2_number.prototype.set_size = function (_size) { + if (typeof _size != 'undefined' && _size != this.input.attr("size")) { + this.size = _size; + this.input.attr("size", this.size); + if (typeof this.options.class == 'undefined' || this.options.class.search('et2_fullWidth') == -1) { + this.input.css('width', _size + 'em'); + } + } + }; + et2_number.prototype.set_min = function (_value) { + this.min = _value; + if (this.min == null) { + this.input.removeAttr("min"); + } + else { + this.input.attr("min", this.min); + } + }; + et2_number.prototype.set_max = function (_value) { + this.max = _value; + if (this.max == null) { + this.input.removeAttr("max"); + } + else { + this.input.attr("max", this.max); + } + }; + et2_number._attributes = { + "value": { + "type": "float" + }, + // Override default width, numbers are usually shorter + "size": { + "default": 5 + }, + "min": { + "name": "Minimum", + "type": "integer", + "default": et2_no_init, + "description": "Minimum allowed value" + }, + "max": { + "name": "Maximum", + "type": "integer", + "default": et2_no_init, + "description": "Maximum allowed value" + }, + "precision": { + // TODO: Implement this in some nice way other than HTML5's step attribute + "name": "Precision", + "type": "integer", + "default": et2_no_init, + "description": "Allowed precision - # of decimal places", + "ignore": true + } + }; + return et2_number; +}(et2_widget_textbox_1.et2_textbox)); +et2_core_widget_1.et2_register_widget(et2_number, ["int", "integer", "float"]); /** * Extend read-only to tell it to ignore special attributes, which * would cause warnings otherwise * @augments et2_textbox_ro * @class */ -var et2_number_ro = (function(){ "use strict"; return et2_textbox_ro.extend( -{ - attributes: { - min: { ignore: true}, - max: { ignore: true}, - precision: { - name: "Precision", - type: "integer", - default: et2_no_init, - description: "Allowed precision - # of decimal places", - ignore: true - }, - value: { type: "float" } - }, - set_value: function(_value) - { - if (typeof this.options.precision != 'undefined' && ""+_value != "") - { - _value = parseFloat(_value).toFixed(this.options.precision); - } - this._super.call(this, _value); - } -});}).call(this); -et2_register_widget(et2_number_ro, ["int_ro", "integer_ro", "float_ro"]); +var et2_number_ro = /** @class */ (function (_super) { + __extends(et2_number_ro, _super); + function et2_number_ro() { + return _super !== null && _super.apply(this, arguments) || this; + } + et2_number_ro.prototype.set_value = function (_value) { + if (typeof this.options.precision != 'undefined' && "" + _value != "") { + _value = parseFloat(_value).toFixed(this.options.precision); + } + _super.prototype.set_value.call(this, _value); + }; + et2_number_ro._attributes = { + min: { ignore: true }, + max: { ignore: true }, + precision: { + name: "Precision", + type: "integer", + default: et2_no_init, + description: "Allowed precision - # of decimal places", + ignore: true + }, + value: { type: "float" } + }; + return et2_number_ro; +}(et2_textbox_ro)); +et2_core_widget_1.et2_register_widget(et2_number_ro, ["int_ro", "integer_ro", "float_ro"]); +//# sourceMappingURL=et2_widget_number.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_number.ts b/api/js/etemplate/et2_widget_number.ts new file mode 100644 index 0000000000..330762c97a --- /dev/null +++ b/api/js/etemplate/et2_widget_number.ts @@ -0,0 +1,186 @@ +/** + * EGroupware eTemplate2 - JS Number object + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage api + * @link http://www.egroupware.org + * @author Nathan Gray + */ + +/*egw:uses + et2_widget_textbox; +*/ + +import {et2_textbox} from "./et2_widget_textbox"; +import {et2_register_widget, WidgetConfig} from "./et2_core_widget"; +import {ClassWithAttributes} from "./et2_core_inheritance"; + +/** + * Class which implements the "int" and textbox type=float XET-Tags + * + * @augments et2_textbox + */ +class et2_number extends et2_textbox +{ + static readonly _attributes : any = { + "value": { + "type": "float" + }, + // Override default width, numbers are usually shorter + "size": { + "default": 5 + }, + "min": { + "name": "Minimum", + "type": "integer", + "default": et2_no_init, + "description": "Minimum allowed value" + }, + "max": { + "name": "Maximum", + "type": "integer", + "default": et2_no_init, + "description": "Maximum allowed value" + }, + "precision": { + // TODO: Implement this in some nice way other than HTML5's step attribute + "name": "Precision", + "type": "integer", + "default": et2_no_init, + "description": "Allowed precision - # of decimal places", + "ignore": true + } + }; + + min : number = null; + max : number = null; + /** + * Constructor + * + * @memberOf et2_number + */ + constructor(_parent?, _attrs? : WidgetConfig, _child? : object) { + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_number._attributes, _child || {})); + } + + transformAttributes(_attrs) + { + super.transformAttributes(_attrs); + + if (typeof _attrs.validator == 'undefined') + { + _attrs.validator = _attrs.type == 'float' ? '/^-?[0-9]*[,.]?[0-9]*$/' : '/^-?[0-9]*$/'; + } + } + + /** + * Clientside validation using regular expression in "validator" attribute + * + * @param {array} _messages + */ + isValid(_messages) + { + let ok = true; + // if we have a html5 validation error, show it, as this.input.val() will be empty! + if (this.input && this.input[0] && (this.input[0]).validationMessage && !(this.input[0]).validity.stepMismatch) + { + _messages.push((this.input[0]).validationMessage); + ok = false; + } + return super.isValid(_messages) && ok; + } + + createInputWidget() + { + this.input = jQuery(document.createElement("input")); + this.input.attr("type", "number"); + this.input.addClass("et2_textbox"); + // bind invalid event to change, to trigger our validation + this.input.on('invalid', jQuery.proxy(this.change, this)); + if (this.options.onkeypress && typeof this.options.onkeypress == 'function') + { + var self = this; + this.input.keypress(function(_ev) + { + return self.options.onkeypress.call(this, _ev, self); + }); + } + this.setDOMNode(this.input[0]); + } + + /** + * Set input widget size + * + * Overwritten from et2_textbox as input type=number seems to ignore size, + * therefore we set width in em instead, if not et2_fullWidth given. + * + * @param _size Rather arbitrary size units, approximately characters + */ + set_size(_size) + { + if (typeof _size != 'undefined' && _size != this.input.attr("size")) + { + this.size = _size; + this.input.attr("size", this.size); + + if (typeof this.options.class == 'undefined' || this.options.class.search('et2_fullWidth') == -1) + { + this.input.css('width', _size+'em'); + } + } + } + + set_min(_value) + { + this.min = _value; + if(this.min == null) { + this.input.removeAttr("min"); + } else { + this.input.attr("min",this.min); + } + } + set_max(_value) + { + this.max = _value; + if(this.max == null) { + this.input.removeAttr("max"); + } else { + this.input.attr("max",this.max); + } + } +} +et2_register_widget(et2_number, ["int", "integer", "float"]); + +/** + * Extend read-only to tell it to ignore special attributes, which + * would cause warnings otherwise + * @augments et2_textbox_ro + * @class + */ +class et2_number_ro extends et2_textbox_ro +{ + static readonly _attributes : any = { + min: { ignore: true}, + max: { ignore: true}, + precision: { + name: "Precision", + type: "integer", + default: et2_no_init, + description: "Allowed precision - # of decimal places", + ignore: true + }, + value: { type: "float" } + }; + + set_value(_value) + { + if (typeof this.options.precision != 'undefined' && ""+_value != "") + { + _value = parseFloat(_value).toFixed(this.options.precision); + } + super.set_value(_value); + } +} +et2_register_widget(et2_number_ro, ["int_ro", "integer_ro", "float_ro"]); + diff --git a/api/js/etemplate/et2_widget_portlet.js b/api/js/etemplate/et2_widget_portlet.js index e6f3062f17..9ad2250f0c 100644 --- a/api/js/etemplate/et2_widget_portlet.js +++ b/api/js/etemplate/et2_widget_portlet.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - JS Portlet object - used by Home * @@ -9,12 +10,28 @@ * @author Nathan Gray * @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 /vendor/bower-asset/jquery/dist/jquery.js; et2_core_baseWidget; */ - +var et2_core_widget_1 = require("./et2_core_widget"); +var et2_core_valueWidget_1 = require("./et2_core_valueWidget"); +var et2_core_inheritance_1 = require("./et2_core_inheritance"); +var et2_core_DOMWidget_1 = require("./et2_core_DOMWidget"); /** * Class which implements the UI of a Portlet * @@ -26,379 +43,320 @@ * @link http://docs.oasis-open.org/wsrp/v2/wsrp-2.0-spec-os-01.html * @augments et2_baseWidget */ -var et2_portlet = (function(){ "use strict"; return et2_valueWidget.extend( -{ - attributes: { - "title": { - "name": "Title", - "description": "Goes in the little bit at the top with the icons", - "type": "string", - "default": "" - }, - "edit_template": { - "name": "Edit template", - "description": "Custom eTemplate used to customize / set up the portlet", - "type": "string", - "default": window.egw_webserverUrl+"/home/templates/default/edit.xet" - }, - "color": { - "name": "Color", - "description": "Set the portlet color", - "type": "string", - "default": '' - }, - "settings": { - "name": "Customization settings", - "description": "Array of customization settings, similar in structure to preference settings", - "type": "any", - "default": et2_no_init - }, - "actions": { - default: {} - }, - "width": { "default": 2, "ignore": true}, - "height": { "default": 1, "type": "integer"}, - "rows": {"ignore": true, default: et2_no_init}, - "cols": {"ignore": true, default: et2_no_init}, - "resize_ratio": {"ignore": true, default: et2_no_init}, // Portlets are explicitly sized - "row": { - "name": "Row", - "description": "Home page location (row) - handled by home app", - "default": 1 - }, - "col": { - "name": "Column", - "description": "Home page location(column) - handled by home app", - "default": 1 - } - }, - - createNamespace: true, - GRID: 55, - - /** - * These are the "normal" actions that every portlet is expected to have. - * The widget provides default actions for all of these, but they can - * be added to or overridden if needed by setting the action attribute. - */ - default_actions: { - edit_settings: { - icon: "edit", - caption: "Configure", - "default": true, - hideOnDisabled: true, - group: "portlet" - }, - remove_portlet: { - icon: "delete", - caption: "Remove", - group: "portlet" - } - }, - - /** - * Constructor - * - * @memberOf et2_portlet - */ - init: function() - { - this._super.apply(this, arguments); - - var self = this; - - // Create DOM nodes - this.div = jQuery(document.createElement("div")) - .addClass(this.options.class) - .addClass("ui-widget ui-widget-content ui-corner-all") - .addClass("et2_portlet") - /* Gridster */ - .attr("data-sizex", this.options.width) - .attr("data-sizey", this.options.height) - .attr("data-row", this.options.row) - .attr("data-col", this.options.col) - - .resizable( { - autoHide: true, - grid: this.GRID, - //containment: this.getParent().getDOMNode(), - stop: function(event, ui) { - self.set_width(Math.round(ui.size.width / self.GRID)); - self.set_height(Math.round(ui.size.height / self.GRID)); - self.egw().jsonq("home.home_ui.ajax_set_properties",[self.id, {},{ - width: self.options.width, - height: self.options.height - }], - null, - self, true, self - ); - // Tell children - self.iterateOver(function(widget) {widget.resize();},null,et2_IResizeable); - } - }); - this.header = jQuery(document.createElement("div")) - .attr('id', this.getInstanceManager().uniqueId+'_'+this.id.replace(/\./g, '-') + '_header') - .addClass("ui-widget-header ui-corner-all") - .appendTo(this.div) - .html(this.options.title); - this.content = jQuery(document.createElement("div")) - .attr('id', this.getInstanceManager().uniqueId+'_'+this.id.replace(/\./g, '-') + '_content') - .appendTo(this.div); - - this.setDOMNode(this.div[0]); - }, - - destroy: function() - { - for(var i = 0; i < this._children.length; i++) - { - // Check for child is a different template and clear it, - // since it won't be cleared by destroy() - if(this._children[i]._inst != this._inst) - { - this._children[i]._inst.clear(); - } - } - this._super.apply(this, arguments); - }, - - doLoadingFinished: function() { - this.set_color(this.options.color); - }, - - /** - * If anyone asks, return the content node, so content goes inside - */ - getDOMNode: function(_sender) { - if(typeof _sender != 'undefined' && _sender != this) - { - return this.content[0]; - } - return this._super.apply(this, arguments); - }, - - /** - * Overriden from parent to add in default actions - */ - set_actions: function(actions) - { - // Set targets for actions - var defaults = {}; - for(var action_name in this.default_actions) - { - defaults[action_name] = this.default_actions[action_name]; - // Translate caption here, as translations aren't available earlier - defaults[action_name].caption = this.egw().lang(this.default_actions[action_name].caption); - if(typeof this[action_name] == "function") - { - defaults[action_name].onExecute = jQuery.proxy(this[action_name],this); - } - } - - // Add in defaults, but let provided actions override them - this.options.actions = jQuery.extend(true,{},defaults,actions); - this._super.apply(this, [this.options.actions]); - }, - - /** - * Override _link_actions to remove edit action, if there is no settings - * - * @param Object[ {ID: attributes..}+] as for set_actions - */ - _link_actions: function(actions) - { - // Get the top level element - 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 - )); - } - - // Delete all old objects - widget_object.clear(); - - // Go over the widget & add links - this is where we decide which actions are - // 'allowed' for this widget at this time - var action_links = []; - for(var i in actions) - { - var id = typeof actions[i].id != 'undefined' ? actions[i].id : i; - var action = { - actionId: id, - enabled: true - }; - - // If there are no settings, there can be no customization, so remove the edit action - if(id == 'edit_settings' && (!this.options.settings || jQuery.isEmptyObject(this.options.settings))) - { - this.egw().debug("log", "No settings for portlet %o, edit_settings action removed", this); - action.enabled = false; - } - action_links.push(action); - } - - widget_object.updateActionLinks(action_links); - }, - - /** - * Create & show a dialog for customizing this portlet - * - * Properties for customization are sent in the 'settings' attribute - */ - edit_settings: function(action, sender) - { - var dialog = et2_createWidget("dialog", { - callback: jQuery.proxy(this._process_edit, this), - template: this.options.edit_template, - value: { - content: this.options.settings - }, - buttons: et2_dialog.BUTTONS_OK_CANCEL - },this); - // Set seperately to avoid translation - dialog.set_title(this.egw().lang("Edit") + " " + (this.options.title || '')); - }, - - _process_edit: function(button_id, value) - { - if(button_id != et2_dialog.OK_BUTTON) return; - - - // Save settings - server might reply with new content if the portlet needs an update, - // but ideally it doesn't - this.div.addClass("loading"); - - // Pass updated settings, unless we're removing - var settings = (typeof value == 'string') ? {} : this.options.settings || {} - this.egw().jsonq("home.home_ui.ajax_set_properties",[this.id, settings, value,this.settings?this.settings.group:false], - function(data) { - // This section not for us - if(!data || typeof data.attributes == 'undefined') return false; - - this.div.removeClass("loading"); - this.set_value(data.content); - for(var key in data.attributes) - { - if(typeof this["set_"+key] == "function") - { - this["set_"+key].call(this, data.attributes[key]); - } - else if (this.attributes[key]) - { - this.options[key] = data.attributes[key]; - } - } - - // Flagged as needing to edit settings? Open dialog - if(typeof data.edit_settings != 'undefined' && data.edit_settings) - { - this.edit_settings(); - } - - // Only resize once, and only if needed - if(data.attributes.width || data.attributes.height) - { - // Tell children - try { - this.iterateOver(function(widget) {widget.resize();},null,et2_IResizeable); - } catch (e) { - // Something went wrong, but do not stop - egw.debug('warn',e,this); - } - } - }, - this, true, this - ); - - // Extend, not replace, because settings has types while value has just value - if(typeof value == 'object') - { - jQuery.extend(this.options.settings, value); - } - }, - - /** - * Remove this portlet from the home page - */ - remove_portlet: function() { - var self = this; - et2_dialog.show_dialog(function(button_id) { - if(button_id != et2_dialog.OK_BUTTON) return; - self._process_edit(button_id, '~remove~'); - self._parent.removeChild(self); - self.destroy(); - },this.egw().lang("Remove"), this.options.title,{}, - et2_dialog.BUTTONS_OK_CANCEL, et2_dialog.QUESTION_MESSAGE - ); - }, - - /** - * Set the HTML content of the portlet - * - * @param value String HTML fragment - */ - set_value: function(value) - { - this.content.html(value); - }, - - /** - * Set the content of the header - * - * @param value String HTML fragment - */ - set_title: function(value) - { - this.header.contents() - .filter(function() { - return this.nodeType === 3; - }) - .remove(); - this.options.title = value; - this.header.append(value); - }, - - /** - * Let this portlet stand out a little by allowing a custom color - */ - set_color: function(color) - { - this.options.color = color; - this.header.css("backgroundColor", color); - this.header.css('color', jQuery.Color(this.header.css("backgroundColor")).lightness() > 0.5 ? 'black':'white'); - this.content.css("backgroundColor", color); - }, - - /** - * Set the number of grid cells this widget spans - * - * @param value int Number of horizontal grid cells - */ - set_width: function(value) - { - this.options.width = value; - this.div.attr("data-sizex", value); - // Clear what's there from jQuery, we get width from CSS according to sizex - this.div.css('width',''); - }, - - /** - * Set the number of vertical grid cells this widget spans - * - * @param value int Number of vertical grid cells - */ - set_height: function(value) - { - this.options.height = value; - this.div.attr("data-sizey", value); - // Clear what's there from jQuery, we get width from CSS according to sizey - this.div.css('height',''); - } - -});}).call(this); -et2_register_widget(et2_portlet, ["portlet"]); +var et2_portlet = /** @class */ (function (_super) { + __extends(et2_portlet, _super); + /** + * Constructor + * + * @memberOf et2_portlet + */ + function et2_portlet(_parent, _attrs, _child) { + var _this = + // Call the inherited constructor + _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_portlet._attributes, _child || {})) || this; + _this.GRID = 55; + /** + * These are the "normal" actions that every portlet is expected to have. + * The widget provides default actions for all of these, but they can + * be added to or overridden if needed by setting the action attribute. + */ + _this.default_actions = { + edit_settings: { + icon: "edit", + caption: "Configure", + "default": true, + hideOnDisabled: true, + group: "portlet" + }, + remove_portlet: { + icon: "delete", + caption: "Remove", + group: "portlet" + } + }; + var self = _this; + // Create DOM nodes + _this.div = jQuery(document.createElement("div")) + .addClass(_this.options.class) + .addClass("ui-widget ui-widget-content ui-corner-all") + .addClass("et2_portlet") + /* Gridster */ + .attr("data-sizex", _this.options.width) + .attr("data-sizey", _this.options.height) + .attr("data-row", _this.options.row) + .attr("data-col", _this.options.col) + .resizable({ + autoHide: true, + grid: _this.GRID, + //containment: this.getParent().getDOMNode(), + stop: function (event, ui) { + self.set_width(Math.round(ui.size.width / self.GRID)); + self.set_height(Math.round(ui.size.height / self.GRID)); + self.egw().jsonq("home.home_ui.ajax_set_properties", [self.id, {}, { + width: self.options.width, + height: self.options.height + }], null, self); + // Tell children + self.iterateOver(function (widget) { widget.resize(); }, null, et2_IResizeable); + } + }); + _this.header = jQuery(document.createElement("div")) + .attr('id', _this.getInstanceManager().uniqueId + '_' + _this.id.replace(/\./g, '-') + '_header') + .addClass("ui-widget-header ui-corner-all") + .appendTo(_this.div) + .html(_this.options.title); + _this.content = jQuery(document.createElement("div")) + .attr('id', _this.getInstanceManager().uniqueId + '_' + _this.id.replace(/\./g, '-') + '_content') + .appendTo(_this.div); + _this.setDOMNode(_this.div[0]); + return _this; + } + et2_portlet.prototype.destroy = function () { + for (var i = 0; i < this._children.length; i++) { + // Check for child is a different template and clear it, + // since it won't be cleared by destroy() + if (this._children[i].getInstanceManager() != this.getInstanceManager()) { + this._children[i].getInstanceManager().clear(); + } + } + _super.prototype.destroy.call(this); + }; + et2_portlet.prototype.doLoadingFinished = function () { + this.set_color(this.options.color); + return true; + }; + /** + * If anyone asks, return the content node, so content goes inside + */ + et2_portlet.prototype.getDOMNode = function (_sender) { + if (typeof _sender != 'undefined' && _sender != this) { + return this.content[0]; + } + return _super.prototype.getDOMNode.call(this, _sender); + }; + /** + * Overriden from parent to add in default actions + */ + et2_portlet.prototype.set_actions = function (actions) { + // Set targets for actions + var defaults = {}; + for (var action_name in this.default_actions) { + defaults[action_name] = this.default_actions[action_name]; + // Translate caption here, as translations aren't available earlier + defaults[action_name].caption = this.egw().lang(this.default_actions[action_name].caption); + if (typeof this[action_name] == "function") { + defaults[action_name].onExecute = jQuery.proxy(this[action_name], this); + } + } + // Add in defaults, but let provided actions override them + this.options.actions = jQuery.extend(true, {}, defaults, actions); + _super.prototype.set_actions.call(this, [this.options.actions]); + }; + /** + * Override _link_actions to remove edit action, if there is no settings + * + * @param actions + */ + et2_portlet.prototype._link_actions = function (actions) { + // Get the top level element + 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_core_DOMWidget_1.et2_action_object_impl(this).getAOI(), this._actionManager || objectManager.manager.getActionById(this.id) || objectManager.manager)); + } + // Delete all old objects + widget_object.clear(); + // Go over the widget & add links - this is where we decide which actions are + // 'allowed' for this widget at this time + var action_links = []; + for (var i in actions) { + var id = typeof actions[i].id != 'undefined' ? actions[i].id : i; + var action = { + actionId: id, + enabled: true + }; + // If there are no settings, there can be no customization, so remove the edit action + if (id == 'edit_settings' && (!this.options.settings || jQuery.isEmptyObject(this.options.settings))) { + this.egw().debug("log", "No settings for portlet %o, edit_settings action removed", this); + action.enabled = false; + } + action_links.push(action); + } + widget_object.updateActionLinks(action_links); + }; + /** + * Create & show a dialog for customizing this portlet + * + * Properties for customization are sent in the 'settings' attribute + */ + et2_portlet.prototype.edit_settings = function () { + var dialog = et2_createWidget("dialog", { + callback: jQuery.proxy(this._process_edit, this), + template: this.options.edit_template, + value: { + content: this.options.settings + }, + buttons: et2_dialog.BUTTONS_OK_CANCEL + }, this); + // Set seperately to avoid translation + dialog.set_title(this.egw().lang("Edit") + " " + (this.options.title || '')); + }; + et2_portlet.prototype._process_edit = function (button_id, value) { + if (button_id != et2_dialog.OK_BUTTON) + return; + // Save settings - server might reply with new content if the portlet needs an update, + // but ideally it doesn't + this.div.addClass("loading"); + // Pass updated settings, unless we're removing + var settings = (typeof value == 'string') ? {} : this.options.settings || {}; + this.egw().jsonq("home.home_ui.ajax_set_properties", [this.id, settings, value, this.settings ? this.settings.group : false], function (data) { + // This section not for us + if (!data || typeof data.attributes == 'undefined') + return false; + this.div.removeClass("loading"); + this.set_value(data.content); + for (var key in data.attributes) { + if (typeof this["set_" + key] == "function") { + this["set_" + key].call(this, data.attributes[key]); + } + else if (this.attributes[key]) { + this.options[key] = data.attributes[key]; + } + } + // Flagged as needing to edit settings? Open dialog + if (typeof data.edit_settings != 'undefined' && data.edit_settings) { + this.edit_settings(); + } + // Only resize once, and only if needed + if (data.attributes.width || data.attributes.height) { + // Tell children + try { + this.iterateOver(function (widget) { widget.resize(); }, null, et2_IResizeable); + } + catch (e) { + // Something went wrong, but do not stop + egw.debug('warn', e, this); + } + } + }, this); + // Extend, not replace, because settings has types while value has just value + if (typeof value == 'object') { + jQuery.extend(this.options.settings, value); + } + }; + /** + * Remove this portlet from the home page + */ + et2_portlet.prototype.remove_portlet = function () { + var self = this; + et2_dialog.show_dialog(function (button_id) { + if (button_id != et2_dialog.OK_BUTTON) + return; + self._process_edit(button_id, '~remove~'); + self.getParent().removeChild(self); + self.destroy(); + }, this.egw().lang("Remove"), this.options.title, {}, et2_dialog.BUTTONS_OK_CANCEL, et2_dialog.QUESTION_MESSAGE); + }; + /** + * Set the HTML content of the portlet + * + * @param value String HTML fragment + */ + et2_portlet.prototype.set_value = function (value) { + this.content.html(value); + }; + /** + * Set the content of the header + * + * @param value String HTML fragment + */ + et2_portlet.prototype.set_title = function (value) { + this.header.contents() + .filter(function () { + return this.nodeType === 3; + }) + .remove(); + this.options.title = value; + this.header.append(value); + }; + /** + * Let this portlet stand out a little by allowing a custom color + */ + et2_portlet.prototype.set_color = function (color) { + this.options.color = color; + this.header.css("backgroundColor", color); + this.header.css('color', jQuery.Color(this.header.css("backgroundColor")).lightness() > 0.5 ? 'black' : 'white'); + this.content.css("backgroundColor", color); + }; + /** + * Set the number of grid cells this widget spans + * + * @param value int Number of horizontal grid cells + */ + et2_portlet.prototype.set_width = function (value) { + this.options.width = value; + this.div.attr("data-sizex", value); + // Clear what's there from jQuery, we get width from CSS according to sizex + this.div.css('width', ''); + }; + /** + * Set the number of vertical grid cells this widget spans + * + * @param value int Number of vertical grid cells + */ + et2_portlet.prototype.set_height = function (value) { + this.options.height = value; + this.div.attr("data-sizey", value); + // Clear what's there from jQuery, we get width from CSS according to sizey + this.div.css('height', ''); + }; + et2_portlet._attributes = { + "title": { + "name": "Title", + "description": "Goes in the little bit at the top with the icons", + "type": "string", + "default": "" + }, + "edit_template": { + "name": "Edit template", + "description": "Custom eTemplate used to customize / set up the portlet", + "type": "string", + "default": egw.webserverUrl + "/home/templates/default/edit.xet" + }, + "color": { + "name": "Color", + "description": "Set the portlet color", + "type": "string", + "default": '' + }, + "settings": { + "name": "Customization settings", + "description": "Array of customization settings, similar in structure to preference settings", + "type": "any", + "default": et2_no_init + }, + "actions": { + default: {} + }, + "width": { "default": 2, "ignore": true }, + "height": { "default": 1, "type": "integer" }, + "rows": { "ignore": true, default: et2_no_init }, + "cols": { "ignore": true, default: et2_no_init }, + "resize_ratio": { "ignore": true, default: et2_no_init }, + "row": { + "name": "Row", + "description": "Home page location (row) - handled by home app", + "default": 1 + }, + "col": { + "name": "Column", + "description": "Home page location(column) - handled by home app", + "default": 1 + } + }; + return et2_portlet; +}(et2_core_valueWidget_1.et2_valueWidget)); +et2_core_widget_1.et2_register_widget(et2_portlet, ["portlet"]); +//# sourceMappingURL=et2_widget_portlet.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_portlet.ts b/api/js/etemplate/et2_widget_portlet.ts new file mode 100644 index 0000000000..c2cdb15046 --- /dev/null +++ b/api/js/etemplate/et2_widget_portlet.ts @@ -0,0 +1,416 @@ +/** + * EGroupware eTemplate2 - JS Portlet object - used by Home + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package home + * @package etemplate + * @subpackage api + * @link http://www.egroupware.org + * @author Nathan Gray + * @version $Id$ + */ + +/*egw:uses + /vendor/bower-asset/jquery/dist/jquery.js; + et2_core_baseWidget; +*/ + +import {et2_register_widget, WidgetConfig} from "./et2_core_widget"; +import {et2_valueWidget} from "./et2_core_valueWidget"; +import {ClassWithAttributes} from "./et2_core_inheritance"; +import {et2_action_object_impl, et2_DOMWidget} from "./et2_core_DOMWidget"; + +/** + * Class which implements the UI of a Portlet + * + * This manages the frame and decoration, but also provides the UI for properties. + * + * Portlets are only internal to EGroupware. + * + * Home does not fully implement WSRP, but tries not to conflict, ether. + * @link http://docs.oasis-open.org/wsrp/v2/wsrp-2.0-spec-os-01.html + * @augments et2_baseWidget + */ +class et2_portlet extends et2_valueWidget +{ + static readonly _attributes : any = { + "title": { + "name": "Title", + "description": "Goes in the little bit at the top with the icons", + "type": "string", + "default": "" + }, + "edit_template": { + "name": "Edit template", + "description": "Custom eTemplate used to customize / set up the portlet", + "type": "string", + "default": egw.webserverUrl+"/home/templates/default/edit.xet" + }, + "color": { + "name": "Color", + "description": "Set the portlet color", + "type": "string", + "default": '' + }, + "settings": { + "name": "Customization settings", + "description": "Array of customization settings, similar in structure to preference settings", + "type": "any", + "default": et2_no_init + }, + "actions": { + default: {} + }, + "width": { "default": 2, "ignore": true}, + "height": { "default": 1, "type": "integer"}, + "rows": {"ignore": true, default: et2_no_init}, + "cols": {"ignore": true, default: et2_no_init}, + "resize_ratio": {"ignore": true, default: et2_no_init}, // Portlets are explicitly sized + "row": { + "name": "Row", + "description": "Home page location (row) - handled by home app", + "default": 1 + }, + "col": { + "name": "Column", + "description": "Home page location(column) - handled by home app", + "default": 1 + } + }; + + private GRID : number = 55; + private settings: any; + /** + * These are the "normal" actions that every portlet is expected to have. + * The widget provides default actions for all of these, but they can + * be added to or overridden if needed by setting the action attribute. + */ + protected default_actions : any = { + edit_settings: { + icon: "edit", + caption: "Configure", + "default": true, + hideOnDisabled: true, + group: "portlet" + }, + remove_portlet: { + icon: "delete", + caption: "Remove", + group: "portlet" + } + }; + protected div: JQuery; + protected header: JQuery; + protected content: JQuery; + + /** + * Constructor + * + * @memberOf et2_portlet + */ + constructor(_parent, _attrs? : WidgetConfig, _child? : object) + { + // Call the inherited constructor + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_portlet._attributes, _child || {})); + + let self = this; + + // Create DOM nodes + this.div = jQuery(document.createElement("div")) + .addClass(this.options.class) + .addClass("ui-widget ui-widget-content ui-corner-all") + .addClass("et2_portlet") + /* Gridster */ + .attr("data-sizex", this.options.width) + .attr("data-sizey", this.options.height) + .attr("data-row", this.options.row) + .attr("data-col", this.options.col) + + .resizable( { + autoHide: true, + grid: this.GRID, + //containment: this.getParent().getDOMNode(), + stop: function(event, ui) { + self.set_width(Math.round(ui.size.width / self.GRID)); + self.set_height(Math.round(ui.size.height / self.GRID)); + self.egw().jsonq("home.home_ui.ajax_set_properties",[self.id, {},{ + width: self.options.width, + height: self.options.height + }], + null, + self + ); + // Tell children + self.iterateOver(function(widget) {widget.resize();},null,et2_IResizeable); + } + }); + this.header = jQuery(document.createElement("div")) + .attr('id', this.getInstanceManager().uniqueId+'_'+this.id.replace(/\./g, '-') + '_header') + .addClass("ui-widget-header ui-corner-all") + .appendTo(this.div) + .html(this.options.title); + this.content = jQuery(document.createElement("div")) + .attr('id', this.getInstanceManager().uniqueId+'_'+this.id.replace(/\./g, '-') + '_content') + .appendTo(this.div); + + this.setDOMNode(this.div[0]); + } + + destroy() + { + for(let i = 0; i < this._children.length; i++) + { + // Check for child is a different template and clear it, + // since it won't be cleared by destroy() + if(this._children[i].getInstanceManager() != this.getInstanceManager()) + { + this._children[i].getInstanceManager().clear(); + } + } + super.destroy(); + } + + doLoadingFinished() + { + this.set_color(this.options.color); + return true; + } + + /** + * If anyone asks, return the content node, so content goes inside + */ + getDOMNode(_sender) + { + if(typeof _sender != 'undefined' && _sender != this) + { + return this.content[0]; + } + return super.getDOMNode(_sender); + } + + /** + * Overriden from parent to add in default actions + */ + set_actions(actions) + { + // Set targets for actions + let defaults: any = {}; + for(let action_name in this.default_actions) + { + defaults[action_name] = this.default_actions[action_name]; + // Translate caption here, as translations aren't available earlier + defaults[action_name].caption = this.egw().lang(this.default_actions[action_name].caption); + if(typeof this[action_name] == "function") + { + defaults[action_name].onExecute = jQuery.proxy(this[action_name],this); + } + } + + // Add in defaults, but let provided actions override them + this.options.actions = jQuery.extend(true,{},defaults,actions); + super.set_actions([this.options.actions]); + } + + /** + * Override _link_actions to remove edit action, if there is no settings + * + * @param actions + */ + _link_actions(actions) + { + // Get the top level element + let objectManager = egw_getAppObjectManager(true); + let 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).getAOI(), + this._actionManager || (objectManager.manager).getActionById(this.id) || objectManager.manager + )); + } + + // Delete all old objects + widget_object.clear(); + + // Go over the widget & add links - this is where we decide which actions are + // 'allowed' for this widget at this time + let action_links = []; + for(let i in actions) + { + let id = typeof actions[i].id != 'undefined' ? actions[i].id : i; + let action = { + actionId: id, + enabled: true + }; + + // If there are no settings, there can be no customization, so remove the edit action + if(id == 'edit_settings' && (!this.options.settings || jQuery.isEmptyObject(this.options.settings))) + { + this.egw().debug("log", "No settings for portlet %o, edit_settings action removed", this); + action.enabled = false; + } + action_links.push(action); + } + + widget_object.updateActionLinks(action_links); + } + + /** + * Create & show a dialog for customizing this portlet + * + * Properties for customization are sent in the 'settings' attribute + */ + edit_settings() + { + let dialog = et2_createWidget("dialog", { + callback: jQuery.proxy(this._process_edit, this), + template: this.options.edit_template, + value: { + content: this.options.settings + }, + buttons: et2_dialog.BUTTONS_OK_CANCEL + },this); + // Set seperately to avoid translation + dialog.set_title(this.egw().lang("Edit") + " " + (this.options.title || '')); + } + + _process_edit(button_id, value) + { + if(button_id != et2_dialog.OK_BUTTON) return; + + + // Save settings - server might reply with new content if the portlet needs an update, + // but ideally it doesn't + this.div.addClass("loading"); + + // Pass updated settings, unless we're removing + let settings = (typeof value == 'string') ? {} : this.options.settings || {}; + this.egw().jsonq("home.home_ui.ajax_set_properties",[this.id, settings, value, this.settings ? this.settings.group : false], + function(data) { + // This section not for us + if(!data || typeof data.attributes == 'undefined') return false; + + this.div.removeClass("loading"); + this.set_value(data.content); + for(let key in data.attributes) + { + if(typeof this["set_"+key] == "function") + { + this["set_"+key].call(this, data.attributes[key]); + } + else if (this.attributes[key]) + { + this.options[key] = data.attributes[key]; + } + } + + // Flagged as needing to edit settings? Open dialog + if(typeof data.edit_settings != 'undefined' && data.edit_settings) + { + this.edit_settings(); + } + + // Only resize once, and only if needed + if(data.attributes.width || data.attributes.height) + { + // Tell children + try { + this.iterateOver(function(widget) {widget.resize();},null,et2_IResizeable); + } catch (e) { + // Something went wrong, but do not stop + egw.debug('warn',e,this); + } + } + }, + this); + + // Extend, not replace, because settings has types while value has just value + if(typeof value == 'object') + { + jQuery.extend(this.options.settings, value); + } + } + + /** + * Remove this portlet from the home page + */ + remove_portlet() + { + let self = this; + et2_dialog.show_dialog(function(button_id) { + if(button_id != et2_dialog.OK_BUTTON) return; + self._process_edit(button_id, '~remove~'); + self.getParent().removeChild(self); + self.destroy(); + },this.egw().lang("Remove"), this.options.title,{}, + et2_dialog.BUTTONS_OK_CANCEL, et2_dialog.QUESTION_MESSAGE + ); + } + + /** + * Set the HTML content of the portlet + * + * @param value String HTML fragment + */ + set_value(value) + { + this.content.html(value); + } + + /** + * Set the content of the header + * + * @param value String HTML fragment + */ + set_title(value) + { + this.header.contents() + .filter(function() { + return this.nodeType === 3; + }) + .remove(); + this.options.title = value; + this.header.append(value); + } + + /** + * Let this portlet stand out a little by allowing a custom color + */ + set_color(color) + { + this.options.color = color; + this.header.css("backgroundColor", color); + this.header.css('color', jQuery.Color(this.header.css("backgroundColor")).lightness() > 0.5 ? 'black':'white'); + this.content.css("backgroundColor", color); + } + + /** + * Set the number of grid cells this widget spans + * + * @param value int Number of horizontal grid cells + */ + set_width(value) + { + this.options.width = value; + this.div.attr("data-sizex", value); + // Clear what's there from jQuery, we get width from CSS according to sizex + this.div.css('width',''); + } + + /** + * Set the number of vertical grid cells this widget spans + * + * @param value int Number of vertical grid cells + */ + set_height(value) + { + this.options.height = value; + this.div.attr("data-sizey", value); + // Clear what's there from jQuery, we get width from CSS according to sizey + this.div.css('height',''); + } +} +et2_register_widget(et2_portlet, ["portlet"]); + diff --git a/api/js/etemplate/et2_widget_progress.js b/api/js/etemplate/et2_widget_progress.js index d03fcac7bf..4e836f5aed 100644 --- a/api/js/etemplate/et2_widget_progress.js +++ b/api/js/etemplate/et2_widget_progress.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - JS Progrss object * @@ -6,178 +7,165 @@ * @subpackage api * @link http://www.egroupware.org * @author Ralf Becker - * @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 - /vendor/bower-asset/jquery/dist/jquery.js; - et2_core_interfaces; - et2_core_valueWidget; + /vendor/bower-asset/jquery/dist/jquery.js; + et2_core_interfaces; + et2_core_valueWidget; */ - +var et2_core_widget_1 = require("./et2_core_widget"); +var et2_core_valueWidget_1 = require("./et2_core_valueWidget"); +var et2_core_inheritance_1 = require("./et2_core_inheritance"); /** * Class which implements the "progress" XET-Tag * * @augments et2_valueWidget */ -var et2_progress = (function(){ "use strict"; return et2_valueWidget.extend([et2_IDetachedDOM], -{ - attributes: { - "href": { - "name": "Link Target", - "type": "string", - "description": "Link URL, empty if you don't wan't to display a link." - }, - "extra_link_target": { - "name": "Link target", - "type": "string", - "default": "_self", - "description": "Link target descriptor" - }, - "extra_link_popup": { - "name": "Popup", - "type": "string", - "description": "widthxheight, if popup should be used, eg. 640x480" - }, - "label": { - "name": "Label", - "default": "", - "type": "string", - "description": "The label is displayed as the title. The label can contain variables, as descript for name. If the label starts with a '@' it is replaced by the value of the content-array at this index (with the '@'-removed and after expanding the variables).", - "translate": true - } - }, - legacyOptions: ["href", "extra_link_target", "imagemap", "extra_link_popup", "id"], - - /** - * Constructor - * - * @memberOf et2_progress - */ - init: function() - { - this._super.apply(this, arguments); - - var outer = document.createElement("div"); - outer.className = "et2_progress"; - this.progress = document.createElement("div"); - this.progress.style.width = "0"; - outer.appendChild(this.progress); - - if (this.options.href) - { - outer.className += ' et2_clickable'; - } - if(this.options["class"]) - { - outer.className += ' '+this.options["class"]; - } - this.setDOMNode(outer); // set's this.node = outer - }, - - click: function() - { - this._super.apply(this, arguments); - - if(this.options.href) - { - this.egw().open_link(this.options.href, this.options.extra_link_target, this.options.extra_link_popup); - } - }, - - // setting the value as width of the progress-bar - set_value: function(_value) - { - _value = parseInt(_value)+"%"; // make sure we have percent attached - this.progress.style.width = _value; - if (!this.options.label) this.set_label(_value); - }, - - // set's label as title of this.node - set_label: function(_value) - { - this.node.title = _value; - }, - - // set's class of this.node; preserve baseclasses et2_progress and if this.options.href is set et2_clickable - set_class: function(_value) - { - var baseClass = "et2_progress"; - if (this.options.href) - { - baseClass += ' et2_clickable'; - } - this.node.setAttribute('class', baseClass + ' ' + _value); - }, - - set_href: function (_value) - { - if (!this.isInTree()) - { - return false; - } - - this.options.href = _value; - if (_value) - { - jQuery(this.node).addClass('et2_clickable') - .wrapAll('"'); - - var href = this.options.href; - var popup = this.options.extra_link_popup; - var target = this.options.extra_link_target; - jQuery(this.node).parent().click(function(e) - { - egw.open_link(href,target,popup); - e.preventDefault(); - return false; - }); - } - else if (jQuery(this.node).parent('a').length) - { - jQuery(this.node).removeClass('et2_clickable') - .unwrap(); - } - - return true; - }, - - /** - * Implementation of "et2_IDetachedDOM" for fast viewing in gridview - * - * * @param {array} _attrs array to add further attributes to - */ - getDetachedAttributes: function(_attrs) { - _attrs.push("value", "label", "href"); - }, - - getDetachedNodes: function() { - return [this.node, this.progress]; - }, - - setDetachedAttributes: function(_nodes, _values) { - // Set the given DOM-Nodes - this.node = _nodes[0]; - this.progress = _nodes[1]; - - // Set the attributes - if (_values["label"]) - { - this.set_label(_values["label"]); - } - if (_values["value"]) - { - this.set_value(_values["value"]); - } - else if (_values["label"]) - { - this.set_value(_values["label"]); - } - if(_values["href"]) - { - jQuery(this.node).addClass('et2_clickable'); - this.set_href(_values["href"]); - } - } -});}).call(this); -et2_register_widget(et2_progress, ["progress"]); +var et2_progress = /** @class */ (function (_super) { + __extends(et2_progress, _super); + /** + * Constructor + * + * @memberOf et2_progress + */ + function et2_progress(_parent, _attrs, _child) { + var _this = + // Call the inherited constructor + _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_progress._attributes, _child || {})) || this; + _this.legacyOptions = ["href", "extra_link_target", "imagemap", "extra_link_popup", "id"]; + _this.progress = null; + var outer = document.createElement("div"); + outer.className = "et2_progress"; + _this.progress = document.createElement("div"); + _this.progress.style.width = "0"; + outer.appendChild(_this.progress); + if (_this.options.href) { + outer.className += ' et2_clickable'; + } + if (_this.options["class"]) { + outer.className += ' ' + _this.options["class"]; + } + _this.setDOMNode(outer); // set's this.node = outer + return _this; + } + et2_progress.prototype.click = function (e) { + _super.prototype.click.call(this, e); + if (this.options.href) { + this.egw().open_link(this.options.href, this.options.extra_link_target, this.options.extra_link_popup); + } + }; + // setting the value as width of the progress-bar + et2_progress.prototype.set_value = function (_value) { + _value = parseInt(_value) + "%"; // make sure we have percent attached + this.progress.style.width = _value; + if (!this.options.label) + this.set_label(_value); + }; + // set's label as title of this.node + et2_progress.prototype.set_label = function (_value) { + this.node.title = _value; + }; + // set's class of this.node; preserve baseclasses et2_progress and if this.options.href is set et2_clickable + et2_progress.prototype.set_class = function (_value) { + var baseClass = "et2_progress"; + if (this.options.href) { + baseClass += ' et2_clickable'; + } + this.node.setAttribute('class', baseClass + ' ' + _value); + }; + et2_progress.prototype.set_href = function (_value) { + if (!this.isInTree()) { + return false; + } + this.options.href = _value; + if (_value) { + jQuery(this.node).addClass('et2_clickable') + .wrapAll('"'); + var href_1 = this.options.href; + var popup_1 = this.options.extra_link_popup; + var target_1 = this.options.extra_link_target; + jQuery(this.node).parent().click(function (e) { + egw.open_link(href_1, target_1, popup_1); + e.preventDefault(); + return false; + }); + } + else if (jQuery(this.node).parent('a').length) { + jQuery(this.node).removeClass('et2_clickable') + .unwrap(); + } + return true; + }; + /** + * Implementation of "et2_IDetachedDOM" for fast viewing in gridview + * + * * @param {array} _attrs array to add further attributes to + */ + et2_progress.prototype.getDetachedAttributes = function (_attrs) { + _attrs.push("value", "label", "href"); + }; + et2_progress.prototype.getDetachedNodes = function () { + return [this.node, this.progress]; + }; + et2_progress.prototype.setDetachedAttributes = function (_nodes, _values) { + // Set the given DOM-Nodes + this.node = _nodes[0]; + this.progress = _nodes[1]; + // Set the attributes + if (_values["label"]) { + this.set_label(_values["label"]); + } + if (_values["value"]) { + this.set_value(_values["value"]); + } + else if (_values["label"]) { + this.set_value(_values["label"]); + } + if (_values["href"]) { + jQuery(this.node).addClass('et2_clickable'); + this.set_href(_values["href"]); + } + }; + et2_progress._attributes = { + "href": { + "name": "Link Target", + "type": "string", + "description": "Link URL, empty if you don't wan't to display a link." + }, + "extra_link_target": { + "name": "Link target", + "type": "string", + "default": "_self", + "description": "Link target descriptor" + }, + "extra_link_popup": { + "name": "Popup", + "type": "string", + "description": "widthxheight, if popup should be used, eg. 640x480" + }, + "label": { + "name": "Label", + "default": "", + "type": "string", + "description": "The label is displayed as the title. The label can contain variables, as descript for name. If the label starts with a '@' it is replaced by the value of the content-array at this index (with the '@'-removed and after expanding the variables).", + "translate": true + } + }; + return et2_progress; +}(et2_core_valueWidget_1.et2_valueWidget)); +et2_core_widget_1.et2_register_widget(et2_progress, ["progress"]); +//# sourceMappingURL=et2_widget_progress.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_progress.ts b/api/js/etemplate/et2_widget_progress.ts new file mode 100644 index 0000000000..b9b0e1f4e7 --- /dev/null +++ b/api/js/etemplate/et2_widget_progress.ts @@ -0,0 +1,193 @@ +/** + * EGroupware eTemplate2 - JS Progrss object + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage api + * @link http://www.egroupware.org + * @author Ralf Becker + */ + +/*egw:uses + /vendor/bower-asset/jquery/dist/jquery.js; + et2_core_interfaces; + et2_core_valueWidget; +*/ + +import {et2_register_widget, WidgetConfig} from "./et2_core_widget"; +import {et2_valueWidget} from "./et2_core_valueWidget"; +import {ClassWithAttributes} from "./et2_core_inheritance"; + +/** + * Class which implements the "progress" XET-Tag + * + * @augments et2_valueWidget + */ +class et2_progress extends et2_valueWidget implements et2_IDetachedDOM +{ + static readonly _attributes : any = { + "href": { + "name": "Link Target", + "type": "string", + "description": "Link URL, empty if you don't wan't to display a link." + }, + "extra_link_target": { + "name": "Link target", + "type": "string", + "default": "_self", + "description": "Link target descriptor" + }, + "extra_link_popup": { + "name": "Popup", + "type": "string", + "description": "widthxheight, if popup should be used, eg. 640x480" + }, + "label": { + "name": "Label", + "default": "", + "type": "string", + "description": "The label is displayed as the title. The label can contain variables, as descript for name. If the label starts with a '@' it is replaced by the value of the content-array at this index (with the '@'-removed and after expanding the variables).", + "translate": true + } + }; + + legacyOptions : string[] = ["href", "extra_link_target", "imagemap", "extra_link_popup", "id"]; + private progress : HTMLElement = null; + + /** + * Constructor + * + * @memberOf et2_progress + */ + constructor(_parent, _attrs? : WidgetConfig, _child? : object) + { + // Call the inherited constructor + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_progress._attributes, _child || {})); + + let outer = document.createElement("div"); + outer.className = "et2_progress"; + this.progress = document.createElement("div"); + this.progress.style.width = "0"; + outer.appendChild(this.progress); + + if (this.options.href) + { + outer.className += ' et2_clickable'; + } + if(this.options["class"]) + { + outer.className += ' '+this.options["class"]; + } + this.setDOMNode(outer); // set's this.node = outer + } + + click(e) + { + super.click(e); + + if(this.options.href) + { + this.egw().open_link(this.options.href, this.options.extra_link_target, this.options.extra_link_popup); + } + } + + // setting the value as width of the progress-bar + set_value(_value) + { + _value = parseInt(_value)+"%"; // make sure we have percent attached + this.progress.style.width = _value; + if (!this.options.label) this.set_label(_value); + } + + // set's label as title of this.node + set_label(_value) + { + this.node.title = _value; + } + + // set's class of this.node; preserve baseclasses et2_progress and if this.options.href is set et2_clickable + set_class(_value) + { + let baseClass = "et2_progress"; + if (this.options.href) + { + baseClass += ' et2_clickable'; + } + this.node.setAttribute('class', baseClass + ' ' + _value); + } + + set_href(_value) + { + if (!this.isInTree()) + { + return false; + } + + this.options.href = _value; + if (_value) + { + jQuery(this.node).addClass('et2_clickable') + .wrapAll('"'); + + let href = this.options.href; + let popup = this.options.extra_link_popup; + let target = this.options.extra_link_target; + jQuery(this.node).parent().click(function(e) + { + egw.open_link(href,target,popup); + e.preventDefault(); + return false; + }); + } + else if (jQuery(this.node).parent('a').length) + { + jQuery(this.node).removeClass('et2_clickable') + .unwrap(); + } + + return true; + } + + /** + * Implementation of "et2_IDetachedDOM" for fast viewing in gridview + * + * * @param {array} _attrs array to add further attributes to + */ + getDetachedAttributes(_attrs) + { + _attrs.push("value", "label", "href"); + } + + getDetachedNodes() + { + return [this.node, this.progress]; + } + + setDetachedAttributes(_nodes, _values) + { + // Set the given DOM-Nodes + this.node = _nodes[0]; + this.progress = _nodes[1]; + + // Set the attributes + if (_values["label"]) + { + this.set_label(_values["label"]); + } + if (_values["value"]) + { + this.set_value(_values["value"]); + } + else if (_values["label"]) + { + this.set_value(_values["label"]); + } + if(_values["href"]) + { + jQuery(this.node).addClass('et2_clickable'); + this.set_href(_values["href"]); + } + } +} +et2_register_widget(et2_progress, ["progress"]); + diff --git a/api/js/etemplate/et2_widget_radiobox.js b/api/js/etemplate/et2_widget_radiobox.js index 2d581d0343..72079a5401 100644 --- a/api/js/etemplate/et2_widget_radiobox.js +++ b/api/js/etemplate/et2_widget_radiobox.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - JS Radiobox object * @@ -9,12 +10,28 @@ * @copyright Nathan Gray 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 - /vendor/bower-asset/jquery/dist/jquery.js; - et2_core_inputWidget; + /vendor/bower-asset/jquery/dist/jquery.js; + et2_core_inputWidget; */ - +var et2_core_inputWidget_1 = require("./et2_core_inputWidget"); +var et2_core_inheritance_1 = require("./et2_core_inheritance"); +var et2_core_widget_1 = require("./et2_core_widget"); +var et2_core_valueWidget_1 = require("./et2_core_valueWidget"); /** * Class which implements the "radiobox" XET-Tag * @@ -24,431 +41,376 @@ * * @augments et2_inputWidget */ -var et2_radiobox = (function(){ "use strict"; return et2_inputWidget.extend( -{ - attributes: { - "set_value": { - "name": "Set value", - "type": "string", - "default": "true", - "description": "Value when selected" - }, - "ro_true": { - "name": "Read only selected", - "type": "string", - "default": "x", - "description": "What should be displayed when readonly and selected" - }, - "ro_false": { - "name": "Read only unselected", - "type": "string", - "default": "", - "description": "What should be displayed when readonly and not selected" - } - }, - - legacyOptions: ["set_value", "ro_true", "ro_false"], - - /** - * Constructor - * - * @memberOf et2_radiobox - */ - init: function() { - this._super.apply(this, arguments); - - this.input = null; - this.id = ""; - - this.createInputWidget(); - }, - transformAttributes: function(_attrs) { - this._super.apply(this, arguments); - var readonly = this.getArrayMgr('readonlys').getEntry(this.id); - if(readonly && readonly.hasOwnProperty(_attrs.set_value)) - { - _attrs.readonly = readonly[_attrs.set_value]; - } - }, - - createInputWidget: function() { - this.input = jQuery(document.createElement("input")) - .val(this.options.set_value) - .attr("type", "radio") - .attr("disabled", this.options.readonly); - - this.input.addClass("et2_radiobox"); - - this.setDOMNode(this.input[0]); - }, - - /** - * Overwritten to set different DOM level ids by appending set_value - * - * @param _id - */ - set_id: function(_id) - { - this._super.apply(this, arguments); - - this.dom_id = this.dom_id.replace('[]', '')+'-'+this.options.set_value; - if (this.input) this.input.attr('id', this.dom_id); - }, - - /** - * Default for radio buttons is label after button - * - * @param _label String New label for radio button. Use %s to locate the radio button somewhere else in the label - */ - set_label: function(_label) { - if(_label.length > 0 && _label.indexOf('%s')==-1) - { - _label = '%s'+_label; - } - this._super.apply(this, [_label]); - }, - - /** - * Override default to match against set/unset value AND iterate over all siblings with same id - * - * @param {string} _value - */ - set_value: function(_value) - { - this.getRoot().iterateOver(function(radio) - { - if (radio.id == this.id) - { - radio.input.prop('checked', _value == radio.options.set_value); - } - }, this, et2_radiobox); - }, - - /** - * Override default to iterate over all siblings with same id - * - * @return {string} - */ - getValue: function() - { - var val = this.options.value; // initial value, when form is loaded - var values = []; - this.getRoot().iterateOver(function(radio) - { - values.push(radio.options.set_value); - if (radio.id == this.id && radio.input && radio.input.prop('checked')) - { - val = radio.options.set_value; - } - }, this, et2_radiobox); - - return val && val.indexOf(values) ? val : null; - }, - - /** - * Overridden from parent so if it's required, only 1 in a group needs a value - * - * @param {array} messages - * @returns {Boolean} - */ - isValid: function(messages) { - var ok = true; - - // Check for required - if (this.options && this.options.needed && !this.options.readonly && !this.disabled && - (this.getValue() == null || this.getValue().valueOf() == '')) - { - if(jQuery.isEmptyObject(this.getInstanceManager().getValues(this.getInstanceManager().widgetContainer)[this.id.replace('[]', '')])) - { - messages.push(this.egw().lang('Field must not be empty !!!')); - ok = false; - } - } - return ok; - } -});}).call(this); -et2_register_widget(et2_radiobox, ["radio"]); - +var et2_radiobox = /** @class */ (function (_super) { + __extends(et2_radiobox, _super); + /** + * Constructor + * + * @memberOf et2_radiobox + */ + function et2_radiobox(_parent, _attrs, _child) { + var _this = _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_radiobox._attributes, _child || {})) || this; + _this.legacyOptions = ["set_value", "ro_true", "ro_false"]; + _this.input = null; + _this.id = ""; + _this.createInputWidget(); + return _this; + } + et2_radiobox.prototype.transformAttributes = function (_attrs) { + _super.prototype.transformAttributes.call(this, _attrs); + var readonly = this.getArrayMgr('readonlys').getEntry(this.id); + if (readonly && readonly.hasOwnProperty(_attrs.set_value)) { + _attrs.readonly = readonly[_attrs.set_value]; + } + }; + et2_radiobox.prototype.createInputWidget = function () { + this.input = jQuery(document.createElement("input")) + .val(this.options.set_value) + .attr("type", "radio") + .attr("disabled", this.options.readonly); + this.input.addClass("et2_radiobox"); + this.setDOMNode(this.input[0]); + }; + /** + * Overwritten to set different DOM level ids by appending set_value + * + * @param _id + */ + et2_radiobox.prototype.set_id = function (_id) { + _super.prototype.set_id.call(this, _id); + this.dom_id = this.dom_id.replace('[]', '') + '-' + this.options.set_value; + if (this.input) + this.input.attr('id', this.dom_id); + }; + /** + * Default for radio buttons is label after button + * + * @param _label String New label for radio button. Use %s to locate the radio button somewhere else in the label + */ + et2_radiobox.prototype.set_label = function (_label) { + if (_label.length > 0 && _label.indexOf('%s') == -1) { + _label = '%s' + _label; + } + _super.prototype.set_label.call(this, _label); + }; + /** + * Override default to match against set/unset value AND iterate over all siblings with same id + * + * @param {string} _value + */ + et2_radiobox.prototype.set_value = function (_value) { + this.getRoot().iterateOver(function (radio) { + if (radio.id == this.id) { + radio.input.prop('checked', _value == radio.options.set_value); + } + }, this, et2_radiobox); + }; + /** + * Override default to iterate over all siblings with same id + * + * @return {string} + */ + et2_radiobox.prototype.getValue = function () { + var val = this.options.value; // initial value, when form is loaded + var values = []; + this.getRoot().iterateOver(function (radio) { + values.push(radio.options.set_value); + if (radio.id == this.id && radio.input && radio.input.prop('checked')) { + val = radio.options.set_value; + } + }, this, et2_radiobox); + return val && val.indexOf(values) ? val : null; + }; + /** + * Overridden from parent so if it's required, only 1 in a group needs a value + * + * @param {array} messages + * @returns {Boolean} + */ + et2_radiobox.prototype.isValid = function (messages) { + var ok = true; + // Check for required + if (this.options && this.options.needed && !this.options.readonly && !this.disabled && + (this.getValue() == null || this.getValue().valueOf() == '')) { + if (jQuery.isEmptyObject(this.getInstanceManager().getValues(this.getInstanceManager().widgetContainer)[this.id.replace('[]', '')])) { + messages.push(this.egw().lang('Field must not be empty !!!')); + ok = false; + } + } + return ok; + }; + et2_radiobox._attributes = { + "set_value": { + "name": "Set value", + "type": "string", + "default": "true", + "description": "Value when selected" + }, + "ro_true": { + "name": "Read only selected", + "type": "string", + "default": "x", + "description": "What should be displayed when readonly and selected" + }, + "ro_false": { + "name": "Read only unselected", + "type": "string", + "default": "", + "description": "What should be displayed when readonly and not selected" + } + }; + return et2_radiobox; +}(et2_core_inputWidget_1.et2_inputWidget)); +et2_core_widget_1.et2_register_widget(et2_radiobox, ["radio"]); /** * @augments et2_valueWidget */ -var et2_radiobox_ro = (function(){ "use strict"; return et2_valueWidget.extend([et2_IDetachedDOM], -{ - attributes: { - "set_value": { - "name": "Set value", - "type": "string", - "default": "true", - "description": "Value when selected" - }, - "ro_true": { - "name": "Read only selected", - "type": "string", - "default": "x", - "description": "What should be displayed when readonly and selected" - }, - "ro_false": { - "name": "Read only unselected", - "type": "string", - "default": "", - "description": "What should be displayed when readonly and not selected" - }, - "label": { - "name": "Label", - "default": "", - "type": "string" - } - }, - - legacyOptions: ["set_value", "ro_true", "ro_false"], - - /** - * Constructor - * - * @memberOf et2_radiobox_ro - */ - init: function() { - this._super.apply(this, arguments); - - this.value = ""; - this.span = jQuery(document.createElement("span")) - .addClass("et2_radiobox"); - - this.setDOMNode(this.span[0]); - }, - - /** - * Override default to match against set/unset value - * - * @param {string} _value - */ - set_value: function(_value) { - this.value = _value; - if(_value == this.options.set_value) { - this.span.text(this.options.ro_true); - } else { - this.span.text(this.options.ro_false); - } - }, - - set_label: function(_label) { - // no label for ro radio, we show label of checked option as content - }, - - /** - * Code for implementing et2_IDetachedDOM - * - * @param {array} _attrs - */ - getDetachedAttributes: function(_attrs) - { - // Show label in nextmatch instead of just x - this.options.ro_true = this.options.label; - _attrs.push("value"); - }, - - getDetachedNodes: function() - { - return [this.span[0]]; - }, - - setDetachedAttributes: function(_nodes, _values) - { - this.span = jQuery(_nodes[0]); - this.set_value(_values["value"]); - } -});}).call(this); -et2_register_widget(et2_radiobox_ro, ["radio_ro"]); - - +var et2_radiobox_ro = /** @class */ (function (_super) { + __extends(et2_radiobox_ro, _super); + /** + * Constructor + * + * @memberOf et2_radiobox_ro + */ + function et2_radiobox_ro(_parent, _attrs, _child) { + var _this = + // Call the inherited constructor + _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_radiobox_ro._attributes, _child || {})) || this; + _this.legacyOptions = ["set_value", "ro_true", "ro_false"]; + _this.value = ""; + _this.span = null; + _this.span = jQuery(document.createElement("span")) + .addClass("et2_radiobox"); + _this.setDOMNode(_this.span[0]); + return _this; + } + /** + * Override default to match against set/unset value + * + * @param {string} _value + */ + et2_radiobox_ro.prototype.set_value = function (_value) { + this.value = _value; + if (_value == this.options.set_value) { + this.span.text(this.options.ro_true); + } + else { + this.span.text(this.options.ro_false); + } + }; + et2_radiobox_ro.prototype.set_label = function (_label) { + // no label for ro radio, we show label of checked option as content + }; + /** + * Code for implementing et2_IDetachedDOM + * + * @param {array} _attrs + */ + et2_radiobox_ro.prototype.getDetachedAttributes = function (_attrs) { + // Show label in nextmatch instead of just x + this.options.ro_true = this.options.label; + _attrs.push("value"); + }; + et2_radiobox_ro.prototype.getDetachedNodes = function () { + return [this.span[0]]; + }; + et2_radiobox_ro.prototype.setDetachedAttributes = function (_nodes, _values) { + this.span = jQuery(_nodes[0]); + this.set_value(_values["value"]); + }; + et2_radiobox_ro._attributes = { + "set_value": { + "name": "Set value", + "type": "string", + "default": "true", + "description": "Value when selected" + }, + "ro_true": { + "name": "Read only selected", + "type": "string", + "default": "x", + "description": "What should be displayed when readonly and selected" + }, + "ro_false": { + "name": "Read only unselected", + "type": "string", + "default": "", + "description": "What should be displayed when readonly and not selected" + }, + "label": { + "name": "Label", + "default": "", + "type": "string" + } + }; + return et2_radiobox_ro; +}(et2_core_valueWidget_1.et2_valueWidget)); +et2_core_widget_1.et2_register_widget(et2_radiobox_ro, ["radio_ro"]); /** * A group of radio buttons * * @augments et2_valueWidget */ -var et2_radioGroup = (function(){ "use strict"; return et2_valueWidget.extend([et2_IDetachedDOM], -{ - attributes: { - "label": { - "name": "Label", - "default": "", - "type": "string", - "description": "The label is displayed above the list of radio buttons. The label can contain variables, as descript for name. If the label starts with a '@' it is replaced by the value of the content-array at this index (with the '@'-removed and after expanding the variables).", - "translate": true - }, - "value": { - "name": "Value", - "type": "string", - "default": "true", - "description": "Value for each radio button" - }, - "ro_true": { - "name": "Read only selected", - "type": "string", - "default": "x", - "description": "What should be displayed when readonly and selected" - }, - "ro_false": { - "name": "Read only unselected", - "type": "string", - "default": "", - "description": "What should be displayed when readonly and not selected" - }, - "options": { - "name": "Radio options", - "type": "any", - "default": {}, - "description": "Options for radio buttons. Should be {value: label, ...}" - }, - "needed": { - "name": "Required", - "default": false, - "type": "boolean", - "description": "If required, the user must select one of the options before the form can be submitted" - } - }, - - createNamespace: false, - - /** - * Constructor - * - * @param parent - * @param attrs - * @memberOf et2_radioGroup - */ - init: function(parent, attrs) { - this._super.apply(this, arguments); - this.node = jQuery(document.createElement("div")) - .addClass("et2_vbox") - .addClass("et2_box_widget"); - if(this.options.needed) - { - // This isn't strictly allowed, but it works - this.node.attr("required","required"); - } - this.setDOMNode(this.node[0]); - - // The supported widget classes array defines a whitelist for all widget - // classes or interfaces child widgets have to support. - this.supportedWidgetClasses = [et2_radiobox,et2_radiobox_ro]; - }, - - set_value: function(_value) { - this.value = _value; - for (var i = 0; i < this._children.length; i++) - { - var radio = this._children[i]; - radio.set_value(_value); - } - }, - - getValue: function() { - return jQuery("input:checked", this.getDOMNode()).val(); - }, - - /** - * Set a bunch of radio buttons - * - * @param {object} _options object with value: label pairs - */ - set_options: function(_options) { - // Call the destructor of all children - for (var i = this._children.length - 1; i >= 0; i--) - { - this._children[i].free(); - } - this._children = []; - // create radio buttons for each option - for(var key in _options) - { - var attrs = { - // Add index so radios work properly - "id": (this.options.readonly ? this.id : this.id + "[" + "]"), - set_value: key, - label: _options[key], - ro_true: this.options.ro_true, - ro_false: this.options.ro_false, - readonly: this.options.readonly - }; - if(typeof _options[key] === 'object' && _options[key].label) - { - attrs.set_value = _options[key].value; - attrs.label = _options[key].label; - } - // Can't have a required readonly, it will warn & be removed later, so avoid the warning - if(attrs.readonly === false) - { - attrs.needed = this.options.needed; - } - var radio = et2_createWidget("radio", attrs, this); - } - this.set_value(this.value); - }, - - /** - * Set a label on the group of radio buttons - * - * @param {string} _value - */ - set_label: function(_value) { - // Abort if ther was no change in the label - if (_value == this.label) - { - return; - } - - if (_value) - { - // Create the label container if it didn't exist yet - if (this._labelContainer == null) - { - this._labelContainer = jQuery(document.createElement("label")); - this.getSurroundings().insertDOMNode(this._labelContainer[0]); - } - - // Clear the label container. - this._labelContainer.empty(); - - // Create the placeholder element and set it - var ph = document.createElement("span"); - this.getSurroundings().setWidgetPlaceholder(ph); - - this._labelContainer - .append(document.createTextNode(_value)) - .append(ph); - } - else - { - // Delete the labelContainer from the surroundings object - if (this._labelContainer) - { - this.getSurroundings().removeDOMNode(this._labelContainer[0]); - } - this._labelContainer = null; - } - }, - - /** - * Code for implementing et2_IDetachedDOM - * This doesn't need to be implemented. - * Individual widgets are detected and handled by the grid, but the interface is needed for this to happen - * - * @param {object} _attrs - */ - getDetachedAttributes: function(_attrs) - { - }, - - getDetachedNodes: function() - { - return [this.getDOMNode()]; - }, - - setDetachedAttributes: function(_nodes, _values) - { - } - -});}).call(this); +var et2_radioGroup = /** @class */ (function (_super) { + __extends(et2_radioGroup, _super); + /** + * Constructor + * + * @param parent + * @param attrs + * @memberOf et2_radioGroup + */ + function et2_radioGroup(_parent, _attrs, _child) { + var _this = + // Call the inherited constructor + _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_radioGroup._attributes, _child || {})) || this; + _this.node = null; + _this.value = null; + _this.node = jQuery(document.createElement("div")) + .addClass("et2_vbox") + .addClass("et2_box_widget"); + if (_this.options.needed) { + // This isn't strictly allowed, but it works + _this.node.attr("required", "required"); + } + _this.setDOMNode(_this.node[0]); + // The supported widget classes array defines a whitelist for all widget + // classes or interfaces child widgets have to support. + _this.supportedWidgetClasses = [et2_radiobox, et2_radiobox_ro]; + return _this; + } + et2_radioGroup.prototype.set_value = function (_value) { + this.value = _value; + for (var i = 0; i < this._children.length; i++) { + var radio = this._children[i]; + radio.set_value(_value); + } + }; + et2_radioGroup.prototype.getValue = function () { + return jQuery("input:checked", this.getDOMNode()).val(); + }; + /** + * Set a bunch of radio buttons + * + * @param {object} _options object with value: label pairs + */ + et2_radioGroup.prototype.set_options = function (_options) { + // Call the destructor of all children + for (var i = this._children.length - 1; i >= 0; i--) { + this._children[i].destroy(); + } + this._children = []; + // create radio buttons for each option + for (var key in _options) { + var attrs = { + // Add index so radios work properly + "id": (this.options.readonly ? this.id : this.id + "[" + "]"), + set_value: key, + label: _options[key], + ro_true: this.options.ro_true, + ro_false: this.options.ro_false, + readonly: this.options.readonly + }; + if (typeof _options[key] === 'object' && _options[key].label) { + attrs.set_value = _options[key].value; + attrs.label = _options[key].label; + } + // Can't have a required readonly, it will warn & be removed later, so avoid the warning + if (attrs.readonly === false) { + attrs['needed'] = this.options.needed; + } + et2_createWidget("radio", attrs, this); + } + this.set_value(this.value); + }; + /** + * Set a label on the group of radio buttons + * + * @param {string} _value + */ + et2_radioGroup.prototype.set_label = function (_value) { + // Abort if ther was no change in the label + if (_value == this.label) { + return; + } + if (_value) { + // Create the label container if it didn't exist yet + if (this._labelContainer == null) { + this._labelContainer = jQuery(document.createElement("label")); + this.getSurroundings().insertDOMNode(this._labelContainer[0]); + } + // Clear the label container. + this._labelContainer.empty(); + // Create the placeholder element and set it + var ph = document.createElement("span"); + this.getSurroundings().setWidgetPlaceholder(ph); + this._labelContainer + .append(document.createTextNode(_value)) + .append(ph); + } + else { + // Delete the labelContainer from the surroundings object + if (this._labelContainer) { + this.getSurroundings().removeDOMNode(this._labelContainer[0]); + } + this._labelContainer = null; + } + }; + /** + * Code for implementing et2_IDetachedDOM + * This doesn't need to be implemented. + * Individual widgets are detected and handled by the grid, but the interface is needed for this to happen + * + * @param {object} _attrs + */ + et2_radioGroup.prototype.getDetachedAttributes = function (_attrs) { + }; + et2_radioGroup.prototype.getDetachedNodes = function () { + return [this.getDOMNode()]; + }; + et2_radioGroup.prototype.setDetachedAttributes = function (_nodes, _values) { + }; + et2_radioGroup._attributes = { + "label": { + "name": "Label", + "default": "", + "type": "string", + "description": "The label is displayed above the list of radio buttons. The label can contain variables, as descript for name. If the label starts with a '@' it is replaced by the value of the content-array at this index (with the '@'-removed and after expanding the variables).", + "translate": true + }, + "value": { + "name": "Value", + "type": "string", + "default": "true", + "description": "Value for each radio button" + }, + "ro_true": { + "name": "Read only selected", + "type": "string", + "default": "x", + "description": "What should be displayed when readonly and selected" + }, + "ro_false": { + "name": "Read only unselected", + "type": "string", + "default": "", + "description": "What should be displayed when readonly and not selected" + }, + "options": { + "name": "Radio options", + "type": "any", + "default": {}, + "description": "Options for radio buttons. Should be {value: label, ...}" + }, + "needed": { + "name": "Required", + "default": false, + "type": "boolean", + "description": "If required, the user must select one of the options before the form can be submitted" + } + }; + return et2_radioGroup; +}(et2_core_valueWidget_1.et2_valueWidget)); // No such tag as 'radiogroup', but it needs something -et2_register_widget(et2_radioGroup, ["radiogroup"]); +et2_core_widget_1.et2_register_widget(et2_radioGroup, ["radiogroup"]); +//# sourceMappingURL=et2_widget_radiobox.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_radiobox.ts b/api/js/etemplate/et2_widget_radiobox.ts new file mode 100644 index 0000000000..9a805240ab --- /dev/null +++ b/api/js/etemplate/et2_widget_radiobox.ts @@ -0,0 +1,466 @@ +/** + * EGroupware eTemplate2 - JS Radiobox object + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage api + * @link http://www.egroupware.org + * @author Nathan Gray + * @copyright Nathan Gray 2011 + * @version $Id$ + */ + +/*egw:uses + /vendor/bower-asset/jquery/dist/jquery.js; + et2_core_inputWidget; +*/ +import {et2_inputWidget} from "./et2_core_inputWidget"; +import {ClassWithAttributes} from "./et2_core_inheritance"; +import {WidgetConfig, et2_register_widget} from "./et2_core_widget"; +import {et2_valueWidget} from './et2_core_valueWidget'; +/** + * Class which implements the "radiobox" XET-Tag + * + * A radio button belongs to same group by giving all buttons of a group same id! + * + * set_value iterates over all of them and (un)checks them depending on given value. + * + * @augments et2_inputWidget + */ +class et2_radiobox extends et2_inputWidget +{ + static readonly _attributes : any = { + "set_value": { + "name": "Set value", + "type": "string", + "default": "true", + "description": "Value when selected" + }, + "ro_true": { + "name": "Read only selected", + "type": "string", + "default": "x", + "description": "What should be displayed when readonly and selected" + }, + "ro_false": { + "name": "Read only unselected", + "type": "string", + "default": "", + "description": "What should be displayed when readonly and not selected" + } + }; + + legacyOptions : string[] = ["set_value", "ro_true", "ro_false"]; + input : JQuery = null; + id : string = ""; + + /** + * Constructor + * + * @memberOf et2_radiobox + */ + constructor(_parent, _attrs? : WidgetConfig, _child? : object) { + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_radiobox._attributes, _child || {})); + this.createInputWidget(); + } + + transformAttributes(_attrs) { + super.transformAttributes(_attrs); + + let readonly = this.getArrayMgr('readonlys').getEntry(this.id); + if(readonly && readonly.hasOwnProperty(_attrs.set_value)) + { + _attrs.readonly = readonly[_attrs.set_value]; + } + } + + createInputWidget() { + this.input = jQuery(document.createElement("input")) + .val(this.options.set_value) + .attr("type", "radio") + .attr("disabled", this.options.readonly); + + this.input.addClass("et2_radiobox"); + + this.setDOMNode(this.input[0]); + } + + /** + * Overwritten to set different DOM level ids by appending set_value + * + * @param _id + */ + set_id(_id) + { + super.set_id(_id); + + this.dom_id = this.dom_id.replace('[]', '')+'-'+this.options.set_value; + if (this.input) this.input.attr('id', this.dom_id); + } + + /** + * Default for radio buttons is label after button + * + * @param _label String New label for radio button. Use %s to locate the radio button somewhere else in the label + */ + set_label(_label) + { + if(_label.length > 0 && _label.indexOf('%s')==-1) + { + _label = '%s'+_label; + } + super.set_label(_label); + } + + /** + * Override default to match against set/unset value AND iterate over all siblings with same id + * + * @param {string} _value + */ + set_value(_value) + { + this.getRoot().iterateOver(function(radio) + { + if (radio.id == this.id) + { + radio.input.prop('checked', _value == radio.options.set_value); + } + }, this, et2_radiobox); + } + + /** + * Override default to iterate over all siblings with same id + * + * @return {string} + */ + getValue() + { + let val = this.options.value; // initial value, when form is loaded + let values = []; + this.getRoot().iterateOver(function(radio) + { + values.push(radio.options.set_value); + if (radio.id == this.id && radio.input && radio.input.prop('checked')) + { + val = radio.options.set_value; + } + }, this, et2_radiobox); + + return val && val.indexOf(values) ? val : null; + } + + /** + * Overridden from parent so if it's required, only 1 in a group needs a value + * + * @param {array} messages + * @returns {Boolean} + */ + isValid(messages) { + let ok = true; + + // Check for required + if (this.options && this.options.needed && !this.options.readonly && !this.disabled && + (this.getValue() == null || this.getValue().valueOf() == '')) + { + if(jQuery.isEmptyObject(this.getInstanceManager().getValues(this.getInstanceManager().widgetContainer)[this.id.replace('[]', '')])) + { + messages.push(this.egw().lang('Field must not be empty !!!')); + ok = false; + } + } + return ok; + } +} +et2_register_widget(et2_radiobox, ["radio"]); + +/** + * @augments et2_valueWidget + */ +class et2_radiobox_ro extends et2_valueWidget implements et2_IDetachedDOM +{ + static readonly _attributes : any = { + "set_value": { + "name": "Set value", + "type": "string", + "default": "true", + "description": "Value when selected" + }, + "ro_true": { + "name": "Read only selected", + "type": "string", + "default": "x", + "description": "What should be displayed when readonly and selected" + }, + "ro_false": { + "name": "Read only unselected", + "type": "string", + "default": "", + "description": "What should be displayed when readonly and not selected" + }, + "label": { + "name": "Label", + "default": "", + "type": "string" + } + }; + + legacyOptions : string[] = ["set_value", "ro_true", "ro_false"]; + value : string = ""; + span : JQuery = null; + + /** + * Constructor + * + * @memberOf et2_radiobox_ro + */ + constructor(_parent, _attrs? : WidgetConfig, _child? : object) + { + // Call the inherited constructor + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_radiobox_ro._attributes, _child || {})); + this.span = jQuery(document.createElement("span")) + .addClass("et2_radiobox"); + + this.setDOMNode(this.span[0]); + } + + /** + * Override default to match against set/unset value + * + * @param {string} _value + */ + set_value(_value) { + this.value = _value; + if(_value == this.options.set_value) { + this.span.text(this.options.ro_true); + } else { + this.span.text(this.options.ro_false); + } + } + + set_label(_label) { + // no label for ro radio, we show label of checked option as content + } + + /** + * Code for implementing et2_IDetachedDOM + * + * @param {array} _attrs + */ + getDetachedAttributes(_attrs) + { + // Show label in nextmatch instead of just x + this.options.ro_true = this.options.label; + _attrs.push("value"); + } + + getDetachedNodes() + { + return [this.span[0]]; + } + + setDetachedAttributes(_nodes, _values) + { + this.span = jQuery(_nodes[0]); + this.set_value(_values["value"]); + } +} +et2_register_widget(et2_radiobox_ro, ["radio_ro"]); + + +/** + * A group of radio buttons + * + * @augments et2_valueWidget + */ +class et2_radioGroup extends et2_valueWidget implements et2_IDetachedDOM +{ + static readonly _attributes : any = { + "label": { + "name": "Label", + "default": "", + "type": "string", + "description": "The label is displayed above the list of radio buttons. The label can contain variables, as descript for name. If the label starts with a '@' it is replaced by the value of the content-array at this index (with the '@'-removed and after expanding the variables).", + "translate": true + }, + "value": { + "name": "Value", + "type": "string", + "default": "true", + "description": "Value for each radio button" + }, + "ro_true": { + "name": "Read only selected", + "type": "string", + "default": "x", + "description": "What should be displayed when readonly and selected" + }, + "ro_false": { + "name": "Read only unselected", + "type": "string", + "default": "", + "description": "What should be displayed when readonly and not selected" + }, + "options": { + "name": "Radio options", + "type": "any", + "default": {}, + "description": "Options for radio buttons. Should be {value: label, ...}" + }, + "needed": { + "name": "Required", + "default": false, + "type": "boolean", + "description": "If required, the user must select one of the options before the form can be submitted" + } + }; + + node : JQuery = null; + value : any = null; + /** + * Constructor + * + * @param parent + * @param attrs + * @memberOf et2_radioGroup + */ + constructor(_parent, _attrs? : WidgetConfig, _child? : object) + { + // Call the inherited constructor + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_radioGroup._attributes, _child || {})); + this.node = jQuery(document.createElement("div")) + .addClass("et2_vbox") + .addClass("et2_box_widget"); + if(this.options.needed) + { + // This isn't strictly allowed, but it works + this.node.attr("required","required"); + } + this.setDOMNode(this.node[0]); + + // The supported widget classes array defines a whitelist for all widget + // classes or interfaces child widgets have to support. + this.supportedWidgetClasses = [et2_radiobox,et2_radiobox_ro]; + } + + set_value(_value) + { + this.value = _value; + for (let i = 0; i < this._children.length; i++) + { + let radio = this._children[i]; + radio.set_value(_value); + } + } + + getValue() + { + return jQuery("input:checked", this.getDOMNode()).val(); + } + + /** + * Set a bunch of radio buttons + * + * @param {object} _options object with value: label pairs + */ + set_options(_options) + { + // Call the destructor of all children + for (let i = this._children.length - 1; i >= 0; i--) + { + this._children[i].destroy(); + } + this._children = []; + // create radio buttons for each option + for(let key in _options) + { + let attrs = { + // Add index so radios work properly + "id": (this.options.readonly ? this.id : this.id + "[" + "]"), + set_value: key, + label: _options[key], + ro_true: this.options.ro_true, + ro_false: this.options.ro_false, + readonly: this.options.readonly + }; + if(typeof _options[key] === 'object' && _options[key].label) + { + attrs.set_value = _options[key].value; + attrs.label = _options[key].label; + } + // Can't have a required readonly, it will warn & be removed later, so avoid the warning + if(attrs.readonly === false) + { + attrs['needed'] = this.options.needed; + } + et2_createWidget("radio", attrs, this); + } + this.set_value(this.value); + } + + /** + * Set a label on the group of radio buttons + * + * @param {string} _value + */ + set_label(_value) { + // Abort if ther was no change in the label + if (_value == this.label) + { + return; + } + + if (_value) + { + // Create the label container if it didn't exist yet + if (this._labelContainer == null) + { + this._labelContainer = jQuery(document.createElement("label")); + this.getSurroundings().insertDOMNode(this._labelContainer[0]); + } + + // Clear the label container. + this._labelContainer.empty(); + + // Create the placeholder element and set it + var ph = document.createElement("span"); + this.getSurroundings().setWidgetPlaceholder(ph); + + this._labelContainer + .append(document.createTextNode(_value)) + .append(ph); + } + else + { + // Delete the labelContainer from the surroundings object + if (this._labelContainer) + { + this.getSurroundings().removeDOMNode(this._labelContainer[0]); + } + this._labelContainer = null; + } + } + + /** + * Code for implementing et2_IDetachedDOM + * This doesn't need to be implemented. + * Individual widgets are detected and handled by the grid, but the interface is needed for this to happen + * + * @param {object} _attrs + */ + getDetachedAttributes(_attrs) + { + } + + getDetachedNodes() + { + return [this.getDOMNode()]; + } + + setDetachedAttributes(_nodes, _values) + { + } + +} +// No such tag as 'radiogroup', but it needs something +et2_register_widget(et2_radioGroup, ["radiogroup"]); + diff --git a/api/js/etemplate/et2_widget_script.js b/api/js/etemplate/et2_widget_script.js index 9633b8262b..bc9368486d 100644 --- a/api/js/etemplate/et2_widget_script.js +++ b/api/js/etemplate/et2_widget_script.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - JS widget class containing javascript * @@ -6,14 +7,26 @@ * @subpackage api * @link http://www.egroupware.org * @author Ralf Becker - * @copyright Stylite 2015 - * @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_widget; + et2_core_widget; */ - +var et2_core_widget_1 = require("./et2_core_widget"); +var et2_core_widget_2 = require("./et2_core_widget"); /** * Function which executes the encapsulated script data. * @@ -31,37 +44,30 @@ * * @augments et2_widget */ -var et2_script = (function(){ "use strict"; return et2_widget.extend( -{ - /** - * Constructor - * - * @memberOf et2_script - */ - init: function() - { - this._super.apply(this, arguments); - - // Allow no child widgets - this.supportedWidgetClasses = []; - }, - - /** - * We can NOT create a script tag containing the content, as this violoates our CSP policy! - * - * @param {string} _content - */ - loadContent: function(_content) - { - try - { - var func = new Function(_content); - func.call(window); - } - catch (e) - { - this.egw.debug('error', 'Error while executing script: ',_content,e); - } - } -});}).call(this); -et2_register_widget(et2_script, ["script"]); +var et2_script = /** @class */ (function (_super) { + __extends(et2_script, _super); + function et2_script(_parent, _attrs, _child) { + var _this = _super.call(this) || this; + // Allow no child widgets + _this.supportedWidgetClasses = []; + return _this; + } + ; + /** + * We can NOT create a script tag containing the content, as this violoates our CSP policy! + * + * @param {string} _content + */ + et2_script.prototype.loadContent = function (_content) { + try { + var func = new Function(_content); + func.call(window); + } + catch (e) { + this.egw.debug('error', 'Error while executing script: ', _content, e); + } + }; + return et2_script; +}(et2_core_widget_2.et2_widget)); +et2_core_widget_1.et2_register_widget(et2_script, ["script"]); +//# sourceMappingURL=et2_widget_script.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_script.ts b/api/js/etemplate/et2_widget_script.ts new file mode 100644 index 0000000000..90bde25622 --- /dev/null +++ b/api/js/etemplate/et2_widget_script.ts @@ -0,0 +1,63 @@ +/** + * EGroupware eTemplate2 - JS widget class containing javascript + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage api + * @link http://www.egroupware.org + * @author Ralf Becker + */ + +/*egw:uses + et2_core_widget; +*/ + +import {et2_register_widget, WidgetConfig} from "./et2_core_widget"; +import {et2_widget} from "./et2_core_widget"; + +/** + * Function which executes the encapsulated script data. + * + * This should only be used for customization and NOT for regular EGroupware code! + * + * We can NOT create a script tag containing the content, as this violoates our CSP policy! + * + * We use new Function(_content) instead. Therefore you have to use window to address global context: + * + * window.some_func = function() {...} + * + * instead of not working + * + * function some_funct() {...} + * + * @augments et2_widget + */ +class et2_script extends et2_widget +{ + constructor(_parent?, _attrs? : WidgetConfig, _child? : object) + { + super(); + // Allow no child widgets + this.supportedWidgetClasses = []; + }; + + /** + * We can NOT create a script tag containing the content, as this violoates our CSP policy! + * + * @param {string} _content + */ + loadContent(_content) + { + try + { + var func = new Function(_content); + func.call(window); + } + catch (e) + { + this.egw.debug('error', 'Error while executing script: ',_content,e); + } + } +} +et2_register_widget(et2_script, ["script"]); + diff --git a/api/js/etemplate/et2_widget_selectAccount.js b/api/js/etemplate/et2_widget_selectAccount.js index a95ac75b06..cd1f1f092a 100644 --- a/api/js/etemplate/et2_widget_selectAccount.js +++ b/api/js/etemplate/et2_widget_selectAccount.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - JS Select account widget * @@ -12,11 +13,29 @@ * @copyright Nathan Gray 2012 * @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_widget_link; + et2_widget_link; */ - +var et2_widget_selectbox_1 = require("./et2_widget_selectbox"); +var et2_core_widget_1 = require("./et2_core_widget"); +var et2_core_inheritance_1 = require("./et2_core_inheritance"); +var et2_widget_link_1 = require("./et2_widget_link"); +var et2_widget_dialog_1 = require("./et2_widget_dialog"); +var et2_widget_link_2 = require("./et2_widget_link"); /** * Account selection widget * Changes according to the user's account_selection preference @@ -27,722 +46,572 @@ * * Only primary_group and popup need anything different from a normal selectbox * - * @augments et2_selectbox */ -var et2_selectAccount = (function(){ "use strict"; return et2_selectbox.extend( -{ - attributes: { - 'account_type': { - 'name': 'Account type', - 'default': 'accounts', - 'type': 'string', - 'description': 'Limit type of accounts. One of {accounts,groups,both,owngroups}.' - } - }, - - legacyOptions: ['empty_label','account_type'], - - account_types: ['accounts','groups','both','owngroups'], - - /** - * Constructor - * - * @param _parent - * @param _attrs - * @memberOf et2_selectAccount - * @returns - */ - init: function(_parent, _attrs) { - - // Type in rows or somewhere else? - if(jQuery.inArray(_attrs['empty_label'], this.account_types) > 0 && ( - jQuery.inArray(_attrs['account_type'], this.account_types) < 0 || - _attrs['account_type'] == this.attributes.account_type['default']) - ) - { - _attrs['account_type'] = _attrs['empty_label']; - _attrs['empty_label'] = ''; - } - if(jQuery.inArray(_attrs['account_type'], this.account_types) < 0) - { - this.egw().debug("warn", "Invalid account_type: %s Valid options:",_attrs['account_type'], this.account_types); - } - - // Holder for search jQuery nodes - this.search = null; - - // Reference to dialog - this.dialog = null; - - // Reference to widget within dialog - this.widgets = null; - - if(!_attrs.empty_label && !_attrs.readonly && _attrs.multiple) - { - _attrs.empty_label = this.egw().lang('Select user or group'); - } - - this._super.call(this, _parent, _attrs); - - // Allow certain widgets inside this one - this.supportedWidgetClasses = [et2_link_entry]; - }, - - destroy: function() { - this._super.apply(this, arguments); - }, - - /** - * Tell et2 widget framework where to go - * - * @param {object} _sender - * - getDOMNode: function(_sender) { - if(this.search_widget != null && _sender == this.search_widget) - { - return this.search != null ? this.search[0] : this.search_widget._parent.getDOMNode(); - } - return this._super.apply(this, arguments); - }, - */ - - - /** - * Single selection - override to add search button - */ - createInputWidget: function() - { - var type = this.egw().preference('account_selection', 'common'); - - switch(type) - { - case 'none': - if(typeof egw.user('apps').admin == 'undefined') - { - this.options.select_options = {}; - break; - } - case 'selectbox': - case 'groupmembers': - default: - this.options.select_options = this._get_accounts(); - break; - } - - this._super.apply(this, arguments); - - // Add search button - if(type == 'primary_group') - { - var button = jQuery(document.createElement("span")) - .addClass("et2_clickable") - .click(this, jQuery.proxy(function(e) { - // Auto-expand - if(this.options.expand_multiple_rows && !this.options.multiple) - { - this.set_multiple(true, this.options.expand_multiple_rows); - } - - if(this.options.multiple) - { - this._open_multi_search(e); - } - else - { - this._open_search(e); - } - },this)) - .attr("title", egw.lang("popup with search")) - .append(''); - - this.getSurroundings().insertDOMNode(button[0]); - } - }, - - /** - * Multiple selection - override to add search button - */ - createMultiSelect: function() { - - var type = this.egw().preference('account_selection', 'common'); - if(type == 'none' && typeof egw.user('apps').admin == 'undefined') return; - - this._super.apply(this, arguments); - - this.options.select_options = this._get_accounts(); - - if(type == 'primary_group') - { - // Allow search 'inside' this widget - this.supportedWidgetClasses = [et2_link_entry]; - - // Add quick search - turn off multiple to get normal result list - this.options.multiple = false; - this._create_search(); - - // Clear search box after select - var old_select = this.search_widget.select; - var self = this; - this.search_widget.select = function(e, selected) { - var current = self.getValue(); - - // Fix ID as sent from server - must be numeric - selected.item.value = parseInt(selected.item.value); - - // This one is important, it makes sure the option is there - old_select.apply(this, arguments); - - // Add quick search selection into current selection - current.push(selected.item.value); - - // Clear search - this.search.val(''); - - self.set_value(current); - }; - - // Put search results as a DOM sibling of the options, for proper display - this.search_widget.search.on("autocompleteopen", jQuery.proxy(function() { - this.search_widget.search.data("ui-autocomplete").menu.element - .appendTo(this.node) - .position({my: 'left top', at: 'left bottom', of: this.multiOptions.prev()}); - },this)); - this.search = jQuery(document.createElement("li")) - .appendTo(this.multiOptions.prev().find('ul')); - this.options.multiple = true; - - // Add search button - var button = jQuery(document.createElement("li")) - .addClass("et2_clickable") - .click(this, this._open_multi_search) - .attr("title", egw.lang("popup with search")) - .append(''); - var type = this.egw().preference('account_selection', 'common'); - - // Put it last so check/uncheck doesn't move around - this.multiOptions.prev().find('ul') - .append(button); - } - }, - - /** - * Override parent to make sure accounts are there as options. - * - * Depending on the widget's attributes and the user's preferences, not all selected - * accounts may be in the cache as options, so we fetch the extras to make sure - * we don't lose any. - * - * As fetching them might only work asynchron (if they are not yet loaded), - * we have to call set_value again, once all labels have arrived from server. - * - * @param {string|array} _value - */ - set_value: function(_value) - { - if(typeof _value == "string" && this.options.multiple && _value.match(this._is_multiple_regexp) !== null) - { - _value = _value.split(','); - } - - if(_value) - { - var search = _value; - if (!jQuery.isArray(search)) - { - search = [_value]; - } - var update_options = false; - var num_calls = 0; - var current_call = 0; - for(var j = 0; j < search.length; j++) - { - var found = false; - - // Not having a value to look up causes an infinite loop - if(!search[j] || search[j] === "0") continue; - - // Options are not indexed, so we must look - for(var i = 0; !found && i < this.options.select_options.length; i++) - { - if (typeof this.options.select_options[i] != 'object') - { - egw.debug('warn',this.id + ' wrong option '+i+' this.options.select_options=', this.options.select_options); - continue; - } - if(this.options.select_options[i].value == search[j]) found = true; - } - // We only look for numeric IDs, non-numeric IDs cause an exception - if(!found && !isNaN(search[j])) - { - // Add it in - var name = this.egw().link_title('api-accounts', search[j]); - if (name) // was already cached on client-side - { - update_options = true; - this.options.select_options.push({value: search[j], label:name}); - } - else // not available: need to call set_value again, after all arrived from server - { - ++num_calls; - // Add immediately with value as label, we'll replace later - this._appendOptionElement(search[j],search[j]); - this.egw().link_title('api-accounts', search[j], function(name) - { - if (++current_call >= num_calls) // only run last callback - { - // Update the label - // Options are not indexed, so we must look - for(var i = 0; i < this.widget.options.select_options.length; i++) - { - var opt = this.widget.options.select_options[i]; - if(opt && opt.value && opt.value == this.unknown && opt.label == this.unknown) - { - opt.label = name; - this.widget.set_select_options(this.widget.options.select_options); - break; - } - } - this.widget.set_value(_value); - } - }, {widget: this, unknown: search[j]}); - } - } - } - if(update_options) - { - this.set_select_options(this.options.select_options); - } - } - this._super.apply(this, arguments); - }, - - /** - * Get account info for select options from common client-side account cache - * - * @return {Array} select options - */ - _get_accounts: function() - { - if (!jQuery.isArray(this.options.select_options)) - { - var options = jQuery.extend({}, this.options.select_options); - this.options.select_options = []; - for(var key in options) - { - if (typeof options[key] == 'object') - { - if (typeof(options[key].key) == 'undefined') - { - options[key].value = key; - } - this.options.select_options.push(options[key]); - } - else - { - this.options.select_options.push({value: key, label: options[key]}); - } - } - } - var type = this.egw().preference('account_selection', 'common'); - var accounts = []; - // for primary_group we only display owngroups == own memberships, not other groups - if (type == 'primary_group' && this.options.account_type != 'accounts') - { - if (this.options.account_type == 'both') - { - accounts = this.egw().accounts('accounts'); - } - accounts = accounts.concat(this.egw().accounts('owngroups')); - } - else - { - accounts = this.egw().accounts(this.options.account_type); - } - return this.options.select_options.concat(accounts); - }, - - /** - * Create & display a way to search & select a single account / group - * Single selection is just link widget - * - * @param e event - */ - _open_search: function(e) { - var widget = e.data; - var search = widget._create_search(); - - // Selecting a single user closes the dialog, this only used if user cleared - var ok_click = function() { - widget.set_value([]); - // Fire change event - if(widget.input) widget.input.trigger("change"); - jQuery(this).dialog("close"); - }; - widget._create_dialog(search, ok_click); - }, - - /** - * Create & display a way to search & select multiple accounts / groups - * - * @param e event - */ - _open_multi_search: function(e) { - var widget = e && e.data ? e.data : this; - var table = widget.search = jQuery('
          '); - table.css("width", "100%").css("height", "100%"); - var search_col = jQuery('#search_col',table); - var select_col = jQuery('#selection_col',table); - - // Search / Selection - search_col.append(widget._create_search()); - - // Currently selected - select_col.append(widget._create_selected()); - - var ok_click = function() { - jQuery(this).dialog("close"); - // Update widget with selected - var ids = []; - var data = {}; - jQuery('#'+widget.getInstanceManager().uniqueId + '_selected li',select_col).each(function() { - var id = jQuery(this).attr("data-id"); - // Add to list - ids.push(id); - - // Make sure option is there - if(!widget.options.multiple && jQuery('input[id$="_opt_'+id+'"]',widget.multiOptions).length == 0) - { - widget._appendMultiOption(id,jQuery('label',this).text()); - } - else if (widget.options.multiple && jQuery('option[value="'+id+'"]',widget.node).length == 0) - { - widget._appendOptionElement(id,jQuery('label',this).text()); - } - }); - - widget.set_value(ids); - - // Fire change event - if(widget.input) widget.input.trigger("change"); - }; - - var container = jQuery(document.createElement("div")).append(table); - return widget._create_dialog(container, ok_click); - }, - - /** - * Create / display popup with search / selection widgets - * - * @param {et2_dialog} widgets - * @param {function} update_function - */ - _create_dialog: function(widgets, update_function) { - this.widgets = widgets; - this.dialog = et2_dialog.show_dialog(false, - '', - this.options.label ? this.options.label : this.egw().lang('Select'), - {}, - [{ - text: this.egw().lang("ok"), - image: 'check', - click: update_function - },{ - text: this.egw().lang("cancel"), - image: 'cancel' - }] - ); - this.dialog.set_dialog_type(''); - // Static size for easier layout - this.dialog.div.dialog({width: "500", height: "370"}); - - this.dialog.div.append(widgets.width('100%')); - return widgets; - }, - - /** - * Search is a link-entry widget, with some special display for multi-select - */ - _create_search: function() { - var self = this; - var search = this.search = jQuery(document.createElement("div")); - - var search_widget = this.search_widget = et2_createWidget('link-entry', { - 'only_app': 'api-accounts', - 'query': function(request, response) { - // Clear previous search results for multi-select - if(!request.options) - { - search.find('#search_results').empty(); - } - // Restrict to specified account type - if(!request.options || !request.options.filter) - { - request.options = {account_type: self.options.account_type}; - } - return true; - }, - 'select': function(e, selected) { - // Make sure option is there - var already_there = false; - var last_key = null; - for(last_key in self.options.select_options) - { - var option = self.options.select_options[last_key]; - already_there = already_there || (typeof option.value != 'undefined' && option.value == selected.item.value); - } - if(!already_there) - { - self.options.select_options[parseInt(last_key)+1] = selected.item; - self._appendOptionElement(selected.item.value, selected.item.label); - } - self.set_value(selected.item.value); - if(self.dialog) - { - self.dialog.div.dialog("close"); - } - // Fire change event - if(self.input) self.input.trigger("change"); - return true; - } - }, this); - // add it where we want it - search.append(search_widget.getDOMNode()); - - if(!this.options.multiple) return search; - - // Multiple is more complicated. It uses a custom display for results to - // allow choosing multiples from a match - var results = jQuery(document.createElement("ul")) - .attr("id", "search_results") - .css("height", "230px") - .addClass("ui-multiselect-checkboxes ui-helper-reset"); - jQuery(document.createElement("div")) - .addClass("et2_selectbox") - .css("height", "100%") - .append(results) - .appendTo(search); - - // Override link-entry auto-complete for custom display - // Don't show normal drop-down - search_widget.search.data("ui-autocomplete")._suggest = function(items) { - jQuery.each(items, function (index, item) { - // Make sure value is numeric - item.value = parseInt(item.value); - self._add_search_result(results, item); - }); - }; - - return search; - }, - - /** - * Add the selected result to the list of search results - * - * @param list - * @param item - */ - _add_search_result: function(list, item) { - - var node = null; - var self = this; - - // Make sure value is numeric - if(item.value) item.value = parseInt(item.value); - - // (containter of) Currently selected users / groups - var selected = jQuery('#'+this.getInstanceManager().uniqueId + "_selected", this.widgets); - - // Group - if(item.value && item.value < 0) - { - node = jQuery(document.createElement('ul')); - // Add button to show users - if(this.options.account_type != 'groups') - { - jQuery('') - .css("float", "left") - .appendTo(node) - .click(function() { - if(jQuery(this).hasClass("ui-icon-circlesmall-plus")) - { - jQuery(this).removeClass("ui-icon-circlesmall-plus") - .addClass("ui-icon-circlesmall-minus"); - - var group = jQuery(this).parent() - .addClass("expanded"); - - if(group.children("li").length == 0) - { - // Fetch group members - self.search_widget.query({ - term:"", - options: {filter:{group: item.value}}, - no_cache:true - }, function(items) { - jQuery(items).each(function(index,item) { - self._add_search_result(node, item); - }); - }); - } - else - { - group.children("li") - // Only show children that are not selected - .each(function(index, item) { - var j = jQuery(item); - if(jQuery('[data-id="'+j.attr("data-id")+'"]',selected).length == 0) - { - j.show(); - } - }); - } - } - else - { - jQuery(this).addClass("ui-icon-circlesmall-plus") - .removeClass("ui-icon-circlesmall-minus"); - - var group = jQuery(this).parent().children("li").hide(); - } - }); - } - - } - // User - else if (item.value) - { - node = jQuery(document.createElement('li')); - } - node.attr("data-id", item.value); - - jQuery('') - .css("float", "right") - .appendTo(node) - .click(function() { - var button = jQuery(this); - self._add_selected(selected, button.parent().attr("data-id")); - // Hide user, but only hide button for group - if(button.parent().is('li')) - { - button.parent().hide(); - } - else - { - button.hide(); - } - }); - - // If already in list, hide it - if(jQuery('[data-id="'+item.value+'"]',selected).length != 0) - { - node.hide(); - } - - var label = jQuery(document.createElement('label')) - .addClass("loading") - .appendTo(node); - - this.egw().link_title('api-accounts', item.value, function(name) { - label.text(name).removeClass("loading"); - }, label); - - node.appendTo(list); - }, - - _create_selected: function() { - var node = jQuery(document.createElement("div")) - .addClass("et2_selectbox"); - - var header = jQuery(document.createElement("div")) - .addClass("ui-widget-header ui-helper-clearfix") - .appendTo(node); - - var selected = jQuery(document.createElement("ul")) - .addClass("ui-multiselect-checkboxes ui-helper-reset") - .attr("id", this.getInstanceManager().uniqueId + "_selected") - .css("height", "230px") - .appendTo(node); - - jQuery(document.createElement("span")) - .text(this.egw().lang("Selection")) - .addClass("ui-multiselect-header") - .appendTo(header); - - var controls = jQuery(document.createElement("ul")) - .addClass('ui-helper-reset') - .appendTo(header); - - jQuery(document.createElement("li")) - .addClass("et2_clickable") - .click(selected, function(e) {jQuery("li",e.data).remove();}) - .append('') - .appendTo(controls); - - // Add in currently selected - if(this.getValue()) - { - var value = this.getValue(); - for(var i = 0; i < value.length; i++) { - this._add_selected(selected, value[i]); - } - } - return node; - }, - - /** - * Add an option to the list of selected accounts - * value is the account / group ID - * - * @param list - * @param value - */ - _add_selected: function(list, value) { - - // Each option only once - var there = jQuery('[data-id="' + value + '"]',list); - if(there.length) - { - there.show(); - return; - } - - var option = jQuery(document.createElement('li')) - .attr("data-id",value) - .appendTo(list); - jQuery('
          ') - .css("float", "right") - .appendTo(option) - .click(function() { - var id = jQuery(this).parent().attr("data-id"); - jQuery(this).parent().remove(); - // Add 'add' button back, if in results list - list.parents("tr").find("[data-id='"+id+"']").show() - // Show button(s) for group - .children('span').show(); - }); - - var label = jQuery(document.createElement('label')) - .addClass("loading") - .appendTo(option); - this.egw().link_title('api-accounts', value, function(name) {this.text(name).removeClass("loading");}, label); - }, - - /** - * Overwritten attachToDOM metod to modify attachToDOM - */ - attachToDOM: function () - { - this._super.apply(this, arguments); - //Chosen needs to be set after widget dettached from DOM (eg. validation_error), because chosen is not part of the widget node - if (this.egw().preference('account_selection', 'common') == 'primary_group') - { - jQuery(this.node).removeClass('chzn-done'); - this.set_tags(this.options.tags, this.options.width); - } - } -});}).call(this); -et2_register_widget(et2_selectAccount, ["select-account"]); - +var et2_selectAccount = /** @class */ (function (_super) { + __extends(et2_selectAccount, _super); + /** + * Constructor + * + */ + function et2_selectAccount(_parent, _attrs, _child) { + var _this = _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_selectAccount._attributes, _child || {})) || this; + _this.legacyOptions = ['empty_label', 'account_type']; + // Type in rows or somewhere else? + if (jQuery.inArray(_attrs['empty_label'], et2_selectAccount.account_types) > 0 && (jQuery.inArray(_attrs['account_type'], et2_selectAccount.account_types) < 0 || + _attrs['account_type'] == et2_selectAccount._attributes.account_type['default'])) { + _attrs['account_type'] = _attrs['empty_label']; + _attrs['empty_label'] = ''; + } + if (jQuery.inArray(_attrs['account_type'], et2_selectAccount.account_types) < 0) { + _this.egw().debug("warn", "Invalid account_type: %s Valid options:", _attrs['account_type'], et2_selectAccount.account_types); + } + // Holder for search jQuery nodes + _this.search = null; + // Reference to dialog + _this.dialog = null; + // Reference to widget within dialog + _this.widgets = null; + if (!_attrs.empty_label && !_attrs.readonly && _attrs.multiple) { + _attrs.empty_label = _this.egw().lang('Select user or group'); + } + // Allow certain widgets inside this one + _this.supportedWidgetClasses = [et2_widget_link_1.et2_link_entry]; + return _this; + } + et2_selectAccount.prototype.destroy = function () { + _super.prototype.destroy.apply(this, arguments); + }; + /** + * Single selection - override to add search button + */ + et2_selectAccount.prototype.createInputWidget = function () { + var type = this.egw().preference('account_selection', 'common'); + switch (type) { + case 'none': + if (typeof egw.user('apps').admin == 'undefined') { + this.options.select_options = {}; + break; + } + case 'selectbox': + case 'groupmembers': + default: + this.options.select_options = this._get_accounts(); + break; + } + _super.prototype.createInputWidget.call(this); + // Add search button + if (type == 'primary_group') { + var button = jQuery(document.createElement("span")) + .addClass("et2_clickable") + .click(this, jQuery.proxy(function (e) { + // Auto-expand + if (this.options.expand_multiple_rows && !this.options.multiple) { + this.set_multiple(true, this.options.expand_multiple_rows); + } + if (this.options.multiple) { + this._open_multi_search(e); + } + else { + this._open_search(e); + } + }, this)) + .attr("title", egw.lang("popup with search")) + .append(''); + this.getSurroundings().insertDOMNode(button[0]); + } + }; + /** + * Multiple selection - override to add search button + */ + et2_selectAccount.prototype.createMultiSelect = function () { + var type = this.egw().preference('account_selection', 'common'); + if (type == 'none' && typeof egw.user('apps').admin == 'undefined') + return; + _super.prototype.createMultiSelect.call(this); + this.options.select_options = this._get_accounts(); + if (type == 'primary_group') { + // Allow search 'inside' this widget + this.supportedWidgetClasses = [et2_widget_link_1.et2_link_entry]; + // Add quick search - turn off multiple to get normal result list + this.options.multiple = false; + this._create_search(); + // Clear search box after select + var old_select = this.search_widget.select; + var self = this; + // @ts-ignore + this.search_widget.select = function (e, selected) { + var current = self.getValue(); + // Fix ID as sent from server - must be numeric + selected.item.value = parseInt(selected.item.value); + // This one is important, it makes sure the option is there + old_select.apply(this, arguments); + // Add quick search selection into current selection + current.push(selected.item.value); + // Clear search + this.search.val(''); + self.set_value(current); + }; + // Put search results as a DOM sibling of the options, for proper display + this.search_widget.search.on("autocompleteopen", jQuery.proxy(function () { + this.search_widget.search.data("ui-autocomplete").menu.element + .appendTo(this.node) + .position({ my: 'left top', at: 'left bottom', of: this.multiOptions.prev() }); + }, this)); + this.search = jQuery(document.createElement("li")) + .appendTo(this.multiOptions.prev().find('ul')); + this.options.multiple = true; + // Add search button + var button = jQuery(document.createElement("li")) + .addClass("et2_clickable") + .click(this, this._open_multi_search) + .attr("title", egw.lang("popup with search")) + .append(''); + var type = this.egw().preference('account_selection', 'common'); + // Put it last so check/uncheck doesn't move around + this.multiOptions.prev().find('ul') + .append(button); + } + }; + /** + * Override parent to make sure accounts are there as options. + * + * Depending on the widget's attributes and the user's preferences, not all selected + * accounts may be in the cache as options, so we fetch the extras to make sure + * we don't lose any. + * + * As fetching them might only work asynchron (if they are not yet loaded), + * we have to call set_value again, once all labels have arrived from server. + * + * @param {string|array} _value + */ + et2_selectAccount.prototype.set_value = function (_value) { + if (typeof _value == "string" && this.options.multiple && _value.match(this._is_multiple_regexp) !== null) { + _value = _value.split(','); + } + if (_value) { + var search = _value; + if (!jQuery.isArray(search)) { + search = [_value]; + } + var update_options = false; + var num_calls = 0; + var current_call = 0; + for (var j = 0; j < search.length; j++) { + var found = false; + // Not having a value to look up causes an infinite loop + if (!search[j] || search[j] === "0") + continue; + // Options are not indexed, so we must look + for (var i = 0; !found && i < this.options.select_options.length; i++) { + if (typeof this.options.select_options[i] != 'object') { + egw.debug('warn', this.id + ' wrong option ' + i + ' this.options.select_options=', this.options.select_options); + continue; + } + if (this.options.select_options[i].value == search[j]) + found = true; + } + // We only look for numeric IDs, non-numeric IDs cause an exception + if (!found && !isNaN(search[j])) { + // Add it in + var name = this.egw().link_title('api-accounts', search[j]); + if (name) // was already cached on client-side + { + update_options = true; + this.options.select_options.push({ value: search[j], label: name }); + } + else // not available: need to call set_value again, after all arrived from server + { + ++num_calls; + // Add immediately with value as label, we'll replace later + this._appendOptionElement(search[j], search[j]); + this.egw().link_title('api-accounts', search[j], function (name) { + if (++current_call >= num_calls) // only run last callback + { + // Update the label + // Options are not indexed, so we must look + for (var i = 0; i < this.widget.options.select_options.length; i++) { + var opt = this.widget.options.select_options[i]; + if (opt && opt.value && opt.value == this.unknown && opt.label == this.unknown) { + opt.label = name; + this.widget.set_select_options(this.widget.options.select_options); + break; + } + } + this.widget.set_value(_value); + } + }, { widget: this, unknown: search[j] }); + } + } + } + if (update_options) { + this.set_select_options(this.options.select_options); + } + } + _super.prototype.set_value.call(this, _value); + }; + /** + * Get account info for select options from common client-side account cache + * + * @return {Array} select options + */ + et2_selectAccount.prototype._get_accounts = function () { + if (!jQuery.isArray(this.options.select_options)) { + var options = jQuery.extend({}, this.options.select_options); + this.options.select_options = []; + for (var key in options) { + if (typeof options[key] == 'object') { + if (typeof (options[key].key) == 'undefined') { + options[key].value = key; + } + this.options.select_options.push(options[key]); + } + else { + this.options.select_options.push({ value: key, label: options[key] }); + } + } + } + var type = this.egw().preference('account_selection', 'common'); + var accounts = []; + // for primary_group we only display owngroups == own memberships, not other groups + if (type == 'primary_group' && this.options.account_type != 'accounts') { + if (this.options.account_type == 'both') { + accounts = this.egw().accounts('accounts'); + } + accounts = accounts.concat(this.egw().accounts('owngroups')); + } + else { + accounts = this.egw().accounts(this.options.account_type); + } + return this.options.select_options.concat(accounts); + }; + /** + * Create & display a way to search & select a single account / group + * Single selection is just link widget + * + * @param e event + */ + et2_selectAccount.prototype._open_search = function (e) { + var widget = e.data; + var search = widget._create_search(); + // Selecting a single user closes the dialog, this only used if user cleared + var ok_click = function () { + widget.set_value([]); + // Fire change event + if (widget.input) + widget.input.trigger("change"); + jQuery(this).dialog("close"); + }; + widget._create_dialog(search, ok_click); + }; + /** + * Create & display a way to search & select multiple accounts / groups + * + * @param e event + */ + et2_selectAccount.prototype._open_multi_search = function (e) { + var widget = e && e.data ? e.data : this; + var table = widget.search = jQuery('
          '); + table.css("width", "100%").css("height", "100%"); + var search_col = jQuery('#search_col', table); + var select_col = jQuery('#selection_col', table); + // Search / Selection + search_col.append(widget._create_search()); + // Currently selected + select_col.append(widget._create_selected()); + var ok_click = function () { + jQuery(this).dialog("close"); + // Update widget with selected + var ids = []; + var data = {}; + jQuery('#' + widget.getInstanceManager().uniqueId + '_selected li', select_col).each(function () { + var id = jQuery(this).attr("data-id"); + // Add to list + ids.push(id); + // Make sure option is there + if (!widget.options.multiple && jQuery('input[id$="_opt_' + id + '"]', widget.multiOptions).length == 0) { + widget._appendMultiOption(id, jQuery('label', this).text()); + } + else if (widget.options.multiple && jQuery('option[value="' + id + '"]', widget.node).length == 0) { + widget._appendOptionElement(id, jQuery('label', this).text()); + } + }); + widget.set_value(ids); + // Fire change event + if (widget.input) + widget.input.trigger("change"); + }; + var container = jQuery(document.createElement("div")).append(table); + return widget._create_dialog(container, ok_click); + }; + /** + * Create / display popup with search / selection widgets + * + * @param {et2_dialog} widgets + * @param {function} update_function + */ + et2_selectAccount.prototype._create_dialog = function (widgets, update_function) { + this.widgets = widgets; + this.dialog = et2_widget_dialog_1.et2_dialog.show_dialog(false, '', this.options.label ? this.options.label : this.egw().lang('Select'), {}, [{ + text: this.egw().lang("ok"), + image: 'check', + click: update_function + }, { + text: this.egw().lang("cancel"), + image: 'cancel' + }]); + this.dialog.set_dialog_type(''); + // Static size for easier layout + this.dialog.div.dialog({ width: "500", height: "370" }); + this.dialog.div.append(widgets.width('100%')); + return widgets; + }; + /** + * Search is a link-entry widget, with some special display for multi-select + */ + et2_selectAccount.prototype._create_search = function () { + var self = this; + var search = this.search = jQuery(document.createElement("div")); + var search_widget = this.search_widget = et2_createWidget('link-entry', { + 'only_app': 'api-accounts', + 'query': function (request, response) { + // Clear previous search results for multi-select + if (!request.options) { + search.find('#search_results').empty(); + } + // Restrict to specified account type + if (!request.options || !request.options.filter) { + request.options = { account_type: self.options.account_type }; + } + return true; + }, + 'select': function (e, selected) { + // Make sure option is there + var already_there = false; + var last_key = null; + for (last_key in self.options.select_options) { + var option = self.options.select_options[last_key]; + already_there = already_there || (typeof option.value != 'undefined' && option.value == selected.item.value); + } + if (!already_there) { + self.options.select_options[parseInt(last_key) + 1] = selected.item; + self._appendOptionElement(selected.item.value, selected.item.label); + } + self.set_value(selected.item.value); + if (self.dialog) { + self.dialog.div.dialog("close"); + } + // Fire change event + if (self.input) + self.input.trigger("change"); + return true; + } + }, this); + // add it where we want it + search.append(search_widget.getDOMNode()); + if (!this.options.multiple) + return search; + // Multiple is more complicated. It uses a custom display for results to + // allow choosing multiples from a match + var results = jQuery(document.createElement("ul")) + .attr("id", "search_results") + .css("height", "230px") + .addClass("ui-multiselect-checkboxes ui-helper-reset"); + jQuery(document.createElement("div")) + .addClass("et2_selectbox") + .css("height", "100%") + .append(results) + .appendTo(search); + // Override link-entry auto-complete for custom display + // Don't show normal drop-down + search_widget.search.data("ui-autocomplete")._suggest = function (items) { + jQuery.each(items, function (index, item) { + // Make sure value is numeric + item.value = parseInt(item.value); + self._add_search_result(results, item); + }); + }; + return search; + }; + /** + * Add the selected result to the list of search results + * + * @param list + * @param item + */ + et2_selectAccount.prototype._add_search_result = function (list, item) { + var node = null; + var self = this; + // Make sure value is numeric + if (item.value) + item.value = parseInt(item.value); + // (containter of) Currently selected users / groups + var selected = jQuery('#' + this.getInstanceManager().uniqueId + "_selected", this.widgets); + // Group + if (item.value && item.value < 0) { + node = jQuery(document.createElement('ul')); + // Add button to show users + if (this.options.account_type != 'groups') { + jQuery('') + .css("float", "left") + .appendTo(node) + .click(function () { + if (jQuery(this).hasClass("ui-icon-circlesmall-plus")) { + jQuery(this).removeClass("ui-icon-circlesmall-plus") + .addClass("ui-icon-circlesmall-minus"); + var group = jQuery(this).parent() + .addClass("expanded"); + if (group.children("li").length == 0) { + // Fetch group members + self.search_widget.query({ + term: "", + options: { filter: { group: item.value } }, + no_cache: true + }, function (items) { + jQuery(items).each(function (index, item) { + self._add_search_result(node, item); + }); + }); + } + else { + group.children("li") + // Only show children that are not selected + .each(function (index, item) { + var j = jQuery(item); + if (jQuery('[data-id="' + j.attr("data-id") + '"]', selected).length == 0) { + j.show(); + } + }); + } + } + else { + jQuery(this).addClass("ui-icon-circlesmall-plus") + .removeClass("ui-icon-circlesmall-minus"); + var group = jQuery(this).parent().children("li").hide(); + } + }); + } + } + // User + else if (item.value) { + node = jQuery(document.createElement('li')); + } + node.attr("data-id", item.value); + jQuery('') + .css("float", "right") + .appendTo(node) + .click(function () { + var button = jQuery(this); + self._add_selected(selected, button.parent().attr("data-id")); + // Hide user, but only hide button for group + if (button.parent().is('li')) { + button.parent().hide(); + } + else { + button.hide(); + } + }); + // If already in list, hide it + if (jQuery('[data-id="' + item.value + '"]', selected).length != 0) { + node.hide(); + } + var label = jQuery(document.createElement('label')) + .addClass("loading") + .appendTo(node); + this.egw().link_title('api-accounts', item.value, function (name) { + label.text(name).removeClass("loading"); + }, label); + node.appendTo(list); + }; + et2_selectAccount.prototype._create_selected = function () { + var node = jQuery(document.createElement("div")) + .addClass("et2_selectbox"); + var header = jQuery(document.createElement("div")) + .addClass("ui-widget-header ui-helper-clearfix") + .appendTo(node); + var selected = jQuery(document.createElement("ul")) + .addClass("ui-multiselect-checkboxes ui-helper-reset") + .attr("id", this.getInstanceManager().uniqueId + "_selected") + .css("height", "230px") + .appendTo(node); + jQuery(document.createElement("span")) + .text(this.egw().lang("Selection")) + .addClass("ui-multiselect-header") + .appendTo(header); + var controls = jQuery(document.createElement("ul")) + .addClass('ui-helper-reset') + .appendTo(header); + jQuery(document.createElement("li")) + .addClass("et2_clickable") + .click(selected, function (e) { jQuery("li", e.data).remove(); }) + .append('') + .appendTo(controls); + // Add in currently selected + if (this.getValue()) { + var value = this.getValue(); + for (var i = 0; i < value.length; i++) { + this._add_selected(selected, value[i]); + } + } + return node; + }; + /** + * Add an option to the list of selected accounts + * value is the account / group ID + * + * @param list + * @param value + */ + et2_selectAccount.prototype._add_selected = function (list, value) { + // Each option only once + var there = jQuery('[data-id="' + value + '"]', list); + if (there.length) { + there.show(); + return; + } + var option = jQuery(document.createElement('li')) + .attr("data-id", value) + .appendTo(list); + jQuery('
          ') + .css("float", "right") + .appendTo(option) + .click(function () { + var id = jQuery(this).parent().attr("data-id"); + jQuery(this).parent().remove(); + // Add 'add' button back, if in results list + list.parents("tr").find("[data-id='" + id + "']").show() + // Show button(s) for group + .children('span').show(); + }); + var label = jQuery(document.createElement('label')) + .addClass("loading") + .appendTo(option); + this.egw().link_title('api-accounts', value, function (name) { this.text(name).removeClass("loading"); }, label); + }; + /** + * Overwritten attachToDOM method to modify attachToDOM + */ + et2_selectAccount.prototype.attachToDOM = function () { + var result = _super.prototype.attachToDOM.call(this); + //Chosen needs to be set after widget dettached from DOM (eg. validation_error), because chosen is not part of the widget node + if (this.egw().preference('account_selection', 'common') == 'primary_group') { + jQuery(this.node).removeClass('chzn-done'); + this.set_tags(this.options.tags, this.options.width); + } + return result; + }; + et2_selectAccount._attributes = { + 'account_type': { + 'name': 'Account type', + 'default': 'accounts', + 'type': 'string', + 'description': 'Limit type of accounts. One of {accounts,groups,both,owngroups}.' + } + }; + et2_selectAccount.account_types = ['accounts', 'groups', 'both', 'owngroups']; + return et2_selectAccount; +}(et2_widget_selectbox_1.et2_selectbox)); +exports.et2_selectAccount = et2_selectAccount; +et2_core_widget_1.et2_register_widget(et2_selectAccount, ["select-account"]); /** * et2_selectAccount_ro is the readonly implementation of select account * It extends et2_link to avoid needing the whole user list on the client. @@ -750,139 +619,109 @@ et2_register_widget(et2_selectAccount, ["select-account"]); * * @augments et2_link_string */ -var et2_selectAccount_ro = (function(){ "use strict"; return et2_link_string.extend([et2_IDetachedDOM], -{ - attributes: { - "empty_label": { - "name": "Empty label", - "type": "string", - "default": "", - "description": "Textual label for first row, eg: 'All' or 'None'. ID will be ''", - translate:true - } - }, - - legacyOptions: ["empty_label"], - - /** - * Constructor - * - * @param _parent - * @param options - * @memberOf et2_selectAccount_ro - */ - init: function(_parent, options) { - /** - Resolve some circular dependency problems here - selectAccount extends link, link is in a file that needs select, - select has menulist wrapper, which needs to know about selectAccount before it allows it - */ - if(_parent.supportedWidgetClasses.indexOf(et2_selectAccount_ro) < 0) - { - _parent.supportedWidgetClasses.push(et2_selectAccount_ro); - } - - this._super.apply(this, arguments); - - // Legacy options could have row count or empty label in first slot - if(typeof this.options.empty_label == "string") - { - if(isNaN(this.options.empty_label)) - { - this.options.empty_label = this.egw().lang(this.options.empty_label); - } - } - - this.options.application = 'api-accounts'; - - // Editable version allows app to set options that aren't accounts, so allow for them - var options = et2_selectbox.find_select_options(this,options['select_options']); - if(!jQuery.isEmptyObject(options)) - { - this.options.select_options = options; - } - - // Don't make it look like a link though - this.list.removeClass("et2_link_string").addClass("et2_selectbox"); - }, - - transformAttributes: function(_attrs) { - et2_selectbox.prototype.transformAttributes.apply(this, arguments); - }, - - set_value: function(_value) { - // Explode csv - if(typeof _value == 'string' && _value.indexOf(',') > 0) - { - _value = _value.split(','); - } - - // Don't bother to lookup if it's not an array, or a number - if(typeof _value == 'object' || !isNaN(_value) && _value != "") - { - this._super.apply(this, arguments); - // Don't make it look like a link though - jQuery('li',this.list).removeClass("et2_link et2_link_string") - // No clicks either - .off(); - return; - } - - // Don't make it look like a link - jQuery('li',this.list).removeClass("et2_link et2_link_string") - // No clicks either - .off(); - - if(this.options.select_options && !jQuery.isEmptyObject(this.options.select_options) || this.options.empty_label) - { - if(!_value) - { - // Empty label from selectbox - this.list.append("
        • "+this.options.empty_label+"
        • "); - } - else if (typeof _value == 'object') - { - // An array with 0 / empty in it? - for(var i = 0; i < _value.length; i++) - { - if(!_value[i] || !parseInt(_value[i])) - { - this.list.append("
        • "+this.options.empty_label+"
        • "); - return; - } - else if (this.options.select_options[_value]) - { - this.list.append("
        • "+this.options.select_options[_value]+"
        • "); - } - } - } - else - { - // Options are not indexed, so we must look - var search = _value; - if (!jQuery.isArray(search)) - { - search = [_value]; - } - for(var j = 0; j < search.length; j++) - { - var found = false; - - // Not having a value to look up causes an infinite loop - if(!search[j]) continue; - - for(var i in this.options.select_options) - { - if(this.options.select_options[i].value == search[j]) - { - this.list.append("
        • "+this.options.select_options[i].label+"
        • "); - break; - } - } - - } - } - } - } -});}).call(this); -et2_register_widget(et2_selectAccount_ro, ["select-account_ro"]); +var et2_selectAccount_ro = /** @class */ (function (_super) { + __extends(et2_selectAccount_ro, _super); + /** + * Constructor + */ + function et2_selectAccount_ro(_parent, _attrs, _child) { + var _this = _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_selectAccount_ro._attributes, _child || {})) || this; + _this.legacyOptions = ["empty_label"]; + /** + Resolve some circular dependency problems here + selectAccount extends link, link is in a file that needs select, + select has menulist wrapper, which needs to know about selectAccount before it allows it + */ + if (_parent.supportedWidgetClasses.indexOf(et2_selectAccount_ro) < 0) { + _parent.supportedWidgetClasses.push(et2_selectAccount_ro); + _parent.addChild(_this); + } + // Legacy options could have row count or empty label in first slot + if (typeof _this.options.empty_label == "string") { + if (isNaN(_this.options.empty_label)) { + _this.options.empty_label = _this.egw().lang(_this.options.empty_label); + } + } + _this.options.application = 'api-accounts'; + // Editable version allows app to set options that aren't accounts, so allow for them + var options = et2_widget_selectbox_1.et2_selectbox.find_select_options(_this, _attrs['select_options'], _this.options); + if (!jQuery.isEmptyObject(options)) { + _this.options.select_options = options; + } + // Don't make it look like a link though + _this.list.removeClass("et2_link_string").addClass("et2_selectbox"); + return _this; + } + et2_selectAccount_ro.prototype.transformAttributes = function (_attrs) { + et2_widget_selectbox_1.et2_selectbox.prototype.transformAttributes.apply(this, arguments); + }; + et2_selectAccount_ro.prototype.set_value = function (_value) { + // Explode csv + if (typeof _value == 'string' && _value.indexOf(',') > 0) { + _value = _value.split(','); + } + // Don't bother to lookup if it's not an array, or a number + if (typeof _value == 'object' || !isNaN(_value) && _value != "") { + _super.prototype.set_value.call(this, _value); + // Don't make it look like a link though + jQuery('li', this.list).removeClass("et2_link et2_link_string") + // No clicks either + .off(); + return; + } + // Don't make it look like a link + jQuery('li', this.list).removeClass("et2_link et2_link_string") + // No clicks either + .off(); + if (this.options.select_options && !jQuery.isEmptyObject(this.options.select_options) || this.options.empty_label) { + if (!_value) { + // Empty label from selectbox + this.list.append("
        • " + this.options.empty_label + "
        • "); + } + else if (typeof _value == 'object') { + // An array with 0 / empty in it? + for (var i = 0; i < _value.length; i++) { + if (!_value[i] || !parseInt(_value[i])) { + this.list.append("
        • " + this.options.empty_label + "
        • "); + return; + } + else if (this.options.select_options[_value]) { + this.list.append("
        • " + this.options.select_options[_value] + "
        • "); + } + } + } + else { + // Options are not indexed, so we must look + var search = _value; + if (!jQuery.isArray(search)) { + search = [_value]; + } + for (var j = 0; j < search.length; j++) { + var found = false; + // Not having a value to look up causes an infinite loop + if (!search[j]) + continue; + for (var i in this.options.select_options) { + if (this.options.select_options[i].value == search[j]) { + this.list.append("
        • " + this.options.select_options[i].label + "
        • "); + break; + } + } + } + } + } + }; + et2_selectAccount_ro._attributes = { + "empty_label": { + "name": "Empty label", + "type": "string", + "default": "", + "description": "Textual label for first row, eg: 'All' or 'None'. ID will be ''", + translate: true + } + }; + return et2_selectAccount_ro; +}(et2_widget_link_2.et2_link_string)); +exports.et2_selectAccount_ro = et2_selectAccount_ro; +et2_core_widget_1.et2_register_widget(et2_selectAccount_ro, ["select-account_ro"]); +//# sourceMappingURL=et2_widget_selectAccount.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_selectAccount.ts b/api/js/etemplate/et2_widget_selectAccount.ts new file mode 100644 index 0000000000..1c8e1409be --- /dev/null +++ b/api/js/etemplate/et2_widget_selectAccount.ts @@ -0,0 +1,893 @@ +/** + * EGroupware eTemplate2 - JS Select account widget + * + * Selecting accounts needs special UI, and displaying needs special consideration + * to avoid sending the entire user list to the client. + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage api + * @link http://www.egroupware.org + * @author Nathan Gray + * @copyright Nathan Gray 2012 + * @version $Id$ + */ + +/*egw:uses + et2_widget_link; +*/ + +import {et2_selectbox} from "./et2_widget_selectbox"; +import {et2_register_widget, WidgetConfig, et2_widget} from "./et2_core_widget"; +import {ClassWithAttributes} from "./et2_core_inheritance"; +import {et2_link_entry} from "./et2_widget_link"; +import {et2_dialog} from "./et2_widget_dialog"; +import {et2_link_string} from "./et2_widget_link"; + +/** + * Account selection widget + * Changes according to the user's account_selection preference + * - 'none' => Server-side: the read-only widget is used, and no values are sent or displayed + * - 'groupmembers' => Non admins can only select groupmembers (Server side - normal selectbox) + * - 'selectbox' => Selectbox with all accounts and groups (Server side - normal selectbox) + * - 'primary_group' => Selectbox with primary group and search + * + * Only primary_group and popup need anything different from a normal selectbox + * + */ +export class et2_selectAccount extends et2_selectbox +{ + static readonly _attributes : any = { + 'account_type': { + 'name': 'Account type', + 'default': 'accounts', + 'type': 'string', + 'description': 'Limit type of accounts. One of {accounts,groups,both,owngroups}.' + } + }; + + public readonly legacyOptions = ['empty_label','account_type']; + + public static readonly account_types = ['accounts','groups','both','owngroups']; + private search: JQuery; + private dialog: et2_dialog; + private widgets: any; + private search_widget: et2_link_entry; + + /** + * Constructor + * + */ + constructor(_parent : et2_widget, _attrs? : WidgetConfig, _child? : object) + { + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_selectAccount._attributes, _child || {})); + + // Type in rows or somewhere else? + if(jQuery.inArray(_attrs['empty_label'], et2_selectAccount.account_types) > 0 && ( + jQuery.inArray(_attrs['account_type'], et2_selectAccount.account_types) < 0 || + _attrs['account_type'] == et2_selectAccount._attributes.account_type['default']) + ) + { + _attrs['account_type'] = _attrs['empty_label']; + _attrs['empty_label'] = ''; + } + if(jQuery.inArray(_attrs['account_type'], et2_selectAccount.account_types) < 0) + { + this.egw().debug("warn", "Invalid account_type: %s Valid options:",_attrs['account_type'], et2_selectAccount.account_types); + } + + // Holder for search jQuery nodes + this.search = null; + + // Reference to dialog + this.dialog = null; + + // Reference to widget within dialog + this.widgets = null; + + if(!_attrs.empty_label && !_attrs.readonly && _attrs.multiple) + { + _attrs.empty_label = this.egw().lang('Select user or group'); + } + + // Allow certain widgets inside this one + this.supportedWidgetClasses = [et2_link_entry]; + } + + destroy( ) + { + super.destroy.apply(this, arguments); + } + + /** + * Single selection - override to add search button + */ + createInputWidget() + { + var type = this.egw().preference('account_selection', 'common'); + + switch(type) + { + case 'none': + if(typeof egw.user('apps').admin == 'undefined') + { + this.options.select_options = {}; + break; + } + case 'selectbox': + case 'groupmembers': + default: + this.options.select_options = this._get_accounts(); + break; + } + + super.createInputWidget(); + + // Add search button + if(type == 'primary_group') + { + var button = jQuery(document.createElement("span")) + .addClass("et2_clickable") + .click(this, jQuery.proxy(function(e) { + // Auto-expand + if(this.options.expand_multiple_rows && !this.options.multiple) + { + this.set_multiple(true, this.options.expand_multiple_rows); + } + + if(this.options.multiple) + { + this._open_multi_search(e); + } + else + { + this._open_search(e); + } + },this)) + .attr("title", egw.lang("popup with search")) + .append(''); + + this.getSurroundings().insertDOMNode(button[0]); + } + } + + /** + * Multiple selection - override to add search button + */ + createMultiSelect( ) + { + + var type = this.egw().preference('account_selection', 'common'); + if(type == 'none' && typeof egw.user('apps').admin == 'undefined') return; + + super.createMultiSelect(); + + this.options.select_options = this._get_accounts(); + + if(type == 'primary_group') + { + // Allow search 'inside' this widget + this.supportedWidgetClasses = [et2_link_entry]; + + // Add quick search - turn off multiple to get normal result list + this.options.multiple = false; + this._create_search(); + + // Clear search box after select + var old_select = this.search_widget.select; + var self = this; + // @ts-ignore + this.search_widget.select = function(e, selected) { + var current = self.getValue(); + + // Fix ID as sent from server - must be numeric + selected.item.value = parseInt(selected.item.value); + + // This one is important, it makes sure the option is there + old_select.apply(this, arguments); + + // Add quick search selection into current selection + current.push(selected.item.value); + + // Clear search + this.search.val(''); + + self.set_value(current); + }; + + // Put search results as a DOM sibling of the options, for proper display + this.search_widget.search.on("autocompleteopen", jQuery.proxy(function() { + this.search_widget.search.data("ui-autocomplete").menu.element + .appendTo(this.node) + .position({my: 'left top', at: 'left bottom', of: this.multiOptions.prev()}); + },this)); + this.search = jQuery(document.createElement("li")) + .appendTo(this.multiOptions.prev().find('ul')); + this.options.multiple = true; + + // Add search button + var button = jQuery(document.createElement("li")) + .addClass("et2_clickable") + .click(this, this._open_multi_search) + .attr("title", egw.lang("popup with search")) + .append(''); + var type = this.egw().preference('account_selection', 'common'); + + // Put it last so check/uncheck doesn't move around + this.multiOptions.prev().find('ul') + .append(button); + } + } + + /** + * Override parent to make sure accounts are there as options. + * + * Depending on the widget's attributes and the user's preferences, not all selected + * accounts may be in the cache as options, so we fetch the extras to make sure + * we don't lose any. + * + * As fetching them might only work asynchron (if they are not yet loaded), + * we have to call set_value again, once all labels have arrived from server. + * + * @param {string|array} _value + */ + set_value(_value) + { + if(typeof _value == "string" && this.options.multiple && _value.match(this._is_multiple_regexp) !== null) + { + _value = _value.split(','); + } + + if(_value) + { + var search = _value; + if (!jQuery.isArray(search)) + { + search = [_value]; + } + var update_options = false; + var num_calls = 0; + var current_call = 0; + for(var j = 0; j < search.length; j++) + { + var found = false; + + // Not having a value to look up causes an infinite loop + if(!search[j] || search[j] === "0") continue; + + // Options are not indexed, so we must look + for(var i = 0; !found && i < this.options.select_options.length; i++) + { + if (typeof this.options.select_options[i] != 'object') + { + egw.debug('warn',this.id + ' wrong option '+i+' this.options.select_options=', this.options.select_options); + continue; + } + if(this.options.select_options[i].value == search[j]) found = true; + } + // We only look for numeric IDs, non-numeric IDs cause an exception + if(!found && !isNaN(search[j])) + { + // Add it in + var name = this.egw().link_title('api-accounts', search[j]); + if (name) // was already cached on client-side + { + update_options = true; + this.options.select_options.push({value: search[j], label:name}); + } + else // not available: need to call set_value again, after all arrived from server + { + ++num_calls; + // Add immediately with value as label, we'll replace later + this._appendOptionElement(search[j],search[j]); + this.egw().link_title('api-accounts', search[j], function(name) + { + if (++current_call >= num_calls) // only run last callback + { + // Update the label + // Options are not indexed, so we must look + for(var i = 0; i < this.widget.options.select_options.length; i++) + { + var opt = this.widget.options.select_options[i]; + if(opt && opt.value && opt.value == this.unknown && opt.label == this.unknown) + { + opt.label = name; + this.widget.set_select_options(this.widget.options.select_options); + break; + } + } + this.widget.set_value(_value); + } + }, {widget: this, unknown: search[j]}); + } + } + } + if(update_options) + { + this.set_select_options(this.options.select_options); + } + } + super.set_value(_value); + } + + /** + * Get account info for select options from common client-side account cache + * + * @return {Array} select options + */ + _get_accounts() + { + if (!jQuery.isArray(this.options.select_options)) + { + var options = jQuery.extend({}, this.options.select_options); + this.options.select_options = []; + for(var key in options) + { + if (typeof options[key] == 'object') + { + if (typeof(options[key].key) == 'undefined') + { + options[key].value = key; + } + this.options.select_options.push(options[key]); + } + else + { + this.options.select_options.push({value: key, label: options[key]}); + } + } + } + var type = this.egw().preference('account_selection', 'common'); + var accounts = []; + // for primary_group we only display owngroups == own memberships, not other groups + if (type == 'primary_group' && this.options.account_type != 'accounts') + { + if (this.options.account_type == 'both') + { + accounts = this.egw().accounts('accounts'); + } + accounts = accounts.concat(this.egw().accounts('owngroups')); + } + else + { + accounts = this.egw().accounts(this.options.account_type); + } + return this.options.select_options.concat(accounts); + } + + /** + * Create & display a way to search & select a single account / group + * Single selection is just link widget + * + * @param e event + */ + _open_search( e) + { + var widget = e.data; + var search = widget._create_search(); + + // Selecting a single user closes the dialog, this only used if user cleared + var ok_click = function() { + widget.set_value([]); + // Fire change event + if(widget.input) widget.input.trigger("change"); + jQuery(this).dialog("close"); + }; + widget._create_dialog(search, ok_click); + } + + /** + * Create & display a way to search & select multiple accounts / groups + * + * @param e event + */ + _open_multi_search( e) + { + var widget = e && e.data ? e.data : this; + var table = widget.search = jQuery('
          '); + table.css("width", "100%").css("height", "100%"); + var search_col = jQuery('#search_col',table); + var select_col = jQuery('#selection_col',table); + + // Search / Selection + search_col.append(widget._create_search()); + + // Currently selected + select_col.append(widget._create_selected()); + + var ok_click = function() { + jQuery(this).dialog("close"); + // Update widget with selected + var ids = []; + var data = {}; + jQuery('#'+widget.getInstanceManager().uniqueId + '_selected li',select_col).each(function() { + var id = jQuery(this).attr("data-id"); + // Add to list + ids.push(id); + + // Make sure option is there + if(!widget.options.multiple && jQuery('input[id$="_opt_'+id+'"]',widget.multiOptions).length == 0) + { + widget._appendMultiOption(id,jQuery('label',this).text()); + } + else if (widget.options.multiple && jQuery('option[value="'+id+'"]',widget.node).length == 0) + { + widget._appendOptionElement(id,jQuery('label',this).text()); + } + }); + + widget.set_value(ids); + + // Fire change event + if(widget.input) widget.input.trigger("change"); + }; + + var container = jQuery(document.createElement("div")).append(table); + return widget._create_dialog(container, ok_click); + } + + /** + * Create / display popup with search / selection widgets + * + * @param {et2_dialog} widgets + * @param {function} update_function + */ + _create_dialog( widgets, update_function) + { + this.widgets = widgets; + this.dialog = et2_dialog.show_dialog(false, + '', + this.options.label ? this.options.label : this.egw().lang('Select'), + {}, + [{ + text: this.egw().lang("ok"), + image: 'check', + click: update_function + },{ + text: this.egw().lang("cancel"), + image: 'cancel' + }] + ); + this.dialog.set_dialog_type(''); + // Static size for easier layout + this.dialog.div.dialog({width: "500", height: "370"}); + + this.dialog.div.append(widgets.width('100%')); + return widgets; + } + + /** + * Search is a link-entry widget, with some special display for multi-select + */ + _create_search( ) + { + var self = this; + var search = this.search = jQuery(document.createElement("div")); + + var search_widget = this.search_widget = et2_createWidget('link-entry', { + 'only_app': 'api-accounts', + 'query'( request, response) + { + // Clear previous search results for multi-select + if(!request.options) + { + search.find('#search_results').empty(); + } + // Restrict to specified account type + if(!request.options || !request.options.filter) + { + request.options = {account_type: self.options.account_type}; + } + return true; + }, + 'select'( e, selected) + { + // Make sure option is there + var already_there = false; + var last_key = null; + for(last_key in self.options.select_options) + { + var option = self.options.select_options[last_key]; + already_there = already_there || (typeof option.value != 'undefined' && option.value == selected.item.value); + } + if(!already_there) + { + self.options.select_options[parseInt(last_key)+1] = selected.item; + self._appendOptionElement(selected.item.value, selected.item.label); + } + self.set_value(selected.item.value); + if(self.dialog) + { + self.dialog.div.dialog("close"); + } + // Fire change event + if(self.input) self.input.trigger("change"); + return true; + } + }, this); + // add it where we want it + search.append(search_widget.getDOMNode()); + + if(!this.options.multiple) return search; + + // Multiple is more complicated. It uses a custom display for results to + // allow choosing multiples from a match + var results = jQuery(document.createElement("ul")) + .attr("id", "search_results") + .css("height", "230px") + .addClass("ui-multiselect-checkboxes ui-helper-reset"); + jQuery(document.createElement("div")) + .addClass("et2_selectbox") + .css("height", "100%") + .append(results) + .appendTo(search); + + // Override link-entry auto-complete for custom display + // Don't show normal drop-down + search_widget.search.data("ui-autocomplete")._suggest = function(items) { + jQuery.each(items, function (index, item) { + // Make sure value is numeric + item.value = parseInt(item.value); + self._add_search_result(results, item); + }); + }; + + return search; + } + + /** + * Add the selected result to the list of search results + * + * @param list + * @param item + */ + _add_search_result( list, item) + { + + var node = null; + var self = this; + + // Make sure value is numeric + if(item.value) item.value = parseInt(item.value); + + // (containter of) Currently selected users / groups + var selected = jQuery('#'+this.getInstanceManager().uniqueId + "_selected", this.widgets); + + // Group + if(item.value && item.value < 0) + { + node = jQuery(document.createElement('ul')); + // Add button to show users + if(this.options.account_type != 'groups') + { + jQuery('') + .css("float", "left") + .appendTo(node) + .click(function() { + if(jQuery(this).hasClass("ui-icon-circlesmall-plus")) + { + jQuery(this).removeClass("ui-icon-circlesmall-plus") + .addClass("ui-icon-circlesmall-minus"); + + var group = jQuery(this).parent() + .addClass("expanded"); + + if(group.children("li").length == 0) + { + // Fetch group members + self.search_widget.query({ + term:"", + options: {filter:{group: item.value}}, + no_cache:true + }, function(items) { + jQuery(items).each(function(index,item) { + self._add_search_result(node, item); + }); + }); + } + else + { + group.children("li") + // Only show children that are not selected + .each(function(index, item) { + var j = jQuery(item); + if(jQuery('[data-id="'+j.attr("data-id")+'"]',selected).length == 0) + { + j.show(); + } + }); + } + } + else + { + jQuery(this).addClass("ui-icon-circlesmall-plus") + .removeClass("ui-icon-circlesmall-minus"); + + var group = jQuery(this).parent().children("li").hide(); + } + }); + } + + } + // User + else if (item.value) + { + node = jQuery(document.createElement('li')); + } + node.attr("data-id", item.value); + + jQuery('') + .css("float", "right") + .appendTo(node) + .click(function() { + var button = jQuery(this); + self._add_selected(selected, button.parent().attr("data-id")); + // Hide user, but only hide button for group + if(button.parent().is('li')) + { + button.parent().hide(); + } + else + { + button.hide(); + } + }); + + // If already in list, hide it + if(jQuery('[data-id="'+item.value+'"]',selected).length != 0) + { + node.hide(); + } + + var label = jQuery(document.createElement('label')) + .addClass("loading") + .appendTo(node); + + this.egw().link_title('api-accounts', item.value, function(name) { + label.text(name).removeClass("loading"); + }, label); + + node.appendTo(list); + } + + _create_selected( ) + { + var node = jQuery(document.createElement("div")) + .addClass("et2_selectbox"); + + var header = jQuery(document.createElement("div")) + .addClass("ui-widget-header ui-helper-clearfix") + .appendTo(node); + + var selected = jQuery(document.createElement("ul")) + .addClass("ui-multiselect-checkboxes ui-helper-reset") + .attr("id", this.getInstanceManager().uniqueId + "_selected") + .css("height", "230px") + .appendTo(node); + + jQuery(document.createElement("span")) + .text(this.egw().lang("Selection")) + .addClass("ui-multiselect-header") + .appendTo(header); + + var controls = jQuery(document.createElement("ul")) + .addClass('ui-helper-reset') + .appendTo(header); + + jQuery(document.createElement("li")) + .addClass("et2_clickable") + .click(selected, function(e) {jQuery("li",e.data).remove();}) + .append('') + .appendTo(controls); + + // Add in currently selected + if(this.getValue()) + { + var value = this.getValue(); + for(var i = 0; i < value.length; i++) { + this._add_selected(selected, value[i]); + } + } + return node; + } + + /** + * Add an option to the list of selected accounts + * value is the account / group ID + * + * @param list + * @param value + */ + _add_selected( list, value) + { + + // Each option only once + var there = jQuery('[data-id="' + value + '"]',list); + if(there.length) + { + there.show(); + return; + } + + var option = jQuery(document.createElement('li')) + .attr("data-id",value) + .appendTo(list); + jQuery('
          ') + .css("float", "right") + .appendTo(option) + .click(function() { + var id = jQuery(this).parent().attr("data-id"); + jQuery(this).parent().remove(); + // Add 'add' button back, if in results list + list.parents("tr").find("[data-id='"+id+"']").show() + // Show button(s) for group + .children('span').show(); + }); + + var label = jQuery(document.createElement('label')) + .addClass("loading") + .appendTo(option); + this.egw().link_title('api-accounts', value, function(name) {this.text(name).removeClass("loading");}, label); + } + + /** + * Overwritten attachToDOM method to modify attachToDOM + */ + attachToDOM() + { + let result = super.attachToDOM(); + //Chosen needs to be set after widget dettached from DOM (eg. validation_error), because chosen is not part of the widget node + if (this.egw().preference('account_selection', 'common') == 'primary_group') + { + jQuery(this.node).removeClass('chzn-done'); + this.set_tags(this.options.tags, this.options.width); + } + + return result; + } +} +et2_register_widget(et2_selectAccount, ["select-account"]); + +/** + * et2_selectAccount_ro is the readonly implementation of select account + * It extends et2_link to avoid needing the whole user list on the client. + * Instead, it just asks for the names of the ones needed, as needed. + * + * @augments et2_link_string + */ +export class et2_selectAccount_ro extends et2_link_string +{ + static readonly _attributes : any = { + "empty_label": { + "name": "Empty label", + "type": "string", + "default": "", + "description": "Textual label for first row, eg: 'All' or 'None'. ID will be ''", + translate:true + } + }; + + legacyOptions : string[] = ["empty_label"]; + + /** + * Constructor + */ + constructor(_parent : et2_widget, _attrs? : WidgetConfig, _child? : object) + { + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_selectAccount_ro._attributes, _child || {})); + + /** + Resolve some circular dependency problems here + selectAccount extends link, link is in a file that needs select, + select has menulist wrapper, which needs to know about selectAccount before it allows it + */ + if(_parent.supportedWidgetClasses.indexOf(et2_selectAccount_ro) < 0) + { + _parent.supportedWidgetClasses.push(et2_selectAccount_ro); + _parent.addChild(this); + } + + // Legacy options could have row count or empty label in first slot + if(typeof this.options.empty_label == "string") + { + if(isNaN(this.options.empty_label)) + { + this.options.empty_label = this.egw().lang(this.options.empty_label); + } + } + + this.options.application = 'api-accounts'; + + // Editable version allows app to set options that aren't accounts, so allow for them + let options = et2_selectbox.find_select_options(this,_attrs['select_options'], this.options); + if(!jQuery.isEmptyObject(options)) + { + this.options.select_options = options; + } + + // Don't make it look like a link though + this.list.removeClass("et2_link_string").addClass("et2_selectbox"); + } + + transformAttributes( _attrs) + { + et2_selectbox.prototype.transformAttributes.apply(this, arguments); + } + + set_value( _value) + { + // Explode csv + if(typeof _value == 'string' && _value.indexOf(',') > 0) + { + _value = _value.split(','); + } + + // Don't bother to lookup if it's not an array, or a number + if(typeof _value == 'object' || !isNaN(_value) && _value != "") + { + super.set_value(_value); + // Don't make it look like a link though + jQuery('li',this.list).removeClass("et2_link et2_link_string") + // No clicks either + .off(); + return; + } + + // Don't make it look like a link + jQuery('li',this.list).removeClass("et2_link et2_link_string") + // No clicks either + .off(); + + if(this.options.select_options && !jQuery.isEmptyObject(this.options.select_options) || this.options.empty_label) + { + if(!_value) + { + // Empty label from selectbox + this.list.append("
        • "+this.options.empty_label+"
        • "); + } + else if (typeof _value == 'object') + { + // An array with 0 / empty in it? + for(let i = 0; i < _value.length; i++) + { + if(!_value[i] || !parseInt(_value[i])) + { + this.list.append("
        • "+this.options.empty_label+"
        • "); + return; + } + else if (this.options.select_options[_value]) + { + this.list.append("
        • "+this.options.select_options[_value]+"
        • "); + } + } + } + else + { + // Options are not indexed, so we must look + var search = _value; + if (!jQuery.isArray(search)) + { + search = [_value]; + } + for(let j = 0; j < search.length; j++) + { + var found = false; + + // Not having a value to look up causes an infinite loop + if(!search[j]) continue; + + for(let i in this.options.select_options) + { + if(this.options.select_options[i].value == search[j]) + { + this.list.append("
        • "+this.options.select_options[i].label+"
        • "); + break; + } + } + + } + } + } + } +} +et2_register_widget(et2_selectAccount_ro, ["select-account_ro"]); diff --git a/api/js/etemplate/et2_widget_selectbox.js b/api/js/etemplate/et2_widget_selectbox.js index 47ba765f81..f35684d00b 100644 --- a/api/js/etemplate/et2_widget_selectbox.js +++ b/api/js/etemplate/et2_widget_selectbox.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - JS Selectbox object * @@ -8,1788 +9,1502 @@ * @author Nathan Gray * @author Andreas Stöckel * @copyright Nathan Gray 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 - /vendor/bower-asset/jquery/dist/jquery.js; - /api/js/jquery/chosen/chosen.jquery.js; - et2_core_xml; - et2_core_DOMWidget; - et2_core_inputWidget; + /vendor/bower-asset/jquery/dist/jquery.js; + /api/js/jquery/chosen/chosen.jquery.js; + et2_core_xml; + et2_core_DOMWidget; + et2_core_inputWidget; */ - +require("./et2_core_common"); +var et2_core_inheritance_1 = require("./et2_core_inheritance"); +var et2_core_widget_1 = require("./et2_core_widget"); +var et2_core_inputWidget_1 = require("./et2_core_inputWidget"); +var et2_core_DOMWidget_1 = require("./et2_core_DOMWidget"); +// all calls to Chosen jQuery plugin as jQuery.(un)chosen() give errors which are currently suppressed with @ts-ignore +// adding npm package @types/chosen-js did NOT help :( /** - * @augments et2_inputWidget + * et2 select(box) widget */ -var et2_selectbox = (function(){ "use strict"; return et2_inputWidget.extend( -{ - attributes: { - // todo fully implement attr[multiple] === "dynamic" to render widget with a button to switch to multiple - // as it is used in account_id selection in admin >> mailaccount (app.admin.edit_multiple method client-side) - "multiple": { - "name": "multiple", - "type": "boolean", - "default": false, - "description": "Allow selecting multiple options" - }, - "expand_multiple_rows": { - "name": "Expand multiple", - "type": "integer", - "default": et2_no_init, - "description": "Shows single select widget, with a button. If the "+ - "user clicks the button, the input will toggle to a multiselect,"+ - "with this many rows. " - }, - "rows": { - "name": "Rows", - "type": "any", // Old options put either rows or empty_label in first space - "default": 1, - "description": "Number of rows to display" - }, - "empty_label": { - "name": "Empty label", - "type": "string", - "default": "", - "description": "Textual label for first row, eg: 'All' or 'None'. ID will be ''", - translate:true - }, - "select_options": { - "type": "any", - "name": "Select options", - "default": {}, - "description": "Internaly used to hold the select options." - }, - "selected_first": { - "name": "Selected options first", - "type": "boolean", - "default": true, - "description": "For multi-selects, put the selected options at the top of the list when first loaded" - }, - - // Chosen options - "search": { - "name": "Search", - "type": "boolean", - "default": false, - "description": "For single selects, add a search box to the drop-down list" - }, - "tags": { - "name": "Tag style", - "type": "boolean", - "default": false, - "description": "For multi-selects, displays selected as a list of tags instead of a big list" - }, - "allow_single_deselect": { - "name": "Allow Single Deselect", - "type": "boolean", - "default": true, - "description": "Allow user to unset current selected value" - }, - - // Value can be string or integer - "value": { - "type": "any" - }, - // Type specific legacy options. Avoid using. - "other": { - "ignore": true, - "type": "any" - }, - value_class: { - name: "Value class", - type: "string", - default: "", - description: "Allow to set a custom css class combined with selected value. (e.g. cat_23)" - } - }, - - legacyOptions: ["rows","other"], // Other is sub-type specific - - /** - * Construtor - * - * @memberOf et2_selectbox - */ - init: function() { - this._super.apply(this, arguments); - - this.input = null; - // Start at '' to avoid infinite loops while setting value/select options - this.value = ''; - - // Allow no other widgets inside this one - this.supportedWidgetClasses = []; - - // Legacy options could have row count or empty label in first slot - if(typeof this.options.rows == "string") - { - if(isNaN(this.options.rows)) - { - this.options.empty_label = this.egw().lang(this.options.rows); - this.options.rows = 1; - } - else - { - this.options.rows = parseInt(this.options.rows); - } - } - - if(this.options.rows > 1) - { - this.options.multiple = true; - if(this.options.tags) - { - this.createInputWidget(); - } - else - { - this.createMultiSelect(); - } - } - else - { - this.createInputWidget(); - } - if(!this.options.empty_label && !this.options.readonly && this.options.multiple) - { - this.options.empty_label = this.egw().lang('Select some options'); - } - }, - - destroy: function() { - if(this.input != null) - { - this.input.unchosen(); - } - if(this.expand_button) - { - this.expand_button.off(); - this.expand_button.remove(); - this.expand_button = null; - } - this._super.apply(this, arguments); - - this.input = null; - }, - - transformAttributes: function(_attrs) { - this._super.apply(this, arguments); - - // If select_options are already known, skip the rest - if(this.options && this.options.select_options && !jQuery.isEmptyObject(this.options.select_options) || - _attrs.select_options && !jQuery.isEmptyObject(_attrs.select_options) || - // Allow children to skip select_options - check to make sure default got set to something (should be {}) - typeof _attrs.select_options == 'undefined' || _attrs.select_options === null - ) - { - // do not return inside nextmatch, as get_rows data might have changed select_options - // for performance reasons we only do it for first row, which should have id "0[...]" - if (this.getParent()._type != 'rowWidget' || !_attrs.id || _attrs.id[0] != '0') return; - } - - var sel_options = et2_selectbox.find_select_options(this, _attrs['select_options'], _attrs); - if(!jQuery.isEmptyObject(sel_options)) - { - _attrs['select_options'] = sel_options; - } - }, - - /** - * Switch instanciated widget to multi-selection and back, optionally enabeling tags too - * - * If you want to switch tags on too, you need to do so after switching to multiple! - * - * @param {boolean} _multiple - * @param {integer} _size default=3 - */ - set_multiple: function(_multiple, _size) - { - this.options.multiple = _multiple; - - if (this.input) - { - if (_multiple) - { - this.input.attr('size', _size || 3); - this.input.attr('multiple', true); - this.input.attr('name', this.id + '[]'); - - if (this.input[0].options.length && this.input[0].options[0].value === '') - { - this.input[0].options[0] = null; - } - } - else - { - this.input.attr('multiple', false); - this.input.removeAttr('size'); - this.input.attr('name', this.id); - - if (this.options.empty_label && this.input[0].options[0].value !== '') - { - this._appendOptionElement('', this.options.empty_label); - } - } - if(this.expand_button) - { - if(_multiple) - { - this.expand_button.addClass('ui-icon-minus').removeClass('ui-icon-plus'); - } - else - { - this.expand_button.removeClass('ui-icon-minus').addClass('ui-icon-plus'); - } - } - } - }, - - change: function(_node, _widget, _value) { - var valid = this._super.apply(this, arguments); - if (!this.input) return valid; - var selected = this.input.siblings().find('a.chzn-single'); - var val = _value && _value.selected ? _value.selected : this.input.val(); - switch (this._type) - { - case 'select-country': - if (selected && selected.length == 1 && val) - { - selected.removeClass (function (index, className) { - return (className.match (/(^|\s)flag-\S+/g) || []).join(' '); - }); - selected.find('span.img').remove(); - selected.prepend(''); - selected.addClass('et2_country-select flag-'+ val.toLowerCase()); - } - else if(selected) - { - selected.removeClass('et2_country-select'); - } - break; - } - return valid; - }, - - /** - * Add an option to regular drop-down select - * - * @param {string} _value value attribute of option - * @param {string} _label label of option - * @param {string} _title title attribute of option - * @param {node} dom_element parent of new option - * @param {string} _class specify classes of option - */ - _appendOptionElement: function(_value, _label, _title, dom_element, _class) { - if(_value == "" && (_label == null || _label == "")) { - return; // empty_label is added in set_select_options anyway, ignoring it here to not add it twice - } - - if(this.input == null) - { - return this._appendMultiOption(_value, _label, _title, dom_element); - } - - var option = jQuery(document.createElement("option")) - .attr("value", _value) - .text(_label+""); - option.addClass(_class); - if (this.options.tags) - { - switch (this._type) - { - case 'select-cat': - option.addClass('cat_'+_value); - break; - case 'select-country': - // jQuery(document.createElement("span")).addClass('et2_country-select').appenTo(option); - option.addClass('et2_country-select flag-'+_value.toLowerCase()); - break; - } - if (this.options.value_class != '') option.addClass(this.options.value_class+_value); - } - if (typeof _title != "undefined" && _title) - { - option.attr("title", _title); - } - if(_label == this.options.empty_label || this.options.empty_label == "" && _value === "") - { - // Make sure empty / all option is first - option.prependTo(this.input); - } - else - { - option.appendTo(dom_element || this.input); - } - }, - - /** - * Append a value to multi-select - * - * @param {string} _value value attribute of option - * @param {string} _label label of option - * @param {string} _title title attribute of option - * @param {node} dom_element parent of new option - */ - _appendMultiOption: function(_value, _label, _title, dom_element) { - var option_data = null; - if(typeof _label == "object") - { - option_data = _label; - _label = option_data.label; - } - - // Already in header - if(_label == this.options.empty_label) return; - - var opt_id = this.dom_id + "_opt_" + _value; - var label = jQuery(document.createElement("label")) - .attr("for", opt_id) - .hover( - function() {jQuery(this).addClass("ui-state-hover");}, - function() {jQuery(this).removeClass("ui-state-hover");} - ); - var option = jQuery(document.createElement("input")) - .attr("type", "checkbox") - .attr("id",opt_id) - .attr("value", _value) - .appendTo(label); - if(typeof _title !== "undefined") - { - option.attr("title",_title); - } - - // Some special stuff for categories - if(option_data ) - { - if(option_data.icon) - { - var img = this.egw().image(option_data.icon); - jQuery(document.createElement(img ? "img" : "div")) - .attr("src", img) - .addClass('cat_icon cat_' + _value) - .appendTo(label); - } - if(option_data.color) - { - label.css("background-color",option_data.color) - .addClass('cat_' + _value); - } - } - label.append(jQuery(""+_label+"")); - var li = jQuery(document.createElement("li")).append(label); - if (this.options.value_class !='') li.addClass(this.options.value_class+_value); - li.appendTo(dom_element || this.multiOptions); - }, - - /** - * Create a regular drop-down select box - */ - createInputWidget: function() { - // Create the base input widget - this.input = jQuery(document.createElement("select")) - .addClass("et2_selectbox") - .attr("size", this.options.rows); - - this.setDOMNode(this.input[0]); - - // Add the empty label - if(this.options.empty_label) - { - this._appendOptionElement("", this.options.empty_label); - } - - // Set multiple - if(this.options.multiple) - { - this.input.attr("multiple", "multiple"); - } - }, - - /** - * Create a list of checkboxes - */ - createMultiSelect: function() { - var node = jQuery(document.createElement("div")) - .addClass("et2_selectbox"); - - var header = jQuery(document.createElement("div")) - .addClass("ui-widget-header ui-helper-clearfix") - .appendTo(node); - var controls = jQuery(document.createElement("ul")) - .addClass('ui-helper-reset') - .appendTo(header); - - jQuery(document.createElement("span")) - .text(this.options.empty_label) - .addClass("ui-multiselect-header") - .appendTo(header); - - - // Set up for options to be added later - var options = this.multiOptions = jQuery(document.createElement("ul")); - this.multiOptions.addClass("ui-multiselect-checkboxes ui-helper-reset") - .css("height", 1.9*this.options.rows + "em") - .appendTo(node); - - if(this.options.rows >= 5) - { - // Check / uncheck all - var header_controls = { - check: { - icon_class: 'ui-icon-check', - label: this.egw().lang('Check all'), - click: function(e) { - var all_off = false; - jQuery("input[type='checkbox']",e.data).each(function() { - if(!jQuery(this).prop("checked")) all_off = true; - }); - jQuery("input[type='checkbox']",e.data).prop("checked", all_off); - } - } - }; - for(var key in header_controls) - { - jQuery(document.createElement("li")) - .addClass("et2_clickable") - .click(options, header_controls[key].click) - .attr("title", header_controls[key].label) - .append('') - .appendTo(controls); - } - - } - - this.setDOMNode(node[0]); - }, - - doLoadingFinished: function() { - this._super.apply(this, arguments); - - this.set_tags(this.options.tags, this.options.width); - - return true; - }, - - loadFromXML: function(_node) { - // Handle special case where legacy option for empty label is used (conflicts with rows), and rows is set as an attribute - var legacy = _node.getAttribute("options"); - if(legacy) - { - var legacy = legacy.split(","); - if(legacy.length && isNaN(legacy[0])) - { - this.options.empty_label = legacy[0]; - } - } - - // Read the option-tags - var options = et2_directChildrenByTagName(_node, "option"); - if(options.length) - { - // Break reference to content manager, we don't want to add to it - this.options.select_options = jQuery.extend([], this.options.select_options); - } - var egw = this.egw(); - for (var i = 0; i < options.length; i++) - { - this.options.select_options.push({ - value: et2_readAttrWithDefault(options[i], "value", options[i].textContent), - // allow options to contain multiple translated sub-strings eg: {Firstname}.{Lastname} - "label": options[i].textContent.replace(/{([^}]+)}/g, function(str,p1) - { - return egw.lang(p1); - }), - "title": et2_readAttrWithDefault(options[i], "title", "") - }); - } - - this.set_select_options(this.options.select_options); - }, - - /** - * Regular expression, to check string-value contains multiple comma-separated values - */ - _is_multiple_regexp: /^[,0-9A-Za-z/_ -]+$/, - - /** - * Regular expression and replace value for escaping values in jQuery selectors used to find options - */ - _escape_value_replace: /\\/g, - _escape_value_with: '\\\\', - - /** - * Find an option by it's value - * - * Taking care of escaping values correctly eg. EGroupware\Api\Mail\Smtp using above regular expression - * - * @param {string} _value - * @return {array} - */ - find_option: function(_value) - { - return jQuery("option[value='"+(typeof _value === 'string' ? _value.replace(this._escape_value_replace, this._escape_value_with) : _value)+"']", this.input); - }, - - /** - * Set value - * - * @param {string|number} _value - * @param {boolean} _dont_try_set_options true: if _value is not in options, use "" instead of calling set_select_options - * (which would go into an infinit loop) - */ - set_value: function(_value, _dont_try_set_options) - { - if (typeof _value == "number") _value = ""+_value; // convert to string for consitent matching - if(typeof _value == "string" && (this.options.multiple || this.options.expand_multiple_rows) && _value.match(this._is_multiple_regexp) !== null) - { - _value = _value.split(','); - } - if(this.input !== null && this.options.select_options && ( - !jQuery.isEmptyObject(this.options.select_options) || this.options.select_options.length > 0 - ) && this.input.children().length == 0) - { - // No options set yet - this.set_select_options(this.options.select_options); - } - // select-cat set/unset right cat_ color for selected value - if ((this._type == 'select-cat' || this.options.value_class) && this.options.tags) { - var chosen = this.input.next(); - var prefix_c = this.options.value_class ? this.options.value_class : 'cat_'; - this.input.removeClass(prefix_c+this._oldValue); - this.input.addClass(prefix_c+this.value); - if (chosen.length > 0) { - chosen.removeClass(prefix_c+this._oldValue); - chosen.addClass(prefix_c+this.value); - } - } - if (this._type == 'select-country' && this.options.tags) - { - var selected = this.input.siblings().find('a.chzn-single'); - if (selected && selected.length == 1 && _value) - { - selected.removeClass (function (index, className) { - return (className.match (/(^|\s)flag-\S+/g) || []).join(' '); - }); - selected.find('span.img').remove(); - selected.prepend(''); - selected.addClass('et2_country-select flag-'+ _value.toLowerCase()); - } - } - if(this._type == "select-bitwise" && _value && !isNaN(_value) && this.options.select_options) - { - var new_value = []; - for(var index in this.options.select_options) - { - var right = this.options.select_options[index].value; - if(!!(_value & right)) - { - new_value.push(right); - } - } - _value = new_value; - } - this._oldValue = this.value; - if(this.input !== null && (this.options.tags || this.options.search)) - { - // Value must be a real Array, not an object - this.input.val(typeof _value == 'object' && _value != null ? jQuery.map(_value,function(value,index){return [value];}) : _value); - this.input.trigger("liszt:updated"); - var self = this; - if (this._type == 'listbox' && this.options.value_class != '') - { - var chosen = this.input.next(); - chosen.find('.search-choice-close').each(function(i,v){ - jQuery(v).parent().addClass(self.options.value_class + self.options.select_options[v.rel]['value']); - }); - } - this.value = _value; - return; - } - if(this.input == null) - { - return this.set_multi_value(_value); - } - // Auto-expand multiple if not yet turned on, and value has multiple - if(this.options.expand_multiple_rows && !this.options.multiple && jQuery.isArray(_value) && _value.length > 1) - { - this.set_multiple(true, this.options.expand_multiple_rows); - } - - jQuery("option",this.input).prop("selected", false); - if (typeof _value == "object") - { - for(var i in _value) - { - this.find_option(_value[i]).prop("selected", true); - } - } - else - { - if(_value && this.find_option(_value).prop("selected", true).length == 0) - { - if(this.options.select_options[_value] || - this.options.select_options.filter && - this.options.select_options.filter(function(value) {return value == _value;}) && - !_dont_try_set_options) - { - // Options not set yet? Do that now, which will try again. - return this.set_select_options(this.options.select_options); - } - else if (_dont_try_set_options) - { - this.value = ""; - } - else if (jQuery.isEmptyObject(this.options.select_options)) - { - this.egw().debug("warn", "Can't set value to '%s', widget has no options set",_value, this); - } - else - { - var debug_value = _value; - if(debug_value === null) debug_value == 'NULL'; - this.egw().debug("warn", "Tried to set value '%s' that isn't an option", debug_value, this); - } - return; - } - } - this.value = _value; - if(this.isAttached() && this._oldValue !== et2_no_init && this._oldValue !== _value) - { - this.input.change(); - } - }, - - /** - * Find an option by it's value - * - * Taking care of escaping values correctly eg. EGroupware\Api\Mail\Smtp - * - * @param {string} _value - * @return {array} - */ - find_multi_option: function(_value) - { - return jQuery("input[value='"+ - (typeof _value === 'string' ? _value.replace(this._escape_value_replace, this._escape_value_with) : _value)+ - "']", this.multiOptions - ); - }, - - set_multi_value: function(_value) - { - jQuery("input",this.multiOptions).prop("checked", false); - if (typeof _value == "object") - { - for(var i in _value) - { - this.find_multi_option(_value[i]).prop("checked", true); - } - } - else - { - if(this.find_multi_option(_value).prop("checked", true).length == 0) - { - var debug_value = _value; - if(debug_value === null) debug_value == 'NULL'; - this.egw().debug("warn", "Tried to set value '%s' that isn't an option", debug_value, this); - } - } - - // Sort selected to the top - if(this.selected_first) - { - this.multiOptions.find("li:has(input:checked)").prependTo(this.multiOptions); - } - this.value = _value; - }, - - /** - * Method to check all options of a multi-select, if not all are selected, or none if all where selected - * - * @todo: add an attribute to automatic add a button calling this method - */ - select_all_toggle: function() - { - var all = jQuery("input",this.multiOptions); - all.prop("checked", jQuery("input:checked",this.multiOptions).length == all.length ? false : true); - }, - - /** - * Add a button to toggle between single select and multi select. - * - * @param {number} _rows How many rows for multi-select - */ - set_expand_multiple_rows: function(_rows) - { - this.options.expand_multiple_rows = _rows; - - var surroundings = this.getSurroundings(); - if(_rows <= 1 && this.expand_button ) - { - // Remove - surroundings.removeDOMNode(this.expand_button.get(0)); - } - else - { - if (!this.expand_button) - { - var button_id = this.getInstanceManager().uniqueId+'_'+this.id.replace(/\./g, '-') + "_expand"; - this.expand_button = jQuery("