/* * Copyright (C) 2005 - 2010 Jaspersoft Corporation. All rights reserved. * http://www.jaspersoft.com. * * Unless you have purchased a commercial license agreement from Jaspersoft, * the following license terms apply: * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ /** * TreeSupport is extend Tree to use it with JasperServer. * You can extend it to change parameters and/or look and feel * * @param id {String} - unique ID for the tree on the page * @param options {JSON Object} - Set of configuration options for tree : * * * See {@link dynamicTree.Tree} for more options. */ dynamicTree.TreeSupport = function(id, options) { dynamicTree.Tree.call(this, id, options); // instance variables this.providerId = options.providerId; this.hideLoader = options.hideLoader; this.rootUri = Object.isUndefined(options.rootUri) ? '/' : options.rootUri; this.nodeClass = (options.nodeClass && Object.isFunction(options.nodeClass)) ? options.nodeClass : dynamicTree.TreeNode; if (options.rootObjectModifier) { this.modifyRootObject = options.rootObjectModifier; } this.resetStatesOnShow = Object.isUndefined(options.resetStatesOnShow) || options.resetStatesOnShow; this.inInit = true; // default ajax related values this.ajaxBufferId = 'ajaxbuffer'; // id of a DIV elements that receives server response this.nodeTextId = 'treeNodeText'; // id of a text element that contains JSONized tree this.urlGetNode = (options.urlGetNode) ? options.urlGetNode : this._getFlowUrl("getNode"); // server url for 'getNode' method this.urlGetChildren = (options.urlGetChildren) ? options.urlGetChildren : this._getFlowUrl("getChildren"); // server url for 'getChildren' method this.urlGetMultipleChildren = (options.urlGetMultipleChildren) ? options.urlGetMultipleChildren : this._getFlowUrl("getMultipleChildren"); // server url for 'getMultipleChildren' method this.urlGetMessage = (options.urlGetMessage) ? options.urlGetMessage : this._getFlowUrl("getMessage"); // server url for 'getMessage' method ///////////////////////////////////// // dragging related variables // (if you require drag support, please pass treeDragSupport argument in ctor and include drag.js) ///////////////////////////////////// if (this.TREE_NN_ITEMS_SELECTED == null) { var callback = function(text) { this.TREE_NN_ITEMS_SELECTED = text; }.bind(this); this.getMessage('TREE_NN_ITEMS_SELECTED', callback, null); } this._initOpenListener(); } dynamicTree.TreeSupport.prototype = deepClone(dynamicTree.Tree.prototype); ///////////////////////////////////// // methods ///////////////////////////////////// /** * Adding handler of 'node:open' event. If event occurred, handler will loads children of node from server. */ dynamicTree.TreeSupport.addMethod('_initOpenListener', function () { this.observe('node:open', function(event) { var node = event.memo.node; if (node && !node.isloaded) { this.getTreeNodeChildren(node); } }.bindAsEventListener(this)); }); /** * Remove all or specified handler from event and adding default handler on 'node:open' event. * * @param eventName {String} event, from which should be removed handlers. * @param handler {Function} handler function, which should be removed */ dynamicTree.TreeSupport.addMethod('stopObserving', function(eventName, handler) { this._getElement().stopObserving(eventName, handler); if (eventName === 'node:open' && !handler) { this._initOpenListener(); } }); dynamicTree.TreeSupport.addMethod('_getFlowUrl', function (methodName) { return "flow.html?_flowId=treeFlow&method=" + methodName; }); /** * Loads tree from server and renders it into given container. * Generates "tree:loaded" event, if request was successful and userCallbackFn isn't specified. * Generates "server:error" event, if request was unsuccessful and errorCallbackFn isn't specified. * * @type Asynchronous method * @param depth {Number} controls how many levels of children to prefetch at this load * @param userCallbackFn {Function} optional callback function, that will be called if request was successful * @param errorCallbackFn {Function} optional callback function, that will be called if request was unsuccessful * @param forceHtmlEscape {Boolean} asks server to escape response as html before putting it into tree div */ dynamicTree.TreeSupport.addMethod('showTree', function (depth, userCallbackFn, errorCallbackFn, forceHtmlEscape) { var url = this.urlGetNode + '&provider=' + this.providerId + '&uri=' + this.rootUri + '&depth=' + depth; forceHtmlEscape && (url += '&forceHtmlEscape=true'); this._showTree(url, userCallbackFn, errorCallbackFn); }); /** * Loads tree from the server and renders it into given container. * Generates "tree:loaded" event, if request was successful and userCallbackFn isn't specified. * Generates "server:error" event, if request was unsuccessful and errorCallbackFn isn't specified. * * @type Asynchronous method * @param prefetchedListStr {String} comma separated uris to prefetch (example: '/reports/samples,/adhoc/topics'). * Controls which tree branches to prefetch at this load. * @param userCallbackFn {Function} optional user function object to call after tree gets loaded and rendered * @param errorCallbackFn {Function} optional user error handler function to call if error occured */ dynamicTree.TreeSupport.addMethod('showTreePrefetchNodes', function (prefetchedListStr, userCallbackFn, errorCallbackFn) { var url = this.urlGetNode + '&provider=' + this.providerId + '&uri=' + this.rootUri; if (prefetchedListStr) { url += '&prefetch=' + encodeURIComponent(prefetchedListStr); } this._showTree(url, userCallbackFn, errorCallbackFn); }); dynamicTree.TreeSupport.addMethod('_showTree', function (url, userCallbackFn, errorCallbackFn) { this.inInit = true; this.wait(); var callback = function(obj, uc, ec) { return function() { return obj.showTreeCallback(uc, ec); } }(this, userCallbackFn, errorCallbackFn); ajaxTargettedUpdate( url, {fillLocation:this.ajaxBufferId, callback:callback, errorHandler: baseErrorHandler, hideLoader: this.hideLoader} ); }); dynamicTree.TreeSupport.addMethod('showTreeCallback', function (userCallbackFn, errorCallbackFn) { // get JSONized Node var div = document.getElementById(this.nodeTextId); if (div == null) { if (errorCallbackFn) { errorCallbackFn(); } else { this.fireServerErrorEvent(); } return; } var rootObj = (div.innerHTML.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')).evalJSON(); if (this.modifyRootObject) { rootObj = this.modifyRootObject(rootObj, false); } // clean AJAX buffer div = document.getElementById(this.ajaxBufferId); div.innerHTML = ''; // build the tree this.setRootNode(this.processNode(rootObj)); this.resortTree(); if (this.resetStatesOnShow) { this.resetStates(); } this.renderTree(); this.inInit = false; if (userCallbackFn) { userCallbackFn(); } else { this.fireTreeLoadedEvent( { tree: this } ); } }); /** * internally used method to turn server model into javascript tree model * Be advised of the power of 'extra' property of server node object. * You can set there pretty much anything and therefore customize you tree behaviour. * TreeNode is available in node handlers which you may assign as callback functions to your code * * @param metaNode {JSON Object} server node */ dynamicTree.TreeSupport.addMethod('processNode', function (metaNode) { var param = {}; param.id = metaNode.id; param.type = metaNode.type; param.uri = metaNode.uri; param.extra = deepClone(metaNode.extra); if (metaNode.cssClass) { param.cssClass = metaNode.cssClass; } var NodeClassFn = this.nodeClass; var localRoot = new NodeClassFn({ name : unescapeBackslash(metaNode.label), param : param, orderNumber : metaNode.order }); if (metaNode.tooltip) { localRoot.tooltip = metaNode.tooltip; } // localRoot.iconTooltip = this.getIconTooltip(localRoot); var ch = metaNode.children; if (ch != null) { var len = ch.length; if (len == 0) { localRoot.setHasChilds(false); } else { for (var i = 0; i < len; i++) { var chNodeObj = ch[i]; if (chNodeObj) { var chTreeNode = this.processNode(chNodeObj); localRoot.addChild(chTreeNode); } } } localRoot.isloaded = true; } return localRoot; }); /** * Dynamically loads children for the node * Generates "children:loaded" event, if request was successful and userCallbackFn isn't specified. * Generates "server:error" event, if request was unsuccessful and errorCallbackFn isn't specified. * * @param parentNode {{@link dynamicTree.TreeNode}} the node, for which children should be loaded * @param userCallbackFn {Function} optional callback function, that will be called if request was successful * @param errorCallbackFn {Function} optional callback function, that will be called if request was unsuccessful */ dynamicTree.TreeSupport.addMethod('getTreeNodeChildren', function (parentNode, userCallbackFn, errorCallbackFn) { var uri = parentNode.param.uri; var callback = function(obj, ni, uc, ec) { return function() { return obj.getTreeNodeChildrenCallback(ni, uc, ec); } }(this, parentNode.id, userCallbackFn, errorCallbackFn); ajaxTargettedUpdate( this.urlGetChildren + '&provider=' + this.providerId + '&uri=' + encodeURIComponent(encodeURIComponent(uri)), {fillLocation:this.ajaxBufferId, callback:callback, errorHandler: baseErrorHandler, hideLoader: this.hideLoader} ); if (!this.inInit) { parentNode.wait(); } }); dynamicTree.TreeSupport.addMethod('getTreeNodeChildrenCallback', function (parentNodeId, userCallbackFn, errorCallbackFn) { var div = document.getElementById(this.nodeTextId); if (div == null) { if (errorCallbackFn) { errorCallbackFn(); } else { this.fireServerErrorEvent(); } return; } var ns = window.eval('(' + div.innerHTML.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>') + ')'); var parentNode = dynamicTree.nodes[parentNodeId]; if (this.modifyRootObject) { ns = this.modifyRootObject(ns, true, parentNode); } div = document.getElementById(this.ajaxBufferId); div.innerHTML = ''; parentNode.resetChilds(); parentNode.stopWaiting(); var len = ns.length; if (len == 0) { parentNode.setHasChilds(false); } else { var treeId = parentNode.getTreeId(); var tree = (treeId) ? dynamicTree.trees[treeId] : null; var tmpSortNodes = (tree) ? tree.sortNodes : null; if (tree) { tree.sortNodes = false; } for (var i = 0; i < len; i++) { var node = this.processNode(ns[i]); parentNode.addChild(node); } if (tree) { tree.sortNodes = tmpSortNodes; parentNode.resortChilds(); } } parentNode.isloaded = true; parentNode.refreshNode(); if (userCallbackFn) { userCallbackFn(parentNode.childs); } else { this.fireChildrenLoadedEvent(parentNode.childs); } }); /** * Loads children for several given nodes * Generates "multipleChildren:loaded" event, if request was successful and userCallbackFn isn't specified. * Generates "server:error" event, if request was unsuccessful and errorCallbackFn isn't specified. * @param parentNodes {Array<{@link dynamicTree.TreeNode}>} array of TreeNode instances * @param userCallbackFn {Function} optional callback function, that will be called if request was successful * @param errorCallbackFn {Function} optional callback function, that will be called if request was unsuccessful */ dynamicTree.TreeSupport.addMethod('getTreeMultipleNodesChildren', function (parentNodes, userCallbackFn, errorCallbackFn) { var uri = ''; var ids = []; var i; if (parentNodes && parentNodes.length) { for (i = 0; i < parentNodes.length; i++) { if (i > 0) { uri += ','; } uri += encodeURIComponent(encodeURIComponent(parentNodes[i].param.uri)); ids[i] = parentNodes[i].id; } } if (!uri.length) { // no nodes requested - just return return; } var callback = function(obj, ni, uc, ec) { return function() { return obj.getTreeMultipleNodesChildrenCallback(ni, uc, ec); } }(this, ids, userCallbackFn, errorCallbackFn); ajaxTargettedUpdate( this.urlGetMultipleChildren + '&provider=' + this.providerId,// + '&uris=' + uri, { fillLocation:this.ajaxBufferId, callback:callback, postData:'uris=' + uri, errorHandler: baseErrorHandler, hideLoader: this.hideLoader }); if (!this.inInit) { for (i = 0; i < parentNodes.length; i++) { parentNodes[i].wait(); } } }); dynamicTree.TreeSupport.addMethod('getTreeMultipleNodesChildrenCallback', function (parentNodeIds, userCallbackFn, errorCallbackFn) { var div = document.getElementById(this.nodeTextId); if (div == null) { if (errorCallbackFn) { errorCallbackFn(); } else { this.fireServerErrorEvent(); } return; } var ns = window.eval('(' + div.innerHTML.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>') + ')'); div = document.getElementById(this.ajaxBufferId); div.innerHTML = ''; if (userCallbackFn) { userCallbackFn(parentNodeIds, ns); } else { this.setMultipleNodesChilden(parentNodeIds, ns); this.fireMultipleChildrenLoadedEvent(parentNodeIds, ns); } }); /** * Default processor for getTreeMultipleNodesChildren * * @param parentNodeIds {Array} parent node IDs for which server was requested * @param nodeHolders {Array} processed server response object * @param noRender {Boolean} if true nodes which loaded from server will not be displayed */ dynamicTree.TreeSupport.addMethod('setMultipleNodesChilden', function (parentNodeIds, nodeHolders, noRender) { if (parentNodeIds && nodeHolders) { for (var nn = 0; nn < nodeHolders.length; nn++) { var nodeHolder = nodeHolders[nn]; var ns = nodeHolder.children; // find a parentNode var parentNode = null; for (var pn = 0; pn < parentNodeIds.length; pn ++) { var tempnode = dynamicTree.nodes[parentNodeIds[pn]]; if (tempnode.param.uri == nodeHolder.parentUri) { parentNode = tempnode; break; } } if (parentNode) { parentNode.resetChilds(); parentNode.stopWaiting(); var len = ns.length; if (len == 0) { parentNode.setHasChilds(false); } else { var treeId = parentNode.getTreeId(); var tree = (treeId) ? dynamicTree.trees[treeId] : null; var tmpSortNodes = (tree) ? tree.sortNodes : null; if (tree) { tree.sortNodes = false; } for (var i = 0; i < len; i++) { var node = this.processNode(ns[i]); parentNode.addChild(node); } if (tree) { tree.sortNodes = tmpSortNodes; parentNode.resortChilds(); } } parentNode.isloaded = true; if (!noRender) { parentNode.refreshNode(); } } } } }); /** * Dynamically loads children for a given node. * Makes sure that all requested nodes get prefetched. * Nodes to be prefetched have to have parentNode as a common (grand*)parent * * Generates "multipleChildren:loaded" event, if request was successful and userCallbackFn isn't specified. * Generates "server:error" event, if request was unsuccessful and errorCallbackFn isn't specified. * @param parentNode {{@link dynamicTree.TreeNode}} the node for which children should be loaded * @param prefetchedListStr {String} comma separated URIs to be prefetched * @param userCallbackFn {Function} optional callback function, that will be called if request was successful * @param errorCallbackFn {Function} optional callback function, that will be called if request was unsuccessful */ dynamicTree.TreeSupport.addMethod('getTreeNodeChildrenPrefetched', function (parentNode, prefetchedListStr, userCallbackFn, errorCallbackFn, depth, disableSorting, noRender) { var uri = parentNode.param.uri; var url = this.urlGetNode + '&provider=' + this.providerId + '&uri=' + uri; var prefetch = ''; if (prefetchedListStr) { // url += '&prefetch=' + prefetchedListStr; prefetch = '&prefetch=' + prefetchedListStr; } if (depth) { url += '&depth=' + depth; } var callback = function(obj, ni, uc, ec, ds, nr) { return function() { return obj.getTreeNodeChildrenPrefetchedCallback(ni, uc, ec, ds, nr); } }(this, parentNode.id, userCallbackFn, errorCallbackFn, disableSorting, noRender); ajaxTargettedUpdate( url, { fillLocation:this.ajaxBufferId, callback:callback, postData:prefetch, errorHandler: baseErrorHandler, hideLoader: this.hideLoader }); if (!this.inInit) { parentNode.wait(); } }); dynamicTree.TreeSupport.addMethod('getTreeNodeChildrenPrefetchedCallback', function (parentNodeId, userCallbackFn, errorCallbackFn, disableSorting, noRender) { var div = document.getElementById(this.nodeTextId); if (div == null) { if (errorCallbackFn) { errorCallbackFn(); } else { this.fireServerErrorEvent(); } return; } var n = window.eval('(' + div.innerHTML.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>') + ')'); div = document.getElementById(this.ajaxBufferId); div.innerHTML = ''; var parentNode = dynamicTree.nodes[parentNodeId]; parentNode.resetChilds(); parentNode.stopWaiting(); if (n.children) { var treeId = parentNode.getTreeId(); var tree = (treeId) ? dynamicTree.trees[treeId] : null; var tmpSortNodes = (tree) ? tree.sortNodes : null; if (tree) { tree.sortNodes = false; } for (var i = 0; i < n.children.length; i++) { var node = this.processNode(n.children[i]); parentNode.addChild(node); } if (tree) { tree.sortNodes = tmpSortNodes; disableSorting || parentNode.resortChilds(); } } parentNode.isloaded = true; if (!noRender) { parentNode.refreshNode(); } if (userCallbackFn) { userCallbackFn(); } else { this.fireChildredPrefetchedLoadedEvent(parentNode.childs); } }); /** * Expands the tree up to a given node, and then select it * * @param uriStr {String} path to the node * @param fnAction {Function} optional action to be called * @param findFirstChild {Boolean} optional parameter, if true first child of the node will be open */ dynamicTree.TreeSupport.addMethod('openAndSelectNode', function(uriStr, fnAction, findFirstChild) { var fn = function(node) { if (node.parent) { var tree = dynamicTree.trees[node.getTreeId()]; if (tree && tree.rootNode != node.parent && tree.getState(node.parent.id) == dynamicTree.TreeNode.State.CLOSED) { node.parent.handleNode(); } } tree._selectOrEditNode(undefined, node, false); }; this.processNodePath(uriStr, fn, findFirstChild); // scroll tree container to make selected node visible var selectedNode = this.getSelectedNode(); if (selectedNode) { selectedNode.scroll(); } if (fnAction) { fnAction(); } }); dynamicTree.TreeSupport.addMethod('processNodePath', function(uriStr, fnForNode, findFirstChild) { var path = uriStr.split('/'); var node = this.getRootNode(); var i; for (i = 0; i < path.length; i++) { if (!path[i]) { continue; } var oldNode = node; node = this.findNodeChildByMetaName(node, path[i]); if (!node) { if(findFirstChild){ node = this.findNodeFirstNodeChildByAlphabeticalOrder(oldNode); if(!node){ return; } }else{ return; } } fnForNode(node); } }); /** * Returns TreeNode which is last node in node hierarchical chain for a given uri * If returned node corresponds to uriStr, it means no more server requests needed * If it corresponds to parent (grand-parent, etc.), the value shows existing root * from which the rest should be requested from server * Example: uriStr='/area/subarea/dept/prod1', return is TreeNode with uri '/area/subarea'. * It means, we need to load children of 'subarea' and children of 'dept' from server * * @param uriStr {String} */ dynamicTree.TreeSupport.addMethod('findLastLoadedNode', function(uriStr) { var nodeHolder = { node: null }; var fn = function(holder) { return function(node) { holder.node = node; } }(nodeHolder); this.processNodePath(uriStr, fn); return nodeHolder.node; }); dynamicTree.TreeSupport.addMethod('findNodeChildByMetaName', function (node, name) { if (node.hasChilds()) { for (var i = 0; i < node.childs.length; i++) { if (node.childs[i].param.id == name) { return node.childs[i]; } } } return null; }); /** * This function gets the first child based on alphabetical order. * For e.g. if the node contains children: {topic, mail, apple, orange}, apple will be returned.. * * @param node {{@link dynamicTree.TreeNode}} parent node */ dynamicTree.TreeSupport.addMethod('findNodeFirstNodeChildByAlphabeticalOrder', function (node){ var firstchildName = null; var firstchildIndex = null; if(node.childs.length > 0){ firstchildName = node.childs[0].param.id; firstchildIndex = 0; var tempChildName = null; var tempChildIndex = null; for(var index = 1; index < node.childs.length; index++){ tempChildName = node.childs[index].param.id; tempChildIndex = index; var loopCount = (tempChildName.length < firstchildName.length) ? tempChildName.length : firstchildName.length; for(var loopIndex = 0; loopIndex < loopCount; loopIndex++){ if(tempChildName.charCodeAt(loopIndex) < firstchildName.charCodeAt(loopIndex)){ firstchildName = tempChildName; firstchildIndex = tempChildIndex; break; }else if(tempChildName.charCodeAt(loopIndex) > firstchildName.charCodeAt(loopIndex)){ //we break out. break; } } } return node.childs[firstchildIndex]; }else{ return null; } }); /** * Recursively searching for node by node identifier starting from root node. * @param nodeId Node identifier. */ dynamicTree.TreeSupport.addMethod('findNodeById', function (nodeId, startNode){ return (function _findNodeById(nodeId, node){ if (!node || !nodeId) { return null; } if (node.param.id === nodeId) { return node; } if (node.hasChilds()) { for (var i = 0; i < node.childs.length; ++i) { var found = _findNodeById(nodeId, node.childs[i]); if (found) { return found; } } } return null; })(nodeId, startNode ? startNode : this.getRootNode()); }); /** * Checks that node has child folders that are loaded and open * * @param node {{@link dynamicTree.TreeNode}} parent node */ dynamicTree.TreeSupport.addMethod('hasVisibleFolders', function(rootObj) { if (this.bShowRoot) { return true; } var children = rootObj.children; if (children) { for (var i = 0; i < children.length; i++) { var grandchildren = rootObj.children[i].children; if (grandchildren && grandchildren.length>0) { return true; } } } return false; }); /////////////////////////////////////////// // Message system support /////////////////////////////////////////// dynamicTree.TreeSupport.addMethod('getMessage', function (messageId, userCallbackFn, errorCallbackFn) { var url = this.urlGetMessage + '&messageId=' + messageId; var callback = function(obj, uc, ec) { return function() { return obj.getMessageCallback(uc, ec); } }(this, userCallbackFn, errorCallbackFn); ajaxTargettedUpdate( url, {fillLocation:this.ajaxBufferId, callback:callback, errorHandler: baseErrorHandler, hideLoader: this.hideLoader} ); }); dynamicTree.TreeSupport.addMethod('getMessageCallback', function (userCallbackFn, errorCallbackFn) { var div = document.getElementById(this.ajaxBufferId); if (div == null) { if (errorCallbackFn) { errorCallbackFn(); } return; } // clean AJAX buffer var text = trim(div.innerHTML); div.innerHTML = ''; if (userCallbackFn) { userCallbackFn(text); } }); //////////////////////////////////////////////////////// // Message system support //////////////////////////////////////////////////////// /** * Generates "server:error" event. */ dynamicTree.TreeSupport.addMethod('fireServerErrorEvent', function() { this._getElement().fire('server:error', { }); }); /** * Generates "tree:loaded" event. */ dynamicTree.TreeSupport.addMethod('fireTreeLoadedEvent', function(tree) { this._getElement().fire('tree:loaded', { tree: tree }); }); /** * Generates "children:loaded" event. */ dynamicTree.TreeSupport.addMethod('fireChildrenLoadedEvent', function(nodes) { this._getElement().fire('children:loaded', { nodes: nodes }); }); /** * Generates "multipleChildren:loaded" event. */ dynamicTree.TreeSupport.addMethod('fireMultipleChildrenLoadedEvent', function(parentNodeIds, metaNodes) { this._getElement().fire('multipleChildren:loaded', { parentNodeIds: parentNodeIds, metaNodes: metaNodes }); }); /** * Generates "childredPrefetched:loaded" event. */ dynamicTree.TreeSupport.addMethod('fireChildredPrefetchedLoadedEvent', function(nodes) { this._getElement().fire('childredPrefetched:loaded', { nodes: nodes }); });