mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-12-29 10:09:10 +01:00
5e3c67a5cf
classes are now uppercase and in their own files. lowercase classes are deprecated. Interfaces are now actual interfaces that should be implemented instead of creating and returning an ai Object every time
1002 lines
28 KiB
TypeScript
1002 lines
28 KiB
TypeScript
/**
|
|
* EGroupware eTemplate2 - JS Tree object
|
|
*
|
|
* @link http://community.egroupware.org/egroupware/api/js/dhtmlxtree/docsExplorer/dhtmlxtree/
|
|
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
|
|
* @package etemplate
|
|
* @subpackage api
|
|
* @link https://www.egroupware.org
|
|
* @author Nathan Gray
|
|
* @author Ralf Becker
|
|
* @copyright Nathan Gray 2011
|
|
*/
|
|
|
|
/*egw:uses
|
|
et2_core_inputWidget;
|
|
/api/js/egw_action/egw_dragdrop_dhtmlx_tree.ts;
|
|
/api/js/dhtmlxtree/codebase/dhtmlxcommon.js;
|
|
// using debugable and fixed source of dhtmltree instead: /api/js/dhtmlxtree/js/dhtmlXTree.js;
|
|
/api/js/dhtmlxtree/sources/dhtmlxtree.js;
|
|
/api/js/dhtmlxtree/sources/ext/dhtmlxtree_json.js;
|
|
// /api/js/dhtmlxtree/sources/ext/dhtmlxtree_start.js;
|
|
*/
|
|
|
|
import {et2_register_widget, WidgetConfig} from "./et2_core_widget";
|
|
import {et2_inputWidget} from "./et2_core_inputWidget";
|
|
import {ClassWithAttributes} from "./et2_core_inheritance";
|
|
import {et2_no_init} from "./et2_core_common";
|
|
import {egw} from "../jsapi/egw_global";
|
|
import {egw_getAppObjectManager, egw_getObjectManager, egwActionObject} from "../egw_action/egw_action";
|
|
import {EGW_AO_FLAG_IS_CONTAINER} from "../egw_action/egw_action_constants";
|
|
import {dhtmlxtreeItemAOI} from "../egw_action/./egw_dragdrop_dhtmlx_tree";
|
|
import {egwIsMobile} from "../egw_action/egw_action_common";
|
|
|
|
/* no module, but egw:uses is ignored, so adding it here commented out
|
|
import '../../../api/js/dhtmlxtree/sources/dhtmlxtree.js';
|
|
import '../../../api/js/dhtmlxtree/sources/ext/dhtmlxtree_json.js';
|
|
import '../../../api/js/dhtmlxtree/sources/ext/dhtmlxtree_start.js';
|
|
*/
|
|
|
|
/**
|
|
* Tree widget
|
|
*
|
|
* For syntax of nodes supplied via sel_options or autoloading refer to Etemplate\Widget\Tree class.
|
|
*
|
|
* @augments et2_inputWidget
|
|
*/
|
|
export class et2_tree extends et2_inputWidget
|
|
{
|
|
static readonly _attributes : any = {
|
|
"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
|
|
onopenstart: {
|
|
"name": "onOpenStart",
|
|
"type": "js",
|
|
"default": et2_no_init,
|
|
"description": "Javascript function executed when user opens a node: function(_id, _widget, _hasChildren) returning true to allow opening!"
|
|
},
|
|
onopenend: {
|
|
"name": "onOpenEnd",
|
|
"type": "js",
|
|
"default": et2_no_init,
|
|
"description": "Javascript function executed when opening a node is finished: function(_id, _widget, _hasChildren)"
|
|
},
|
|
"image_path": {
|
|
"name": "Image directory",
|
|
"type": "string",
|
|
"default": egw().webserverUrl + "/api/templates/default/images/dhtmlxtree/",
|
|
"description": "Directory for tree structure images, set on server-side to 'dhtmlx' subdir of templates image-directory"
|
|
},
|
|
"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.png,folderClosed.png,folderOpen.png), images with extension get loaded from image_path, just 'image' or 'appname/image' are allowed too"
|
|
},
|
|
"multimarking": {
|
|
"name": "multimarking",
|
|
"type": "any",
|
|
"default": false,
|
|
"description": "Allow marking multiple nodes, default is false which means disabled multiselection, true or 'strict' activates it and 'strict' makes it strick to only same level marking"
|
|
},
|
|
highlighting:{
|
|
"name": "highlighting",
|
|
"type": "boolean",
|
|
"default": false,
|
|
"description": "Add highlighting class on hovered over item, highlighting is disabled by default"
|
|
}
|
|
};
|
|
|
|
private input : any = null;
|
|
private div : JQuery;
|
|
private autoloading_url: any;
|
|
/**
|
|
* Regexp used by _htmlencode
|
|
*/
|
|
_lt_regexp : RegExp = /</g;
|
|
|
|
/**
|
|
* Constructor
|
|
*
|
|
* @memberOf et2_tree
|
|
*/
|
|
constructor(_parent, _attrs? : WidgetConfig, _child? : object)
|
|
{
|
|
// Call the inherited constructor
|
|
super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_tree._attributes, _child || {}));
|
|
|
|
this.input = null;
|
|
|
|
this.div = jQuery(document.createElement("div")).addClass("dhtmlxTree");
|
|
this.setDOMNode(this.div[0]);
|
|
}
|
|
|
|
destroy() {
|
|
if(this.input)
|
|
{
|
|
this.input.destructor();
|
|
}
|
|
this.input = null;
|
|
super.destroy();
|
|
}
|
|
|
|
/**
|
|
* Get tree items from the sel_options data array
|
|
*
|
|
* @param {object} _attrs
|
|
*/
|
|
transformAttributes(_attrs) {
|
|
super.transformAttributes(_attrs);
|
|
|
|
// If select_options are already known, skip the rest
|
|
if(this.options && this.options.select_options && !jQuery.isEmptyObject(this.options.select_options))
|
|
{
|
|
return;
|
|
}
|
|
|
|
let 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
|
|
let 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"] && !jQuery.isEmptyObject(_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
|
|
let 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(_node) {}
|
|
|
|
createTree(widget)
|
|
{
|
|
widget.input = new dhtmlXTreeObject({
|
|
parent: widget.div[0],
|
|
width: '100%',
|
|
height: '100%',
|
|
image_path: widget.options.image_path,
|
|
checkbox: widget.options.multiple
|
|
});
|
|
// Allow controlling icon size by CSS
|
|
widget.input.def_img_x = "";
|
|
widget.input.def_img_y = "";
|
|
|
|
// to allow "," in value, eg. folder-names, IF value is specified as array
|
|
widget.input.dlmtr = ':}-*(';
|
|
|
|
if(widget.options.std_images)
|
|
{
|
|
widget.setImages.apply(widget, widget.options.std_images.split(','));
|
|
}
|
|
else
|
|
{
|
|
// calling setImages to get our png or svg default images
|
|
widget.setImages();
|
|
}
|
|
// Add in the callback so we can keep the two in sync
|
|
widget.input.AJAX_callback = function(dxmlObject) {
|
|
widget._dhtmlxtree_json_callback(JSON.parse(dxmlObject.xmlDoc.responseText), widget.input.lastLoadedXMLId);
|
|
// Call this in case we added some options that were already selected, but missing
|
|
if(widget.options.multiple)
|
|
{
|
|
widget.set_value(widget.value);
|
|
}
|
|
};
|
|
|
|
if (widget.options.autoloading)
|
|
{
|
|
let url = widget.options.autoloading;
|
|
|
|
//Set escaping mode to utf8, as url in
|
|
//autoloading needs to be utf8 encoded.
|
|
//For instance item id with umlaut.
|
|
widget.input.setEscapingMode('utf8');
|
|
|
|
if (url.charAt(0) != '/' && url.substr(0,4) != 'http')
|
|
{
|
|
url = '/json.php?menuaction='+url;
|
|
}
|
|
this.autoloading_url = url;
|
|
|
|
widget.input.setXMLAutoLoading(egw.link(url));
|
|
widget.input.setDataMode('JSON');
|
|
}
|
|
|
|
if (widget.options.multimarking)
|
|
{
|
|
widget.input.enableMultiselection(!!widget.options.multimarking, widget.options.multimarking === 'strict');
|
|
}
|
|
// Enable/Disable highlighting
|
|
widget.input.enableHighlighting(!!widget.options.highlighting);
|
|
|
|
// if templates supplies open/close right/down arrows, show no more lines and use them instead of plus/minus
|
|
let open = egw.image('dhtmlxtree/open');
|
|
let close = egw.image('dhtmlxtree/close');
|
|
if (open && close)
|
|
{
|
|
widget.input.enableTreeLines(false);
|
|
open = this._rel_url(open);
|
|
widget.input.setImageArrays('plus', open, open, open, open, open);
|
|
close = this._rel_url(close);
|
|
widget.input.setImageArrays('minus', close, close, close, close, close);
|
|
}
|
|
|
|
this._install_handler('onBeforeCheck', function() {
|
|
return !this.options.readonly;
|
|
}.bind(this));
|
|
}
|
|
|
|
/**
|
|
* Install event handlers on tree
|
|
*
|
|
* @param _name
|
|
* @param _handler
|
|
*/
|
|
private _install_handler(_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';
|
|
let handler = _handler;
|
|
let widget = this;
|
|
this.input.attachEvent(_name, function(_id){
|
|
let 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);
|
|
// try to close mobile sidemenu after clicking on node
|
|
if (egwIsMobile() && typeof args[2] == 'string') framework.toggleMenu('on');
|
|
return handler.apply(this, args);
|
|
});
|
|
}
|
|
}
|
|
|
|
set_onchange(_handler) { this._install_handler('onchange', _handler); }
|
|
set_onclick(_handler) { this._install_handler('onclick', _handler); }
|
|
set_onselect(_handler) { this._install_handler('onselect', _handler); }
|
|
set_onopenstart(_handler) { this._install_handler('onOpenStart', _handler); }
|
|
set_onopenend(_handler) { this._install_handler('onOpenEnd', _handler); }
|
|
|
|
set_select_options(options)
|
|
{
|
|
let custom_images = false;
|
|
this.options.select_options = options;
|
|
|
|
if(this.input == null)
|
|
{
|
|
this.createTree(this);
|
|
}
|
|
|
|
// Structure data for category tree
|
|
if(this.getType() == 'tree-cat')
|
|
{
|
|
let data = {id:0,item:[]};
|
|
let stack = {};
|
|
for(let key=0; key < options.length; key++)
|
|
{
|
|
// See if item has an icon
|
|
if(options[key].data && typeof options[key].data.icon !== 'undefined' && options[key].data.icon)
|
|
{
|
|
let 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;
|
|
}
|
|
let 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)
|
|
{
|
|
let path = this.input.iconURL;
|
|
this.input.setIconPath("");
|
|
for(let k = 0; k < this.input.imageArray.length; k++)
|
|
this.input.imageArray[k] = path + this.input.imageArray[k];
|
|
}
|
|
let f = function(data, _f)
|
|
{
|
|
if (stack[data.id])
|
|
{
|
|
data.item=stack[data.id];
|
|
for (let j=0; j<data.item.length; j++)
|
|
{
|
|
f(data.item[j], _f);
|
|
}
|
|
}
|
|
};
|
|
|
|
f(data, f);
|
|
options = data;
|
|
}
|
|
this.input.deleteChildItems("0");
|
|
// if no options given, but autoloading url, use that to load initial nodes
|
|
if (typeof options.id == 'undefined' && this.input.XMLsource)
|
|
this.input.loadJSON(this.input.XMLsource);
|
|
else
|
|
this.input.loadJSONObject(this._htmlencode_node(options));
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* html encoding of text of node
|
|
*
|
|
* We only do a minimal html encoding by replacing opening bracket < with <
|
|
* as tree seems not to need more and we dont want to waste time.
|
|
*
|
|
* @param {string} _text text to encode
|
|
* @return {string}
|
|
*/
|
|
private _htmlencode(_text : string) : string
|
|
{
|
|
if (_text && _text.indexOf('<') >= 0)
|
|
{
|
|
_text = _text.replace(this._lt_regexp, '<');
|
|
}
|
|
return _text;
|
|
}
|
|
|
|
/**
|
|
* html encoding of text of node incl. all children
|
|
*
|
|
* @param {object} _item with required attributes text, id and optional tooltip and item
|
|
* @return {object} encoded node
|
|
*/
|
|
private _htmlencode_node(_item : {text : string, item : any}) : object
|
|
{
|
|
_item.text = this._htmlencode(_item.text);
|
|
|
|
if (_item.item && jQuery.isArray(_item.item))
|
|
{
|
|
for(let i=0; i < _item.item.length; ++i)
|
|
{
|
|
this._htmlencode_node(_item.item[i]);
|
|
}
|
|
}
|
|
return _item;
|
|
}
|
|
|
|
set_value(new_value)
|
|
{
|
|
this.value = this._oldValue = (typeof new_value === 'string' && this.options.multiple ? new_value.split(',') : new_value);
|
|
if(this.input == null) return;
|
|
|
|
if (this.options.multiple)
|
|
{
|
|
// Clear all checked
|
|
let checked = this.input.getAllChecked().split(this.input.dlmtr);
|
|
for(let i = 0; i < checked.length; i++)
|
|
{
|
|
this.input.setCheck(checked[i], false);
|
|
}
|
|
|
|
// Check selected
|
|
for(let i = 0; i < this.value.length; i++)
|
|
{
|
|
this.input.setCheck(this.value[i], true);
|
|
// autoloading openning needs to be absolutely based on user interaction
|
|
// or open flag in folder structure, therefore, We should
|
|
// not force it to open the node
|
|
if (!this.options.autoloading) this.input.openItem(this.value[i]);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
this.input.selectItem(this.value, false); // false = do not trigger onSelect
|
|
this.input.focusItem(this.value);
|
|
this.input.openItem(this.value);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Links actions to tree nodes
|
|
*
|
|
* @param {object} actions [ {ID: attributes..}+] as for set_actions
|
|
*/
|
|
_link_actions(actions)
|
|
{
|
|
// Get the top level element for the tree
|
|
// Only look 1 level deep for application object manager
|
|
let objectManager = egw_getObjectManager(this.egw().app_name(),true,1);
|
|
let treeObj = objectManager.getObjectById(this.id);
|
|
if (treeObj == null) {
|
|
// Add a new container to the object manager which will hold the tree
|
|
// objects
|
|
treeObj = objectManager.addObject(
|
|
new egwActionObject(this.id, objectManager, null, this._actionManager, EGW_AO_FLAG_IS_CONTAINER),
|
|
null, EGW_AO_FLAG_IS_CONTAINER
|
|
);
|
|
}
|
|
|
|
// Delete all old objects
|
|
treeObj.clear();
|
|
|
|
// Go over the tree parts & add links
|
|
let action_links = this._get_action_links(actions);
|
|
|
|
if (typeof this.options.select_options != 'undefined')
|
|
{
|
|
|
|
// Iterate over the options (leaves) and add action to each one
|
|
let apply_actions = function(treeObj, option)
|
|
{
|
|
// Add a new action object to the object manager
|
|
// @ts-ignore
|
|
let obj = treeObj.addObject((typeof option.id == 'number' ? String(option.id) : option.id), new dhtmlxtreeItemAOI(this.input, option.id));
|
|
obj.updateActionLinks(action_links);
|
|
|
|
if(option.item && option.item.length > 0)
|
|
{
|
|
for(let 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()
|
|
{
|
|
if(this.input == null) return null;
|
|
if (this.options.multiple)
|
|
{
|
|
let allChecked = this.input.getAllChecked().split(this.input.dlmtr);
|
|
let allUnchecked = this.input.getAllUnchecked().split(this.input.dlmtr);
|
|
if (this.options.autoloading)
|
|
{
|
|
|
|
let res = {};
|
|
for (let i=0;i<allChecked.length;i++)
|
|
{
|
|
res[allChecked[i]]= {value:true};
|
|
}
|
|
for (let i=0;i<allUnchecked.length;i++)
|
|
{
|
|
res[allUnchecked[i]]= {value:false};
|
|
}
|
|
return res;
|
|
}
|
|
else
|
|
{
|
|
return allChecked;
|
|
}
|
|
}
|
|
return this.input.getSelectedItemId();
|
|
}
|
|
|
|
/**
|
|
* getSelectedLabel, retrieves the Label of the selected Item
|
|
* @return string or null
|
|
*/
|
|
getSelectedLabel()
|
|
{
|
|
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 {string} _id ID of the node
|
|
* @param {string} _newItemId ID of the node
|
|
* @param {string} _label label to set
|
|
*/
|
|
renameItem(_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 = (<egwActionObject><unknown>egw_getAppObjectManager()).getObjectById(this.id);
|
|
for(var i=0; i < treeObj.children.length; i++)
|
|
{
|
|
if(treeObj.children[i].id == _id)
|
|
{
|
|
treeObj.children[i].id = _newItemId;
|
|
if (treeObj.children[i].iface) treeObj.children[i].iface.id = _newItemId;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (typeof _label != 'undefined') this.setLabel(_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(_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
|
|
let treeObj = (<egwActionObject><unknown>egw_getAppObjectManager()).getObjectById(this.id);
|
|
for(let i=0; i < treeObj.children.length; i++)
|
|
{
|
|
if(treeObj.children[i].id == _id)
|
|
{
|
|
treeObj.children.splice(i,1);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Updates a leaf of the tree by requesting new information from the server using the
|
|
* autoloading attribute.
|
|
*
|
|
* @param {string} _id ID of the node
|
|
* @param {Object} [data] If provided, the item is refreshed directly with
|
|
* the provided data instead of asking the server
|
|
* @return void
|
|
*/
|
|
refreshItem(_id,data)
|
|
{
|
|
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);
|
|
*/
|
|
|
|
let self = this;
|
|
if(typeof data != 'undefined' && data != null)
|
|
{
|
|
this.input.loadJSONObject(data,
|
|
function() { self._dhtmlxtree_json_callback(data, _id);}
|
|
);
|
|
}
|
|
else
|
|
{
|
|
this.input.loadJSON(this.egw().link(this.autoloading_url, {id: _id}),
|
|
function(dxmlObject) {
|
|
self._dhtmlxtree_json_callback(JSON.parse(dxmlObject.xmlDoc.responseText), _id);
|
|
|
|
// refreshing root node causes binding actions fails in dhtmlx tree, we try to refresh the opened node
|
|
// in order to rebind the actions again.
|
|
if (_id == 0)
|
|
{
|
|
let openedId = self._oldValue.split("::")[0];
|
|
let interval = setInterval(()=> {
|
|
if (self.input.getOpenState(openedId))
|
|
{
|
|
clearInterval(interval);
|
|
self.refreshItem(openedId);
|
|
}
|
|
}, 100);
|
|
}
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* focus the item, and scrolls it into view
|
|
*
|
|
* @param _id ID of the node
|
|
* @return void
|
|
*/
|
|
focusItem(_id)
|
|
{
|
|
if(this.input == null) return null;
|
|
this.input.focusItem(_id);
|
|
}
|
|
|
|
/**
|
|
* hasChildren
|
|
*
|
|
* @param _id ID of the node
|
|
* @return the number of childelements
|
|
*/
|
|
hasChildren(_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 {string} 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
|
|
*/
|
|
private _dhtmlxtree_json_callback(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
|
|
let 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
|
|
let 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
|
|
*/
|
|
private _find_in_item(_id, _item)
|
|
{
|
|
if (_item && _item.id == _id)
|
|
{
|
|
return _item;
|
|
}
|
|
if (_item && typeof _item.item != 'undefined')
|
|
{
|
|
for(let i=0; i < _item.item.length; ++i)
|
|
{
|
|
let found = this._find_in_item(_id, _item.item[i]);
|
|
if (found) return found;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Get node data by id
|
|
*
|
|
* @param {string} _id id of node
|
|
* @return {object} object with attributes id, im0-2, text, tooltip, ... as set via select_options or autoload url
|
|
*/
|
|
getNode(_id)
|
|
{
|
|
return this._find_in_item(_id, this.options.select_options);
|
|
}
|
|
|
|
/**
|
|
* Sets label of an item by id
|
|
*
|
|
* @param _id ID of the node
|
|
* @param _label label to set
|
|
* @param _tooltip new tooltip, default is previous set tooltip
|
|
* @return void
|
|
*/
|
|
setLabel(_id, _label, _tooltip?)
|
|
{
|
|
if(this.input == null) return null;
|
|
let tooltip = _tooltip || (this.getNode(_id) && this.getNode(_id).tooltip ?
|
|
this.getNode(_id).tooltip : "");
|
|
this.input.setItemText(_id, this._htmlencode(_label), tooltip);
|
|
}
|
|
|
|
/**
|
|
* Sets a style for an item by id
|
|
*
|
|
* @param {string} _id ID of node
|
|
* @param {string} _style style to set
|
|
* @return void
|
|
*/
|
|
setStyle(_id, _style)
|
|
{
|
|
if(this.input == null) return null;
|
|
this.input.setItemStyle(_id, _style);
|
|
}
|
|
|
|
/**
|
|
* getLabel, gets the Label of of an item by id
|
|
* @param _id ID of the node
|
|
* @return _label
|
|
*/
|
|
getLabel(_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()
|
|
{
|
|
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 {string} _nodeID the nodeID where to start from (initial node)
|
|
* @param {string} mode the mode to run in: "forced" fakes the initial node openState to be open
|
|
* @return {object} structured array of node ids: array(message-ids)
|
|
*/
|
|
getTreeNodeOpenItems(_nodeID : string, mode? : string)
|
|
{
|
|
if(this.input == null) return null;
|
|
let z = this.input.getSubItems(_nodeID).split(this.input.dlmtr);
|
|
let oS;
|
|
let PoS;
|
|
let rv;
|
|
let returnValue = [_nodeID];
|
|
let modetorun = "none";
|
|
if (mode) { modetorun = mode; }
|
|
PoS = this.input.getOpenState(_nodeID);
|
|
if (modetorun == "forced") PoS = 1;
|
|
if (PoS == 1) {
|
|
for(let i=0;i<z.length;i++) {
|
|
oS = this.input.getOpenState(z[i]);
|
|
//alert(z[i]+' OpenState:'+oS);
|
|
if (oS == -1) { returnValue.push(z[i]); }
|
|
if (oS == 0) { returnValue.push(z[i]); }
|
|
if (oS == 1) {
|
|
//alert("got here")
|
|
rv = this.getTreeNodeOpenItems(z[i]);
|
|
//returnValue.concat(rv); // not working as expected; the following does
|
|
for(let j=0;j<rv.length;j++) {returnValue.push(rv[j]);}
|
|
}
|
|
}
|
|
}
|
|
//alert(returnValue.join('#,#'));
|
|
return returnValue;
|
|
}
|
|
|
|
/**
|
|
* Fetch user-data stored in specified node under given name
|
|
*
|
|
* User-data need to be stored in json as follows:
|
|
*
|
|
* {"id": "node-id", "im0": ..., "userdata": [{"name": "user-name", "content": "user-value"},...]}
|
|
*
|
|
* In above example getUserData("node-id", "user-name") will return "user-value"
|
|
*
|
|
* @param _nodeId
|
|
* @param _name
|
|
* @returns
|
|
*/
|
|
getUserData(_nodeId, _name)
|
|
{
|
|
if(this.input == null) return null;
|
|
return this.input.getUserData(_nodeId, _name);
|
|
}
|
|
|
|
/**
|
|
* Stores / updates user-data in specified node and name
|
|
*
|
|
* @param _nodeId
|
|
* @param _name
|
|
* @param _value
|
|
* @returns
|
|
*/
|
|
setUserData(_nodeId, _name, _value)
|
|
{
|
|
if(this.input == null) return null;
|
|
return this.input.setUserData(_nodeId, _name, _value);
|
|
}
|
|
|
|
/**
|
|
* Query nodes open state and optinal change it
|
|
*
|
|
* @param _id node-id
|
|
* @param _open specify to change true: open, false: close, everything else toggle
|
|
* @returns true if open, false if closed
|
|
*/
|
|
openItem(_id, _open?)
|
|
{
|
|
if (this.input == null) return null;
|
|
|
|
let is_open = this.input.getOpenState(_id) == 1;
|
|
|
|
if (typeof _open != 'undefined' && is_open !== _open)
|
|
{
|
|
if(is_open)
|
|
{
|
|
this.input.closeItem(_id);
|
|
}
|
|
else
|
|
{
|
|
this.input.openItem(_id);
|
|
}
|
|
}
|
|
return is_open;
|
|
}
|
|
|
|
/**
|
|
* reSelectItem, reselects an item by id
|
|
* @param _id ID of the node
|
|
*/
|
|
reSelectItem(_id)
|
|
{
|
|
if (this.input == null) return null;
|
|
this.input.selectItem(_id,false,false);
|
|
}
|
|
|
|
/**
|
|
* Set images for a specific node or all new nodes (default)
|
|
*
|
|
* If images contain an extension eg. "leaf" they are asumed to be in image path (/phpgwapi/templates/default/images/dhtmlxtree/).
|
|
* Otherwise they get searched via egw.image() in current app, phpgwapi or can be specified as "app/image".
|
|
*
|
|
* @param {string} _leaf leaf image, default "leaf"
|
|
* @param {string} _closed closed folder image, default "folderClosed"
|
|
* @param {string} _open opened folder image, default "folderOpen"
|
|
* @param {string} _id if not given, standard images for new nodes are set
|
|
*/
|
|
setImages(_leaf? : string, _closed? : string, _open? : string, _id? : string)
|
|
{
|
|
// NOTE: The image order for open/closed as documented in dhtmltree is backwards
|
|
let images = [_leaf || 'dhtmlxtree/leaf', _open || 'dhtmlxtree/folderOpen', _closed || 'dhtmlxtree/folderClosed'];
|
|
let image_extensions = /\.(gif|png|jpe?g|svg)/i;
|
|
for(let i = 0; i < 3; ++i)
|
|
{
|
|
let image = images[i];
|
|
if (!image.match(image_extensions))
|
|
{
|
|
images[i] = this._rel_url(this.egw().image(image) || image);
|
|
}
|
|
}
|
|
if (typeof _id == 'undefined')
|
|
{
|
|
this.input.setStdImages.apply(this.input, images);
|
|
}
|
|
else
|
|
{
|
|
images.unshift(_id);
|
|
this.input.setItemImage2.apply(this.input, images);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set state of node incl. it's children
|
|
*
|
|
* @param {string} _id id of node
|
|
* @param {boolean|string} _state or "toggle" to toggle state
|
|
*/
|
|
setSubChecked(_id, _state)
|
|
{
|
|
if (_state === "toggle") _state = !this.input.isItemChecked(_id);
|
|
this.input.setSubChecked(_id, _state);
|
|
}
|
|
|
|
/**
|
|
* Get URL relative to image_path option
|
|
*
|
|
* Both URL start with EGroupware webserverUrl and image_path gets allways appended to images by tree.
|
|
*
|
|
* @param {string} _url
|
|
* @return {string} relativ url
|
|
*/
|
|
private _rel_url(_url)
|
|
{
|
|
let path_parts = this.options.image_path.split(this.egw().webserverUrl);
|
|
path_parts = path_parts[1].split('/');
|
|
let url_parts = _url.split(this.egw().webserverUrl);
|
|
url_parts = url_parts[1].split('/');
|
|
|
|
for(let i=0; i < path_parts.length; ++i)
|
|
{
|
|
if (path_parts[i] != url_parts[i])
|
|
{
|
|
while(++i < path_parts.length) url_parts.unshift('..');
|
|
break;
|
|
}
|
|
url_parts.shift();
|
|
}
|
|
return url_parts.join('/');
|
|
}
|
|
}
|
|
et2_register_widget(et2_tree, ["tree","tree-cat"]); |