/// /// /// /// /// /// /// /// Umbraco.Sys.registerNamespace("Umbraco.Controls"); (function($) { $.fn.UmbracoTree = function(opts) { /// jQuery plugin to create an Umbraco tree. See option remarks below. return this.each(function() { new Umbraco.Controls.UmbracoTree().init($(this), opts); }); }; $.fn.UmbracoTreeAPI = function() { /// exposes the Umbraco Tree api for the selected object if ($(this).length != 1) { throw "UmbracoTreeAPI selector requires that there be exactly one control selected, this selector returns " + $(this).length; }; // check if there's an api stored for the id of the object specified, if there's not // check if the first child is a div and if that has the api specified var api = Umbraco.Controls.UmbracoTree.inst[$(this).attr("id")] || null; if (api == null) return Umbraco.Controls.UmbracoTree.inst[$(this).children("div").attr("id")] || null; return api; }; Umbraco.Controls.TreeDefaultOptions = function() { return { doNotInit: false, //this is used for the main umbraco tree so that the tree doesn't fully initialize until rebuildTree is explicitly called jsonFullMenu: {}, //The tree menu, by default is empty appActions: null, //A reference to a MenuActions object deletingText: "Deleting...", //the txt to display when a node is deleting treeMode: "standard", //determines the type of tree: false/null = normal, 'checkbox' = checkboxes enabled, 'inheritedcheckbox' = parent nodes have checks inherited from children recycleBinId: -20, //the id of the recycle bin for the current tree serviceUrl: "", //Url path for the tree client service dataUrl: "", //Url path for the tree data service //These are all properties of the ITreeService and are //used to pass the properties in to the InitAppTreeData service app: "", //the application name to render treeType: "", //the active tree application showContext: true, //boolean indicating whether or not to show a context menu isDialog: false, dialogMode: "none", //boolean indicating whether or not the tree is in dialog mode functionToCall: "", nodeKey: "" }; } Umbraco.Controls.UmbracoTree = function() { /// /// The object that manages the Umbraco tree. /// Has these events: syncNotFound, syncFound, rebuiltTree, newchildNodeFound, nodeMoved, nodeCopied, ajaxError, nodeClicked /// return { _opts: {}, _cntr: ++Umbraco.Controls.UmbracoTree.cntr, //increments the number of tree instances. _containerId: null, _context: null, //the jquery context used to get the container _actionNode: new Umbraco.Controls.NodeDefinition(), //the most recent node right clicked for context menu _activeTreeType: "content", //tracks which is the active tree type, this is used in searching and syncing. _tree: null, //reference to the jsTree object _isEditMode: false, //not really used YET _isDebug: false, //set to true to enable alert debugging _loadedApps: [], //stores the application names that have been loaded to track which JavaScript code has been inserted into the DOM _treeClass: "umbTree", //used for other libraries to detect which elements are an umbraco tree _currenAJAXRequest: false, //used to determine if there is currently an ajax request being executed. _isSyncing: false, addEventHandler: function(fnName, fn) { /// Adds an event listener to the event name event this._getContainer().bind(fnName, fn); }, removeEventHandler: function(fnName, fn) { /// Removes an event listener to the event name event this._getContainer().unbind(fnName, fn); }, _raiseEvent: function(evName, args) { /// Raises an event and attaches it to the container this._getContainer().trigger(evName, args); }, init: function(jItem, opts) { /// Initializes the tree with the options and stores the tree API in the jQuery data object for the current element this._debug("init: creating new tree with class/id: " + jItem.attr("class") + " / " + jItem.attr("id")); this._opts = $.extend(Umbraco.Controls.TreeDefaultOptions(), opts); this._context = jItem.get(0).ownerDocument; //wire up event handlers if (this._opts.appActions != null) { var _this = this; //wrapped functions maintain scope this._opts.appActions.addEventHandler("nodeDeleting", function(E) { _this.onNodeDeleting(E) }); this._opts.appActions.addEventHandler("nodeDeleted", function(E) { _this.onNodeDeleted(E) }); this._opts.appActions.addEventHandler("nodeRefresh", function(E) { _this.onNodeRefresh(E) }); } this._containerId = jItem.attr("id"); if (!this._opts.doNotInit) { //initializes the jsTree this._tree = $.tree.create(); this._tree.init(this._getContainer(), this._getInitOptions()); } jItem.addClass(this._treeClass); //store a reference to this api by the id and the counter Umbraco.Controls.UmbracoTree.inst[this._cntr] = this; if (!this._getContainer().attr("id")) this._getContainer().attr("id", "UmbTree_" + this._cntr); Umbraco.Controls.UmbracoTree.inst[this._getContainer().attr("id")] = Umbraco.Controls.UmbracoTree.inst[this._cntr]; }, setRecycleBinNodeId: function(id) { this._opts.recycleBinId = id; }, clearTreeCache: function() { // This will remove all stored trees in client side cache so that the next time a tree needs loading it will be refreshed this._debug("clearTreeCache..."); this._loadedApps = []; }, toggleEditMode: function(enable) { this._debug("Edit mode. Currently: " + this._tree.settings.rules.draggable); this._isEditMode = enable; this.saveTreeState(this._opts.app); //need to trick the system so it thinks it's a different app, then rebuild with new rules var app = this._opts.app; this._opts.app = "temp"; this.rebuildTree(app); this._debug("Edit mode. New Mode: " + this._tree.settings.rules.draggable); if (this._opts.appActions) this._opts.appActions.showSpeachBubble("info", "Tree Edit Mode", "The tree is now operating in edit mode"); }, refreshTree: function(treeType) { /// This wraps the standard jsTree functionality unless a treeType is specified. If one is, then it will just reload that nodes children this._debug("refreshTree: " + treeType); if (!treeType) { this.rebuildTree(); } else { var allRoots = this._getContainer().find("li[rel='rootNode']"); var _this = this; var root = allRoots.filter(function() { return ($(this).attr("umb:type") == _this._activeTreeType); //filter based on custom namespace requires custom function }); if (root.length == 1) { this._debug("refreshTree: reloading tree type: " + treeType); this._loadChildNodes(root); } else { //couldn't find it, so refresh the whole tree this.rebuildTree(); } } }, rebuildTree: function(app, callback) { /// This will rebuild the tree structure for the application specified this._debug("rebuildTree"); //if app is null, then we will rebuild the current app which also means clearing the cache. if (!app) { this.clearTreeCache(); this._opts.app = this._opts.app; } else if (this._tree&& (this._opts.app.toLowerCase() == app.toLowerCase())) { this._debug("not rebuilding"); //don't rebuild if the tree object exists, the app that's being requested to be loaded is //flagged as already loaded, and the tree actually has nodes in it return; } else { this._opts.app = app; } //kill the tree if (this._tree) { this._tree.destroy(); } var _this = this; //check if we should rebuild from a saved tree var saveData = this._loadedApps["tree_" + app]; if (saveData != null) { this._debug("rebuildTree: rebuilding from cache: app = " + app); //create the tree from the saved data. //this._initNode = saveData.d; this._tree = $.tree.create(); this._tree.init(this._getContainer(), this._getInitOptions(saveData.d)); //ensure the static data is gone this._tree.settings.data.opts.static = null; this._configureNodes(this._getContainer().find("li"), true); //select the last node var lastSelected = saveData.selected != null ? $(saveData.selected[0]).attr("id") : null; if (lastSelected != null) { //create an event handler for the tree sync var _this = this; var foundHandler = function(EV, node) { //remove the event handler from firing again _this.removeEventHandler("syncFound", foundHandler); _this._debug("rebuildTree: node synced, selecting node..."); //ensure the node is selected, ensure the event is fired and reselect is true since jsTree thinks this node is already selected by id _this.selectNode(node, false, true); }; this._debug("rebuildTree: syncing to last selected: " + lastSelected); //add the event handler for the tree sync and sync the tree this.addEventHandler("syncFound", foundHandler); this.setActiveTreeType($(saveData.selected[0]).attr("umb:type")); this.syncTree(lastSelected); } if (typeof callback == "function") callback.apply(this, [lastSelected]); } else { this._debug("rebuildTree: rebuilding from scratch: app = " + app); this._currentAJAXRequest = true; _this._tree = $.tree.create(); _this._tree.init(_this._getContainer(), _this._getInitOptions()); if (typeof callback == "function") callback.apply(this, []); } }, saveTreeState: function(appAlias) { /// /// Saves the state of the current application trees so we can restore it next time the user visits the app /// this._debug("saveTreeState: " + appAlias + " : ajax request? " + this._currentAJAXRequest); //clear the saved data for the current app before saving this._loadedApps["tree_" + appAlias] = null; //if an ajax request is currently in progress, abort saving the tree state and set the //data object for the application to null. if (!this._currentAJAXRequest) { //only save the data if there are nodes var nodeCount = this._getContainer().find("li[rel='dataNode']").length; if (nodeCount > 0) { this._debug("saveTreeState: node count = " + nodeCount); var treeData = this._tree.get(); //need to update the 'state' of the data. jsTree get doesn't return the state of nodes properly! this._updateJSONNodeState(treeData); this._debug("saveTreeState: treeData = " + treeData); this._loadedApps["tree_" + appAlias] = { selected: this._tree.selected, d: treeData }; } } }, _updateJSONNodeState: function(obj) { /// /// A recursive function to store the state of the node for the JSON object when using saveTreeState. /// This is required since jsTree doesn't output the state of the tree nodes with the request to getJSON method. /// This is also required to save the correct title for each node since we store our title in a div tag, not just the a tag /// var node = $("li[id='" + obj.attributes.id + "']").filter(function() { return ($(this).attr("umb:type") == obj.attributes["umb:type"]); //filter based on custom namespace requires custom function }); //saves the correct title obj.data.title = $.trim(node.children("a").children("div").text()); obj.state = obj.data.state; //ensures that the style property of the data contains only single quotes since jsTree puts double around the attr for (var i in obj.data.attributes) { if (!obj.data.attributes.hasOwnProperty(i)) continue; if (i == "style" || i == "class") { obj.data.attributes[i] = obj.data.attributes[i].replace(/\"/g, "'"); } } //recurse through children if (obj.children != null) { for (var x in obj.children) { this._updateJSONNodeState(obj.children[x]); } } }, syncTree: function(path, forceReload) { /// /// Syncronizes the tree with the path supplied and makes that node visible/selected. /// /// The path of the node /// If true, will ensure that the node to be synced is synced with data from the server this._debug("syncTree: " + path + ", " + forceReload); //set the flag so that multiple synces aren't attempted this._isSyncing = true; this._syncTree.call(this, path, forceReload, null, null); }, childNodeCreated: function() { /// /// Reloads the children of the current action node and selects the node that didn't exist there before. /// If it cannot determine which node is new, then no node is selected. If the children are not already /// loaded, then it is impossible for this method to determine which child is new. /// this._debug("childNodeCreated"); //store the current child ids so we can determine which one is the new one var childrenIds = new Array(); this._actionNode.jsNode.find("ul > li").each(function() { childrenIds.push($(this).attr("id")); }); var _this = this; var currId = this._actionNode.nodeId; this.reloadActionNode(true, false, function(success) { if (success && childrenIds.length > 0) { var found = false; var actionNode = _this.findNode(currId); if (actionNode) { actionNode.find("ul > li").each(function() { //if the id of the current child is not found in the original list, then this is the new one, store it if ($.inArray($(this).attr("id"), childrenIds) == -1) { found = $(this); } }); } if (found) { _this._debug("childNodeCreated: selecting new child node: " + found.attr("id")); _this.selectNode(found, true, true); _this._raiseEvent("newChildNodeFound", [found]); return; } } _this._debug("childNodeCreated: could not select new child!"); }); }, moveNode: function(nodeId, parentPath) { /// Moves a node in the tree. This will remove the existing node by id and sync the tree to the new path this._debug("moveNode"); //remove the old node var old = this.findNode(nodeId); if (old) old.remove(); //build the path to the new node var newPath = parentPath + "," + nodeId; //create an event handler for the tree sync var _this = this; var foundHandler = function(EV, node) { //remove the event handler from firing again _this.removeEventHandler("syncFound", foundHandler); //ensure the node is selected, ensure the event is fired and reselect is true since jsTree thinks this node is already selected by id _this.selectNode(node, false, true); _this._raiseEvent("nodeMoved", [node]); }; //add the event handler for the tree sync and sync the tree this.addEventHandler("syncFound", foundHandler); this.syncTree(newPath); }, copyNode: function(nodeId, parentPath) { /// Copies a node in the tree. This will keep the current node selected but will sync the tree to show the copied node too this._debug("copyNode"); var originalNode = this.findNode(nodeId); //create an event handler for the tree sync var _this = this; var foundHandler = function(EV, node) { //remove the event handler from firing again _this.removeEventHandler("syncFound", foundHandler); //now that the new parent node is found, expand it _this._loadChildNodes(node, null); //reselect the original node since sync will select the one that was copied if (originalNode) _this.selectNode(originalNode, true); _this._raiseEvent("nodeCopied", [node]); }; //add the event handler for the tree sync and sync the to the parent path this.addEventHandler("syncFound", foundHandler); this.syncTree(parentPath); }, findNode: function(nodeId, findGlobal) { /// Returns either the found branch or false if not found in the tree /// Optional. If true, disregards the tree type and searches the entire tree for the id var _this = this; var branch = this._getContainer().find("li[id='" + nodeId + "']"); if (!findGlobal) branch = branch.filter(function() { return ($(this).attr("umb:type") == _this._activeTreeType); //filter based on custom namespace requires custom function }); var found = branch.length > 0 ? branch : false; this._debug("findNode: " + nodeId + " in '" + this._activeTreeType + "' tree. Found? " + found); return found; }, selectNode: function(node, supressEvent, reselect) { /// /// Makes the selected node the active node, but only if it is not already selected or if reselect is true. /// /// If set to true, will select the node but will supress the onSelected event /// If set to true, will call the select_branch method even if the node is already selected //this._debug("selectNode, edit mode? " + this._isEditMode); var selectedId = this._tree.selected != null ? $(this._tree.selected[0]).attr("id") : null; this._debug("selectNode (" + node.attr("id") + "). supressEvent? " + supressEvent + ", reselect? " + reselect); if (reselect || (selectedId == null || selectedId != node.attr("id"))) { //if we don't wan the event to fire, we'll set the callback to a null method and set it back after we call the select_branch method if (supressEvent || this._isEditMode) { this._tree.settings.callback.onselect = function() { }; } this._tree.select_branch(node); //reset the method / maintain scope in callback var _this = this; this._tree.settings.callback.onselect = function(N, T) { _this.onSelect(N, T) }; } }, reloadActionNode: function(supressSelect, supressChildReload, callback) { /// /// Gets the current action node's parent's data source url, then passes this url and the current action node's id /// to a web service. The webservice will find the JSON data for the current action node and return it. This /// will parse the returned JSON into html and replace the current action nodes' markup with the refreshed server data. /// If by chance, the ajax call fails because of inconsistent data (a developer has implemented poor tree design), then /// this use the build in jsTree reload which works ok. /// /// /// A callback function which will have a boolean parameter passed. True = the reload was succesful, /// False = the reload failed and the generic _tree.refresh() method was used. /// this._debug("reloadActionNode: supressSelect = " + supressSelect + ", supressChildReload = " + supressChildReload); if (this._actionNode != null && this._actionNode.jsNode != null) { var nodeParent = this._actionNode.jsNode.parents("li:first"); this._debug("reloadActionNode: found " + nodeParent.length + " parent nodes"); if (nodeParent.length == 1) { var nodeDef = this.getNodeDef(nodeParent); this._debug("reloadActionNode: loading ajax for node: " + nodeDef.nodeId); var _this = this; //replace the node to refresh with loading and return the new loading element var toReplace = $("
  • " + (this._tree.settings.lang.loading || "Loading ...") + "
  • ").replaceAll(this._actionNode.jsNode); $.get(this._getUrl(nodeDef.sourceUrl), null, function(msg) { if (!msg || msg.length == 0) { _this._debug("reloadActionNode: error loading ajax data, performing jsTree refresh"); _this.rebuildTree(); /*try jsTree refresh as last resort */ if (callback != null) callback.call(_this, false); return; } //filter the results to find the object corresponding to the one we want refreshed var oFound = null; for (var o in msg) { if (msg[o].attributes != null && msg[o].attributes.id == _this._actionNode.nodeId) { oFound = $.tree.datastores.json().parse(msg[o], _this._tree); //ensure the tree type is the same too if ($(oFound).attr("umb:type") == _this._actionNode.treeType) { break; } else { oFound = null; } } } if (oFound != null) { _this._debug("reloadActionNode: node is refreshed! : " + supressSelect); var reloaded = $(oFound).replaceAll(toReplace); _this._configureNodes(reloaded, true); if (!supressSelect) _this.selectNode(reloaded, true, true); if (!supressChildReload) { _this._loadChildNodes(reloaded, function() { if (callback != null) callback.call(_this, true); }); } else { if (callback != null) callback.call(_this, true); } } else { _this._debug("reloadActionNode: error finding child node in ajax data, performing jsTree refresh"); _this.rebuildTree(); /*try jsTree refresh as last resort */ if (callback != null) callback.call(_this, false); } }, "json"); return; } this._debug("reloadActionNode: error finding parent node, performing jsTree refresh"); this.rebuildTree(); /*try jsTree refresh as last resort */ if (callback != null) callback.call(this, false); } }, getActionNode: function() { /// Returns the latest node interacted with this._debug("getActionNode: " + this._actionNode.nodeId); return this._actionNode; }, setActiveTreeType: function(treeType) { /// /// All interactions with the tree are done so based on the current tree type (i.e. content, media). /// When sycning, or searching, the operations will be done on the current tree type so developers /// can explicitly specify on with this method before performing the operations. /// The active tree type is always updated any time a node interaction takes place. /// this._activeTreeType = treeType; }, onNodeDeleting: function(EV) { /// Event handler for when a tree node is about to be deleted this._debug("onNodeDeleting") //first, close the branch this._tree.close_branch(this._actionNode.jsNode); //show the deleting text this._actionNode.jsNode.find("a div") .html(this._opts.deletingText) .effect("highlight", {}, 1000); }, onNodeDeleted: function(EV) { /// Event handler for when a tree node is deleted after ajax call this._debug("onNodeDeleted"); var nodeToDel = this._actionNode.jsNode; //ensure the branch is closed this._tree.close_branch(nodeToDel); //make the node disapear nodeToDel.hide("drop", { direction: "down" }, 400, function() { //remove the node from the DOM, do this after 1 second as IE doesn't like it when you try this right away. setTimeout(function() { nodeToDel.remove(); }, 1000); }); this._updateRecycleBin(); }, onNodeRefresh: function(EV) { /// Handles the nodeRefresh event of the context menu and does the refreshing this._debug("onNodeRefresh"); this._loadChildNodes(this._actionNode.jsNode, null); }, onSelect: function(NODE, TREE_OBJ) { /// Fires the JS associated with the node, if the tree is in edit mode, allows for rename instead //this._debug("onSelect, edit mode? " + this._isEditMode); this._debug("onSelect"); if (this._isEditMode) { this._tree.rename(NODE); return false; } else { this.setActiveTreeType($(NODE).attr("umb:type")); var js = $(NODE).children("a").attr("href").replace("javascript:", ""); this._debug("onSelect: js: " + js); try { var func = eval(js); if (func != null) { func.call(); } } catch (e) { } return true; } }, onBeforeOpen: function(NODE, TREE_OBJ) { /// Before opening child nodes, ensure that the data method and url are set properly this._currentAJAXRequest = true; TREE_OBJ.settings.data.opts.url = this._opts.dataUrl; TREE_OBJ.settings.data.opts.method = "GET"; }, onJSONData: function(DATA, TREE_OBJ) { this._debug("onJSONData"); this._ensureContext(); this._currentAJAXRequest = false; if (typeof DATA.d != "undefined") { var msg = DATA.d; //recreates the tree if ($.inArray(msg.app, this._loadedApps) == -1) { this._debug("loading js for app: " + msg.app); this._loadedApps.push(msg.app); //inject the scripts this._getContainer().after(""); } return eval(msg.json); } return DATA; }, onBeforeRequest: function(NODE, TREE_OBJ) { this._ensureContext(); if (TREE_OBJ.settings.data.opts.method == "POST") { var parameters = "{'app':'" + this._opts.app + "','showContextMenu':'" + this._opts.showContext + "', 'isDialog':'" + this._opts.isDialog + "', 'dialogMode':'" + this._opts.dialogMode + "', 'treeType':'" + this._opts.treeType + "', 'functionToCall':'" + this._opts.functionToCall + "', 'nodeKey':'" + this._opts.nodeKey + "'}" return parameters; } else { var nodeDef = this.getNodeDef($(NODE)); return this._getUrlParams(nodeDef.sourceUrl); } }, onChange: function(NODE, TREE_OBJ) { //bubble an event! this._raiseEvent("nodeClicked", [NODE]); }, onBeforeContext: function(NODE, TREE_OBJ, EV) { //update the action node's NodeDefinition and set the active tree type this._actionNode = this.getNodeDef($(NODE)); this.setActiveTreeType($(NODE).attr("umb:type")); this._debug("onBeforeContext: " + this._actionNode.menu); return this._actionNode.menu; }, onLoad: function(TREE_OBJ) { /// When the application first loads, load the child nodes this._debug("onLoad"); //ensure the static data is gone this._tree.settings.data.opts.static = null; var _this = this; _this._loadChildNodes($(_this._getContainer()).find("li"), null); }, onParse: function(STR, TREE_OBJ) { this._debug("onParse"); this._ensureContext(); var obj = $(STR); this._configureNodes(obj); //this will return the full html of the configured node return $('
    ').append($(obj).clone()).remove().html(); }, onDestroy: function(TREE_OBJ) { /// /// When the tree is destroyed we need to ensure that all of the events both /// live and bind are gone. For some reason the jstree unbinding doesn't seem to do it's job /// so instead we need to clear all of the events ourselves /// this._debug("onDestroy: " + TREE_OBJ.container.attr("id")); TREE_OBJ.container .unbind("contextmenu") .unbind("click") .unbind("dblclick") .unbind("mouseover") .unbind("mousedown") .unbind("mouseup"); $("a", TREE_OBJ.container.get(0)) .die("contextmenu") .die("click") .die("dblclick") .die("mouseover") .die("mousedown"); //also need to kill the custom selector we've fixed in jstree source $("#" + TREE_OBJ.container.attr("id") + " li").die("click"); $("li", TREE_OBJ.container.get(0)) .die("click"); }, onError: function(ERR, TREE_OBJ) { this._debug("ERROR!!!!! " + ERR); }, _debug: function(strMsg) { if (this._isDebug && Sys && Sys.Debug) { Sys.Debug.trace("UmbracoTree: " + strMsg); } }, _configureNodes: function(nodes, reconfigure) { /// /// Ensures the node is configured properly after it's loaded via ajax. /// This includes setting overlays and ensuring the correct icon paths are used. /// This also ensures that the correct markup is rendered for the tree (i.e. inserts html nodes for text, etc...) /// var _this = this; //don't process the nodes that have already been loaded, unless reconfigure is true if (!reconfigure) { nodes = nodes.not("li[class*='loaded']"); } this._debug("_configureNodes: " + nodes.length); var rxInput = new RegExp("\\boverlay-\\w+\\b", "gi"); nodes.each(function() { //if it is checkbox tree (not standard), don't worry about overlays and remove the default icon. if (_this._opts.treeMode != "standard") { $(this).children("a:first").css("background", ""); return; } //remove all overlays if reconfiguring $(this).children("div").remove(); var m = $(this).attr("class").match(rxInput); if (m != null) { for (i = 0; i < m.length; i++) { _this._debug("_configureNodes: adding overlay: " + m[i] + " for node: " + $(this).attr("id")); $(this).children("a:first").before("
    "); } } //create a div for the text var a = $(this).children("a"); var ins = a.children("ins"); ins.remove(); //need to remove before you do a .text() otherwise whitespace is included var txt = $("
    " + a.text() + "
    "); //check if it's not a sprite, if not then move the ins node just after the anchor, otherwise remove if (a.hasClass("noSpr")) { a.attr("style", ins.attr("style")); } else { } a.html(txt); //add the loaded class to each element so we know not to process it again $(this).addClass("loaded"); }); }, getNodeDef: function(NODE) { /// Converts a jquery node with metadata to a NodeDefinition //get our meta data stored with our node var nodedata = $(NODE).children("a").metadata({ type: 'attr', name: 'umb:nodedata' }); this._debug("getNodeDef: " + $(NODE).attr("id") + ", " + nodedata.nodeType + ", " + nodedata.source); var def = new Umbraco.Controls.NodeDefinition(); def.updateDefinition(this._tree, $(NODE), $(NODE).attr("id"), $(NODE).find("a > div").html(), nodedata.nodeType, nodedata.source, nodedata.menu, $(NODE).attr("umb:type")); return def; }, _updateRecycleBin: function() { /// Generally used for when a node is deleted. This will set the actionNode to the recycle bin node and force a refresh of it's children this._debug("_updateRecycleBin BinId: " + this._opts.recycleBinId); var rNode = this.findNode(this._opts.recycleBinId, true); if (rNode) { this._actionNode = this.getNodeDef(rNode); var _this = this; this.reloadActionNode(true, true, function(success) { if (success) { _this.findNode(_this._opts.recycleBinId, true).effect("highlight", {}, 1000); } }); } }, _ensureContext: function() { /// /// ensure that the tree object always has the correct context. /// this is a fix for the TinyMCE dialog window, as it tends to lose object context for some wacky reason /// when ajax calls are made. Works fine in all other instances. /// this._tree.container = this._getContainer(); }, _loadChildNodes: function(liNode, callback) { /// jsTree won't allow you to open a node that doesn't explitly have childen, this will force it to try /// a jquery object for the current li node this._debug("_loadChildNodes: " + liNode.attr("id")); liNode.removeClass("leaf"); var _this = this; //close branch will actually cause a select to happen so we'll intercept the select callback and then reset it once complete //if we don't wan the event to fire, we'll set the callback to a null method and set it back after we call the select_branch method this._tree.settings.callback.onselect = function() { }; this._tree.close_branch(liNode, true); this._tree.settings.callback.onselect = function(N, T) { _this.onSelect(N, T) }; liNode.children("ul:eq(0)").remove(); this._tree.open_branch(liNode, false, callback); }, _syncTree: function(path, forceReload, numPaths, numAsync) { /// /// This is the internal method that will recursively search for the nodes to sync. If an invalid path is /// passed to this method, it will raise an event which can be handled. /// /// The path of the node to find /// If true, will ensure that the node to be synced is synced with data from the server /// the number of id's deep to search starting from the end of the path. Used in recursion. /// the number of async calls made so far to sync. Used in recursion and used to determine if the found node has been loaded by ajax. this._debug("_syncTree"); var paths = path.split(","); var found = null; var foundIndex = null; if (numPaths == null) numPaths = (paths.length - 0); for (var i = 0; i < numPaths; i++) { foundIndex = paths.length - (1 + i); found = this.findNode(paths[foundIndex]); this._debug("_syncTree: finding... " + paths[foundIndex] + " found? " + found); if (found) break; } //if no node has been found at all in the entire path, then bubble an error event if (!found) { this._debug("no node found in path: " + path + " : " + numPaths); this._isSyncing = false; //reset flag this._raiseEvent("syncNotFound", [path]); return; } //if the found node was not the end of the path, we need to load them in recursively. if (found.attr("id") != paths[paths.length - 1]) { var _this = this; this._loadChildNodes(found, function(NODE, TREE_OBJ) { //check if the next node to be found is in the children, if it is not, there's a problem bubble an event! var pathsToSearch = paths.length - (Number(foundIndex) + 1); if (_this.findNode(paths[foundIndex + 1])) { _this._syncTree(path, forceReload, pathsToSearch, (numAsync == null ? numAsync == 1 : ++numAsync)); } else { _this._debug("node not found in children: " + path + " : " + numPaths); this._isSyncing = false; //reset flag _this._raiseEvent("syncNotFound", [path]); } }); } else { //only force the reload of this nodes data if forceReload is specified and the node has not already come from the server var doReload = (forceReload && (numAsync == null || numAsync < 1)); this._debug("_syncTree: found! numAsync: " + numAsync + ", forceReload: " + forceReload + ", doReload: " + doReload); if (doReload) { this._actionNode = this.getNodeDef(found); this.reloadActionNode(false, true, null); } else { //we have found our node, select it but supress the selecting event if (found.attr("id") != "-1") this.selectNode(found, true); this._configureNodes(found, doReload); } this._isSyncing = false; //reset flag //bubble event this._raiseEvent("syncFound", [found]); } }, _getUrlParams: function(nodeSource) { /// This converts Url query string params to json var p = {}; if (nodeSource) { var urlSplit = nodeSource.split("?"); if (urlSplit.length > 1) { var sp = urlSplit[1].split("&"); for (var i = 0; i < sp.length; i++) { var e = sp[i].split("="); p[e[0]] = e[1]; } p["rnd2"] = Umbraco.Utils.generateRandom(); } } return p; }, _getUrl: function(nodeSource) { /// Returns the json service url if (nodeSource == null || nodeSource == "") { return this._opts.dataUrl; } var params = nodeSource.split("?")[1]; return this._opts.dataUrl + "?" + params + "&rnd2=" + Umbraco.Utils.generateRandom(); }, _getContainer: function() { return $("#" + this._containerId, this._context); }, _getInitOptions: function(initData) { /// return the initialization objects for the tree this._debug("_getInitOptions"); var _this = this; var options = { data: { type: "json", async: true, opts: { static: initData == null ? null : initData, method: "POST", url: _this._opts.serviceUrl, outer_attrib: ["id", "umb:type", "class", "rel"], inner_attrib: ["umb:nodedata", "href", "class", "style"] } }, ui: { dots: false, rtl: false, animation: false, hover_mode: true, theme_path: false, theme_name: "umbraco" }, langs: { new_node: "New folder", loading: "
    " + (this._tree.settings.lang.loading || "Loading ...") + "
    " }, callback: { //ensures that the node id isn't appended to the async url beforedata: function(N, T) { return _this.onBeforeRequest(N, T); }, //wrapped functions maintain scope in callback beforeopen: function(N, T) { _this.onBeforeOpen(N, T); }, onselect: function(N, T) { _this.onSelect(N, T); }, onchange: function(N, T) { _this.onChange(N, T); }, ondata: function(D, T) { return _this.onJSONData(D, T); }, onload: function(T) { if (initData == null) _this.onLoad(T); }, onparse: function(S, T) { return _this.onParse(S, T); }, error: function(E, T) { _this.onError(E, T); }, ondestroy: function(T) { _this.onDestroy(T); } }, plugins: { //UmbracoContext comes before context menu so that the events fire first UmbracoContext: { fullMenu: _this._opts.jsonFullMenu, onBeforeContext: function(N, T, E) { return _this.onBeforeContext(N, T, E); } }, contextmenu: {} } }; if (this._opts.treeMode != "standard") { options.plugins.checkbox = { three_state: false } } //if there's no service URL, then disable ajax requests if (this._opts.serviceUrl == "" || this._opts.dataUrl == "") { options.data.async = false; options.data.opts.static = {}; } //set global ajax settings: $.ajaxSetup({ contentType: "application/json; charset=utf-8" }); this._debug("_getInitOptions. Async enabled = " + options.data.async); return options; } }; } // instance manager Umbraco.Controls.UmbracoTree.cntr = 0; Umbraco.Controls.UmbracoTree.inst = {}; })(jQuery);