/** * File: LeftAndMain.Tree.js */ (function($) { $.entwine('ss', function($){ $('.cms-tree').entwine({ Hints: null, onmatch: function() { this._super(); // Don't reapply (expensive) tree behaviour if already present if(!$.isNaN(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 Refresh after language <select> change (with Translatable enabled) * @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) * @todo When new edit form is loaded, automatically: Select matching node, set correct parent, * update icon and title */ var self = this; this .jstree({ '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 = (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 // 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': 'sapphire/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 sapphire/admin/scss/_tree.css 'plugins': [ 'html_data', 'ui', 'dnd', 'crrm', 'themes', 'checkbox' // checkboxes are hidden unless .multiple is set ] }) .bind('loaded.jstree', function(e, data) { self.css('visibility', 'visible'); // 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; } }}}); // 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'); if(node.hasClass('disabled')) { e.stopImmediatePropagation(); return false; } } }) .bind('move_node.jstree', function(e, data) { var movedNode = data.rslt.o, newParentNode = data.rslt.np, oldParentNode = data.inst._get_parent(movedNode); var siblingIDs = $.map($(movedNode).siblings().andSelf(), function(el) { return $(el).data('id'); }); $.ajax({ 'url': self.data('urlSavetreenode'), 'data': { ID: $(movedNode).data('id'), ParentID: $(newParentNode).data('id') || 0, SiblingIDs: siblingIDs } }); }); this.parents('.cms-content:first').bind('reloadeditform', function(e, data) { self._onLoadNewPage(e, data); }); }, /** * 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+']'); }, /** * Assumes to be triggered by a form element with the following input fields: * ID, ParentID, TreeTitle (or Title), ClassName */ _onLoadNewPage: function(e, eventData) { var self = this; // finds a certain value in an array generated by jQuery.serializeArray() var findInSerializedArray = function(arr, name) { for(var i=0; i<arr.length; i++) { if(arr[i].name == name) return arr[i].value; }; return false; }; var handledform = $(e.target).children('.cms-edit-form:first')[0]; var id = $(handledform.ID).val(); // check if a form with a valid ID exists if(id) { var parentID = $(handledform.ParentID).val(), parentNode = this.find('li[data-id='+parentID+']'); node = this.find('li[data-id='+id+']'), title = $((handledform.TreeTitle) ? handledform.TreeTitle : handledform.Title).val(), className = $(handledform.ClassName).val(); // set title (either from TreeTitle or from Title fields) // Treetitle has special HTML formatting to denote the status changes. if(title) this.jstree('rename_node', node, title); // TODO Fix node icon setting // // update icon (only if it has changed) // if(className) this.setNodeIcon(id, className); // check if node exists, might have been created instead if(!node.length) { this.jstree( 'create_node', parentNode, 'inside', {data: '', attr: {'class': className, 'data-id': id}}, function() { var newNode = self.find('li[data-id='+id+']'); // TODO Fix hardcoded link // TODO Fix replacement of jstree-icon inside <a> tag newNode.find('a:first').html(title).attr('href', 'admin/show/'+id); self.jstree('deselect_node', parentNode); self.jstree('select_node', newNode); } ); // set current tree element this.jstree('select_node', node); } // TODO Fix node parent setting // // set correct parent (only if it has changed) // if(parentID) this.setNodeParentID(id, jQuery(e.target.ParentID).val()); // TODO Fix doubleup when replacing page form with root form, reloads the old form over the root // set current tree element regardless of wether the item was new // this.jstree('select_node', node); } else { if(typeof eventData.origData != 'undefined') { var node = this.find('li[data-id='+eventData.origData.ID+']'); if(node && node.data('id') != 0) this.jstree('delete_node', node); } } }, onunmatch: function() { } }); $('.cms-tree.multiple').entwine({ onmatch: function() { this._super(); this.jstree('show_checkboxes'); }, onunmatch: function() { this._super(); this.jstree('uncheck_all'); this.jstree('hide_checkboxes'); }, /** * Function: getSelectedIDs * * Returns: * (Array) */ getSelectedIDs: function() { return $.map($(this).jstree('get_checked'), function(el, i) {return $(el).data('id');}); } }); $('.cms-tree li').entwine({ /** * Function: setEnabled * * Parameters: * (bool) */ setEnabled: function(bool) { this.toggleClass('disabled', !(bool)); }, /** * Function: getClassname * * Returns PHP class for this element. Useful to check business rules like valid drag'n'drop targets. */ getClassname: function() { var matches = this.attr('class').match(/class-([^\s]*)/i); return matches ? matches[1] : ''; }, /** * Function: getID * * Returns: * (Number) */ getID: function() { return this.data('id'); } }); $('.cms-tree-view-modes input.view-mode').entwine({ onmatch: function() { // set active by default this.trigger('click'); this._super(); }, onclick: function(e) { $('.cms-tree') .toggleClass('draggable', $(e.target).val() == 'draggable') .toggleClass('multiple', $(e.target).val() == 'multiselect'); } }); }); }(jQuery));