/** * File: LeftAndMain.Tree.js */ (function($) { $.entwine('ss.tree', function($){ $('.cms-tree').entwine({ Hints: null, IsUpdatingTree: false, IsLoaded: false, onadd: function(){ this._super(); // Don't reapply (expensive) tree behaviour if already present if($.isNumeric(this.data('jstree_instance_id'))) return; var hints = this.attr('data-hints'); if(hints) this.setHints($.parseJSON(hints)); /** * @todo Icon and page type hover support * @todo Sorting of sub nodes (originally placed in context menu) * @todo Automatic load of full subtree via ajax on node checkbox selection (minNodeCount = 0) * to avoid doing partial selection with "hidden nodes" (unloaded markup) * @todo Disallow drag'n'drop when node has "noChildren" set (see siteTreeHints) * @todo Disallow moving of pages marked as deleted * most likely by server response codes rather than clientside * @todo "defaultChild" when creating a page (sitetreeHints) * @todo Duplicate page (originally located in context menu) * @todo Update tree node title information and modified state after reordering (response is a JSON array) * * Tasks most likely not required after moving to a standalone tree: * * @todo Context menu - to be replaced by a bezel UI * @todo Refresh form for selected tree node if affected by reordering (new parent relationship) * @todo Cancel current form load via ajax when new load is requested (synchronous loading) */ var self = this; this .jstree(this.getTreeConfig()) .bind('loaded.jstree', function(e, data) { self.setIsLoaded(true); // Add ajax settings after init period to avoid unnecessary initial ajax load // of existing tree in DOM - see load_node_html() data.inst._set_settings({'html_data': {'ajax': { 'url': self.data('urlTree'), 'data': function(node) { var params = self.data('searchparams') || []; // Avoid duplication of parameters params = $.grep(params, function(n, i) {return (n.name != 'ID' && n.name != 'value');}); params.push({name: 'ID', value: $(node).data("id") ? $(node).data("id") : 0}); params.push({name: 'ajax', value: 1}); return params; } }}}); self.updateFromEditForm(); self.css('visibility', 'visible'); // Only show checkboxes with .multiple class data.inst.hide_checkboxes(); }) .bind('before.jstree', function(e, data) { if(data.func == 'start_drag') { // Don't allow drag'n'drop if multi-select is enabled' if(!self.hasClass('draggable') || self.hasClass('multiselect')) { e.stopImmediatePropagation(); return false; } } if($.inArray(data.func, ['check_node', 'uncheck_node'])) { // don't allow check and uncheck if parent is disabled var node = $(data.args[0]).parents('li:first'); var allowedChildren = node.find('li:not(.disabled)'); // if there are child nodes that aren't disabled, allow expanding the tree if(node.hasClass('disabled') && allowedChildren == 0) { e.stopImmediatePropagation(); return false; } } }) .bind('move_node.jstree', function(e, data) { if(self.getIsUpdatingTree()) return; var movedNode = data.rslt.o, newParentNode = data.rslt.np, oldParentNode = data.inst._get_parent(movedNode), newParentID = $(newParentNode).data('id') || 0, nodeID = $(movedNode).data('id'); var siblingIDs = $.map($(movedNode).siblings().andSelf(), function(el) { return $(el).data('id'); }); $.ajax({ 'url': self.data('urlSavetreenode'), 'type': 'POST', 'data': { ID: nodeID, ParentID: newParentID, SiblingIDs: siblingIDs }, success: function() { // We only need to update the ParentID if the current page we're on is the page being moved if ($('.cms-edit-form :input[name=ID]').val() == nodeID) { $('.cms-edit-form :input[name=ParentID]').val(newParentID); } self.updateNodesFromServer([nodeID]); }, statusCode: { 403: function() { $.jstree.rollback(data.rlbk); } } }); }) // Make some jstree events delegatable .bind('select_node.jstree check_node.jstree uncheck_node.jstree', function(e, data) { $(document).triggerHandler(e, data); }); }, onremove: function(){ this.jstree('destroy'); this._super(); }, 'from .cms-container': { onafterstatechange: function(e){ this.updateFromEditForm(); // No need to refresh tree nodes, we assume only form submits cause state changes } }, 'from .cms-container form': { onaftersubmitform: function(e){ var id = $('.cms-edit-form :input[name=ID]').val(); // TODO Trigger by implementing and inspecting "changed records" metadata // sent by form submission response (as HTTP response headers) this.updateNodesFromServer([id]); } }, getTreeConfig: function() { var self = this; return { 'core': { 'initially_open': ['record-0'], 'animation': 0, 'html_titles': true }, 'html_data': { // 'ajax' will be set on 'loaded.jstree' event }, 'ui': { "select_limit" : 1, 'initially_select': [this.find('.current').attr('id')] }, "crrm": { 'move': { // Check if a node is allowed to be moved. // Caution: Runs on every drag over a new node 'check_move': function(data) { var movedNode = $(data.o), newParent = $(data.np), isMovedOntoContainer = data.ot.get_container()[0] == data.np[0], movedNodeClass = movedNode.getClassname(), newParentClass = newParent.getClassname(), // Check allowedChildren of newParent or against root node rules hints = self.getHints(), disallowedChildren = [], hintKey = newParentClass ? newParentClass : 'Root', hint = (hints && typeof hints[hintKey] != 'undefined') ? hints[hintKey] : null; // Special case for VirtualPage: Check that original page type is an allowed child if(hint && movedNode.attr('class').match(/VirtualPage-([^\s]*)/)) movedNodeClass = RegExp.$1; if(hint) disallowedChildren = (typeof hint.disallowedChildren != 'undefined') ? hint.disallowedChildren : []; var isAllowed = ( // Don't allow moving the root node movedNode.data('id') !== 0 // Archived pages can't be moved && !movedNode.hasClass('status-archived') // Only allow moving node inside the root container, not before/after it && (!isMovedOntoContainer || data.p == 'inside') // Children are generally allowed on parent && !newParent.hasClass('nochildren') // movedNode is allowed as a child && (!disallowedChildren.length || $.inArray(movedNodeClass, disallowedChildren) == -1) ); return isAllowed; } } }, 'dnd': { "drop_target" : false, "drag_target" : false }, 'checkbox': { 'two_state': true }, 'themes': { 'theme': 'apple', 'url': $('body').data('frameworkpath') + '/thirdparty/jstree/themes/apple/style.css' }, // Caution: SilverStripe has disabled $.vakata.css.add_sheet() for performance reasons, // which means you need to add any CSS manually to framework/admin/scss/_tree.css 'plugins': [ 'html_data', 'ui', 'dnd', 'crrm', 'themes', 'checkbox' // checkboxes are hidden unless .multiple is set ] }; }, /** * Function: * search * * Parameters: * (Object) data Pass empty data to cancel search * (Function) callback Success callback */ search: function(params, callback) { if(params) this.data('searchparams', params); else this.removeData('searchparams'); this.jstree('refresh', -1, callback); }, /** * Function: getNodeByID * * Parameters: * (Int) id * * Returns * DOMElement */ getNodeByID: function(id) { return this.find('*[data-id='+id+']'); }, /** * Creates a new node from the given HTML. * Wrapping around jstree API because we want the flexibility to define * the node's