/** * EGroupware eTemplate2 - JS Tree object * * @link http://community.egroupware.org/egroupware/phpgwapi/js/dhtmlxtree/docsExplorer/dhtmlxtree/ * @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 * @author Ralf Becker * @copyright Nathan Gray 2011 * @version $Id$ */ "use strict"; /*egw:uses et2_core_inputWidget; /phpgwapi/js/egw_action/egw_dragdrop_dhtmlx_tree.js; /phpgwapi/js/dhtmlxtree/js/dhtmlXCommon.js; // using debugable and fixed source of dhtmltree instead: /phpgwapi/js/dhtmlxtree/js/dhtmlXTree.js; /phpgwapi/js/dhtmlxtree/dhtmlxTree/sources/dhtmlxtree.js; /phpgwapi/js/dhtmlxtree/dhtmlxTree/sources/ext/dhtmlxtree_json.js; // /phpgwapi/js/dhtmlxtree/dhtmlxTree/sources/ext/dhtmlxtree_start.js; */ /** * Tree widget * * For syntax of nodes supplied via sel_optons or autoloading refer to etemplate_widget_tree class. * * @augments et2_inputWidget */ var et2_tree = et2_inputWidget.extend( { attributes: { "multiple": { "name": "multiple", "type": "boolean", "default": false, "description": "Allow selecting multiple options" }, "select_options": { "type": "any", "name": "Select options", "default": {}, "description": "Used to set the tree options." }, "onclick": { "description": "JS code which gets executed when clicks on text of a node" }, "onselect": { "name": "onSelect", "type": "js", "default": et2_no_init, "description": "Javascript executed when user selects a node" }, "oncheck": { "name": "onCheck", "type": "js", "default": et2_no_init, "description": "Javascript executed when user checks a node" }, // onChange event is mapped depending on multiple to onCheck or onSelect "image_path": { "name": "Image directory", "type": "string", "default": this.egw().webserverUrl + "/phpgwapi/templates/default/images/dhtmlxtree/", "description": "Directory for tree structure images" }, "value": { "type": "any", "default": {} }, "actions": { "name": "Actions array", "type": "any", "default": et2_no_init, "description": "List of egw actions that can be done on the tree. This includes context menu, drag and drop. TODO: Link to action documentation" }, "autoloading": { "name": "Autoloading", "type": "string", "default": "", "description": "JSON URL or menuaction to be called for nodes marked with child=1, but not having children, GET parameter selected contains node-id" }, "std_images": { "name": "Standard images", "type": "string", "default": "", "description": "comma-separated names of icons for a leaf, closed and opend folder (default: leaf.gif,folderClosed.gif,folderOpen.gif), images with extension get loaded from image_path, just 'image' or 'appname/image' are allowed too" } }, /** * Constructor * * @memberOf et2_tree */ init: function() { this._super.apply(this, arguments); this.input = null; this.div = $j(document.createElement("div")).addClass("dhtmlxTree"); this.setDOMNode(this.div[0]); }, destroy: function() { if(this.input) { this.input.destructor(); } this.input = null; this._super.apply(this, arguments); }, /** * Get tree items from the sel_options data array */ 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)) { return; } var name_parts = this.id.replace(/]/g,'').split('['); // Try to find the options inside the "sel-options" array if(this.getArrayMgr("sel_options")) { // Select options tend to be defined once, at the top level, so try that first var content_options = this.getArrayMgr("sel_options").getRoot().getEntry(name_parts[name_parts.length-1]); // Try again according to ID if(!content_options) content_options = this.getArrayMgr("sel_options").getEntry(this.id); if(_attrs["select_options"] && content_options) { _attrs["select_options"] = jQuery.extend({},_attrs["select_options"],content_options); } else if (content_options) { _attrs["select_options"] = content_options; } } // Check whether the options entry was found, if not read it from the // content array. if (_attrs["select_options"] == null) { // Again, try last name part at top level var content_options = this.getArrayMgr('content').getRoot().getEntry(name_parts[name_parts.length-1]); // If that didn't work, check according to ID _attrs["select_options"] = content_options ? content_options : this.getArrayMgr('content') .getEntry("options-" + this.id); } // Default to an empty object if (_attrs["select_options"] == null) { _attrs["select_options"] = {}; } }, // overwrite default onclick to do nothing, as we install onclick via dhtmlxtree click: function(_node) {}, createTree: function(widget) { widget.input = new dhtmlXTreeObject({ parent: widget.div[0], width: '100%', height: '100%', image_path: widget.options.image_path, checkbox: widget.options.multiple, }); if (widget.options.std_images) { widget.setImages.apply(widget, widget.options.std_images.split(',')); } // Add in the callback so we can keep the two in sync widget.input.AJAX_callback = function() { widget._dhtmlxtree_json_callback(JSON.parse(this.response), widget.input.lastLoadedXMLId);}; if (widget.options.autoloading) { var url = widget.options.autoloading; if (url.charAt(0) != '/' && url.substr(0,4) != 'http') { url = '/json.php?menuaction='+url; } if (url.charAt(0) == '/') url = egw.webserverUrl+url; this.autoloading_url = url; widget.input.setXMLAutoLoading(url); widget.input.setDataMode('JSON'); } }, /** * Install event handlers on tree * * @param _name * @param _handler */ _install_handler: function(_name, _handler) { if (typeof _handler == 'function') { if(this.input == null) this.createTree(this); // automatic convert onChange event to oncheck or onSelect depending on multiple is used or not if (_name == 'onchange') _name = this.options.multiple ? 'oncheck' : 'onselect'; var handler = _handler; var widget = this; this.input.attachEvent(_name, function(_id){ var args = jQuery.makeArray(arguments); // splice in widget as 2. parameter, 1. is new node-id, now 3. is old node id args.splice(1, 0, widget); handler.apply(this, args); }); } }, set_onchange: function(_handler) { this._install_handler('onchange', _handler); }, set_onclick: function(_handler) { this._install_handler('onclick', _handler); }, set_onselect: function(_handler) { this._install_handler('onselect', _handler); }, set_select_options: function(options) { var custom_images = false; this.options.select_options = options; if(this.input == null) { this.createTree(this); } // Structure data for category tree if(this._type == 'tree-cat' && !jQuery.isArray(options)) { var data = {id:0,item:[]}; var stack = []; for(var key in options) { // See if item has an icon if(options[key].data && typeof options[key].data.icon !== 'undefined' && options[key].data.icon) { var img = this.egw().image(options[key].data.icon, options[key].appname); if(img) { custom_images = true; options[key].im0 = options[key].im1 = options[key].im2 = img; } } // Item color - not working if(options[key].data && typeof options[key].data.color !== 'undefined' && options[key].data.color) { options[key].style = options[key].style || "" + "background-color ='"+options[key].data.color+"';"; } // Tooltip if(options[key].description && !options[key].tooltip) { options[key].tooltip = options[key].description; } var parent_id = parseInt(options[key]['parent']); if(isNaN(parent_id)) parent_id = 0; if(!stack[parent_id]) stack[parent_id] = []; stack[parent_id].push(options[key]); } if(custom_images) { var path = this.input.iconURL; this.input.setIconPath(""); for(var k = 0; k < this.input.imageArray.length; k++) this.input.imageArray[k] = path + this.input.imageArray[k]; } var f=function(data,f) { if (stack[data.id]) { data.item=stack[data.id]; for (var j=0; j 0) { for(var i = 0; i < option.item.length; i++) { apply_actions.call(this, treeObj, option.item[i]); } } }; apply_actions.call(this, treeObj, this.options.select_options); } }, /** * getValue, retrieves the Id of the selected Item * @return string or object or null */ getValue: function() { if(this.input == null) return null; return this.options.multiple ? this.input.getAllChecked().split(this.input.dlmtr) : this.input.getSelectedItemId(); }, /** * getSelectedLabel, retrieves the Label of the selected Item * @return string or null */ getSelectedLabel: function() { if(this.input == null) return null; if (this.options.multiple) { /* var out = []; var checked = this.input.getAllChecked().split(this.input.dlmtr); for(var i = 0; i < checked.length; i++) { out.push(this.input.getItemText(checked[i])); } return out; */ return null; // not supported yet } else { return this.input.getSelectedItemText(); } }, /** * renameItem, renames an item by id * @param _id ID of the node * @param _newid ID of the node * @param _label label to set * @return void */ renameItem: function(_id, _newItemId, _label) { if(this.input == null) return null; this.input.changeItemId(_id,_newItemId); // Update action // since the action ID has to = this.id, getObjectById() won't work var treeObj = egw_getAppObjectManager().getObjectById(this.id); for(var i=0; i < treeObj.children.length; i++) { if(treeObj.children[i].iface && treeObj.children[i].iface.id == _id) { treeObj.children[i].iface.id = _newItemId; } } if (typeof _label != 'undefined') this.input.setItemText(_newItemId,_label); }, /** * deleteItem, deletes an item by id * @param _id ID of the node * @param _selectParent select the parent node true/false * @return void */ deleteItem: function(_id, _selectParent) { if(this.input == null) return null; this.input.deleteItem(_id, _selectParent); // Update action // since the action ID has to = this.id, getObjectById() won't work var treeObj = egw_getAppObjectManager().getObjectById(this.id); for(var i=0; i < treeObj.children.length; i++) { if(treeObj.children[i].iface && treeObj.children[i].iface.id == _id) { delete treeObj.children[i].iface.id; delete treeObj.children[i].iface.node; } } }, /** * Updates a leaf of the tree by requesting new information from the server using the * autoloading attribute. * * @param _id ID of the node * @return void */ refreshItem: function(_id) { if(this.input == null) return null; this.input.deleteChildItems(_id); this.input.setDataMode('JSON'); /* Can't use this, it doesn't allow a callback this.input.refreshItem(_id); */ var self = this; this.input.loadJSON(this.egw().link(this.autoloading_url, {id: _id}), function() { self._dhtmlxtree_json_callback(JSON.parse(this.response), _id);} ); }, /** * focus the item, and scrolls it into view * * @param _id ID of the node * @return void */ focusItem: function(_id) { if(this.input == null) return null; this.input.focusItem(_id); }, /** * hasChildren * * @param _id ID of the node * @return the number of childelements */ hasChildren: function(_id) { if(this.input == null) return null; return this.input.hasChildren(_id); }, /** * Callback for after using dhtmlxtree's AJAX loading * The tree has visually already been updated at this point, we just need * to update the internal data. * * @param Object new_data Fresh data for the tree * @param Object update_option_id optional If provided, only update that node (and children) with the * provided data instead of the whole thing. Allows for partial updates. * @return void */ _dhtmlxtree_json_callback: function(new_data, update_option_id) { // not sure if it makes sense to try update_option_id, so far I only seen it to be -1 var parent_id = typeof update_option_id != 'undefined' && update_option_id != -1 ? update_option_id : new_data.id; // find root of loaded data to merge it there var option = this._find_in_item(parent_id, this.options.select_options); // if we found it, merge it if (option) { jQuery.extend(option,new_data || {}); } else // else store it in root { this.options.select_options = new_data; } // Update actions by just re-setting them this.set_actions(this.options.actions || {}); }, /** * Recursive search item object for given id * * @param string _id * @param object _item * @returns */ _find_in_item: function(_id, _item) { if (_item && _item.id == _id) { return _item; } if (_item && typeof _item.item != 'undefined') { for(var i=0; i < _item.item.length; ++i) { var found = this._find_in_item(_id, _item.item[i]); if (found) return found; } } return null; }, /** * setLabel, sets the Label of of an item by id * @param _id ID of the node * @param _label label to set * @return void */ setLabel: function(_id, _label) { if(this.input == null) return null; this.input.setItemText(_id,_label); }, /** * getLabel, gets the Label of of an item by id * @param _id ID of the node * @return _label */ getLabel: function(_id) { if(this.input == null) return null; return this.input.getItemText(_id); }, /** * getSelectedNode, retrieves the full node of the selected Item * @return string or null */ getSelectedNode: function() { if(this.input == null) return null; // no support for multiple selections // as there is no get Method to return the full selected node, we use this return this.options.multiple ? null : this.input._selected[0]; }, /** * getTreeNodeOpenItems * * @param _nodeID, the nodeID where to start from (initial node) * @param mode, the mode to run in: forced fakes the initial node openState to be open * @return structured array of node ids: array(message-ids) */ getTreeNodeOpenItems: function (_nodeID, mode) { if(this.input == null) return null; var z = this.input.getSubItems(_nodeID).split(","); var oS; var PoS; var rv; var returnValue = [_nodeID]; var modetorun = "none"; if (mode) { modetorun = mode; } PoS = this.input.getOpenState(_nodeID); if (modetorun == "forced") PoS = 1; if (PoS == 1) { for(var i=0;i