mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
- Updates icon, badges, title, and position in hierarchy - New LeftAndMain_TreeNode API to allow rendering of single tree nodes without their hierarchy, extracted from LeftAndMain->getSiteTreeFor() - New LeftAndMain->updatetreenodes() endpoint to request updated state for one or more nodes. Triggered on demand by form refreshes.
This commit is contained in:
parent
36c8fc2e93
commit
120de7cba2
@ -72,6 +72,7 @@ class LeftAndMain extends Controller implements PermissionProvider {
|
||||
'save',
|
||||
'savetreenode',
|
||||
'getsubtree',
|
||||
'updatetreenodes',
|
||||
'printable',
|
||||
'show',
|
||||
'ping',
|
||||
@ -678,16 +679,8 @@ class LeftAndMain extends Controller implements PermissionProvider {
|
||||
$controller = $this;
|
||||
$recordController = ($this->stat('tree_class') == 'SiteTree') ? singleton('CMSPageEditController') : $this;
|
||||
$titleFn = function(&$child) use(&$controller, &$recordController) {
|
||||
$classes = $child->CMSTreeClasses();
|
||||
if($controller->isCurrentPage($child)) $classes .= " current";
|
||||
$flags = $child->hasMethod('getStatusFlags') ? $child->getStatusFlags() : false;
|
||||
if($flags) $classes .= ' ' . implode(' ', array_keys($flags));
|
||||
return "<li id=\"record-$child->ID\" data-id=\"$child->ID\" data-pagetype=\"$child->ClassName\" class=\"" . $classes . "\">" .
|
||||
"<ins class=\"jstree-icon\"> </ins>" .
|
||||
"<a href=\"" . Controller::join_links($recordController->Link("show"), $child->ID) . "\" title=\"" .
|
||||
_t('LeftAndMain.PAGETYPE','Page type: ') .
|
||||
"$child->class\" ><ins class=\"jstree-icon\"> </ins><span class=\"text\">" . ($child->TreeTitle).
|
||||
"</span></a>";
|
||||
$link = Controller::join_links($recordController->Link("show"), $child->ID);
|
||||
return LeftAndMain_TreeNode::create($child, $link, $controller->isCurrentPage($child))->forTemplate();
|
||||
};
|
||||
$html = $obj->getChildrenAsUL(
|
||||
"",
|
||||
@ -740,6 +733,32 @@ class LeftAndMain extends Controller implements PermissionProvider {
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows requesting a view update on specific tree nodes.
|
||||
* Similar to {@link getsubtree()}, but doesn't enforce loading
|
||||
* all children with the node. Useful to refresh views after
|
||||
* state modifications, e.g. saving a form.
|
||||
*
|
||||
* @return String JSON
|
||||
*/
|
||||
public function updatetreenodes($request) {
|
||||
$data = array();
|
||||
$ids = explode(',', $request->getVar('ids'));
|
||||
foreach($ids as $id) {
|
||||
$record = $this->getRecord($id);
|
||||
$recordController = ($this->stat('tree_class') == 'SiteTree') ? singleton('CMSPageEditController') : $this;
|
||||
$link = Controller::join_links($recordController->Link("show"), $record->ID);
|
||||
$html = LeftAndMain_TreeNode::create($record, $link, $this->isCurrentPage($record))->forTemplate() . '</li>';
|
||||
$data[$id] = array(
|
||||
'html' => $html,
|
||||
'ParentID' => $record->ParentID,
|
||||
'Sort' => $record->Sort
|
||||
);
|
||||
}
|
||||
$this->response->addHeader('Content-Type', 'text/json');
|
||||
return Convert::raw2json($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save handler
|
||||
@ -1499,3 +1518,87 @@ class LeftAndMain_HTTPResponse extends SS_HTTPResponse {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around objects being displayed in a tree.
|
||||
* Caution: Volatile API.
|
||||
*
|
||||
* @todo Implement recursive tree node rendering
|
||||
*/
|
||||
class LeftAndMain_TreeNode extends ViewableData {
|
||||
|
||||
/**
|
||||
* @var obj
|
||||
*/
|
||||
protected $obj;
|
||||
|
||||
/**
|
||||
* @var String Edit link to the current record in the CMS
|
||||
*/
|
||||
protected $link;
|
||||
|
||||
/**
|
||||
* @var Bool
|
||||
*/
|
||||
protected $isCurrent;
|
||||
|
||||
function __construct($obj, $link = null, $isCurrent = false) {
|
||||
$this->obj = $obj;
|
||||
$this->link = $link;
|
||||
$this->isCurrent = $isCurrent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns template, for further processing by {@link Hierarchy->getChildrenAsUL()}.
|
||||
* Does not include closing tag to allow this method to inject its own children.
|
||||
*
|
||||
* @todo Remove hardcoded assumptions around returning an <li>, by implementing recursive tree node rendering
|
||||
*
|
||||
* @return String
|
||||
*/
|
||||
function forTemplate() {
|
||||
$obj = $this->obj;
|
||||
return "<li id=\"record-$obj->ID\" data-id=\"$obj->ID\" data-pagetype=\"$obj->ClassName\" class=\"" . $this->getClasses() . "\">" .
|
||||
"<ins class=\"jstree-icon\"> </ins>" .
|
||||
"<a href=\"" . $this->getLink() . "\" title=\"" .
|
||||
_t('LeftAndMain.PAGETYPE','Page type: ') .
|
||||
"$obj->class\" ><ins class=\"jstree-icon\"> </ins><span class=\"text\">" . ($obj->TreeTitle).
|
||||
"</span></a>";
|
||||
}
|
||||
|
||||
function getClasses() {
|
||||
$classes = $this->obj->CMSTreeClasses();
|
||||
if($this->isCurrent) $classes .= " current";
|
||||
$flags = $this->obj->hasMethod('getStatusFlags') ? $this->obj->getStatusFlags() : false;
|
||||
if($flags) $classes .= ' ' . implode(' ', array_keys($flags));
|
||||
return $classes;
|
||||
}
|
||||
|
||||
function getObj() {
|
||||
return $this->obj;
|
||||
}
|
||||
|
||||
function setObj($obj) {
|
||||
$this->obj = $obj;
|
||||
return $this;
|
||||
}
|
||||
|
||||
function getLink() {
|
||||
return $this->link;
|
||||
}
|
||||
|
||||
function setLink($link) {
|
||||
$this->link = $link;
|
||||
return $this;
|
||||
}
|
||||
|
||||
function getIsCurrent() {
|
||||
return $this->isCurrent;
|
||||
}
|
||||
|
||||
function setIsCurrent($bool) {
|
||||
$this->isCurrent = $bool;
|
||||
return $this;
|
||||
}
|
||||
|
||||
}
|
@ -10,6 +10,10 @@
|
||||
|
||||
Hints: null,
|
||||
|
||||
IsUpdatingTree: false,
|
||||
|
||||
IsLoaded: false,
|
||||
|
||||
onadd: function(){
|
||||
this._super();
|
||||
|
||||
@ -22,7 +26,6 @@
|
||||
/**
|
||||
* @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)
|
||||
@ -37,13 +40,12 @@
|
||||
* @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(this.getTreeConfig())
|
||||
.bind('loaded.jstree', function(e, data) {
|
||||
self.setIsLoaded(true);
|
||||
self.updateFromEditForm();
|
||||
self.css('visibility', 'visible');
|
||||
// Add ajax settings after init period to avoid unnecessary initial ajax load
|
||||
@ -82,6 +84,8 @@
|
||||
}
|
||||
})
|
||||
.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);
|
||||
var siblingIDs = $.map($(movedNode).siblings().andSelf(), function(el) {
|
||||
return $(el).data('id');
|
||||
@ -104,7 +108,7 @@
|
||||
// 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');
|
||||
@ -113,11 +117,17 @@
|
||||
|
||||
'from .cms-container': {
|
||||
onafterstatechange: function(e){
|
||||
this.updateFromEditForm(e.origData);
|
||||
},
|
||||
this.updateFromEditForm();
|
||||
// No need to refresh tree nodes, we assume only form submits cause state changes
|
||||
}
|
||||
},
|
||||
|
||||
'from .cms-container form': {
|
||||
onaftersubmitform: function(e){
|
||||
this.updateFromEditForm(e.origData);
|
||||
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]);
|
||||
}
|
||||
},
|
||||
|
||||
@ -216,87 +226,155 @@
|
||||
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 <li> ourselves. Places the node in the tree
|
||||
* according to data.ParentID
|
||||
*
|
||||
* Parameters:
|
||||
* (String) HTML New node content (<li>)
|
||||
* (Object) Map of additional data, e.g. ParentID
|
||||
* (Function) Success callback
|
||||
*/
|
||||
createNode: function(html, data, callback) {
|
||||
var self = this,
|
||||
parentNode = data.ParentID ? self.find('li[data-id='+data.ParentID+']') : false,
|
||||
newNode = $(html);
|
||||
|
||||
this.jstree(
|
||||
'create_node',
|
||||
parentNode.length ? parentNode : -1,
|
||||
'last',
|
||||
'',
|
||||
function(node) {
|
||||
var origClasses = node.attr('class');
|
||||
// Copy attributes
|
||||
for(var i=0; i<newNode[0].attributes.length; i++){
|
||||
var attr = newNode[0].attributes[i];
|
||||
node.attr(attr.name, attr.value);
|
||||
}
|
||||
node.addClass(origClasses).html(newNode.html());
|
||||
callback(node);
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates a node's state in the tree,
|
||||
* including all of its HTML, as well as its position.
|
||||
*
|
||||
* Parameters:
|
||||
* (DOMElement) Existing node
|
||||
* (String) HTML New node content (<li>)
|
||||
* (Object) Map of additional data, e.g. ParentID
|
||||
*/
|
||||
updateNode: function(node, html, data) {
|
||||
var self = this, newNode = $(html), origClasses = node.attr('class'),
|
||||
parentNode = data.ParentID ? this.find('li[data-id='+data.ParentID+']') : false;
|
||||
|
||||
// Copy attributes. We can't replace the node completely
|
||||
// without removing or detaching its children nodes.
|
||||
for(var i=0; i<newNode[0].attributes.length; i++){
|
||||
var attr = newNode[0].attributes[i];
|
||||
node.attr(attr.name, attr.value);
|
||||
}
|
||||
|
||||
// Replace inner content
|
||||
node.addClass(origClasses).html(newNode.html());
|
||||
|
||||
// Set correct parent
|
||||
this.jstree('move_node', node, parentNode.length ? parentNode : -1, data.Sort);
|
||||
},
|
||||
|
||||
/**
|
||||
* Assumes to be triggered by a form element with the following input fields:
|
||||
* ID, ParentID, TreeTitle (or Title), ClassName.
|
||||
*
|
||||
* @todo Serverside node refresh, see http://open.silverstripe.org/ticket/7450
|
||||
* Sets the current state based on the form the tree is managing.
|
||||
*/
|
||||
updateFromEditForm: function(origData) {
|
||||
var self = this,
|
||||
form = $('.cms-edit-form').get(0),
|
||||
id = form ? $(form.ID).val() : null,
|
||||
urlEditPage = this.data('urlEditpage');
|
||||
|
||||
// check if a form with a valid ID exists
|
||||
updateFromEditForm: function() {
|
||||
var node, id = $('.cms-edit-form :input[name=ID]').val();
|
||||
if(id) {
|
||||
var parentID = $(form.ParentID).val(),
|
||||
parentNode = this.find('li[data-id='+parentID+']');
|
||||
node = this.find('li[data-id='+id+']'),
|
||||
title = $((form.TreeTitle) ? form.TreeTitle : form.Title).val(),
|
||||
className = $(form.ClassName).val();
|
||||
|
||||
// set title (either from TreeTitle or from Title fields)
|
||||
// Treetitle has special HTML formatting to denote the status changes.
|
||||
// only update immediate text element, we don't want to update all the nested ones
|
||||
if(title) node.find('.text:first').html(title);
|
||||
|
||||
// Collect flag classes and also apply to parent
|
||||
var statusFlags = [];
|
||||
node.children('a').find('.badge').each(function() {
|
||||
statusFlags = statusFlags.concat($(this).attr('class').replace('badge', '').split(' '));
|
||||
});
|
||||
// TODO Doesn't remove classes, gets too complex: Best handled through complete serverside replacement
|
||||
node.addClass(statusFlags.join(' '));
|
||||
|
||||
// check if node exists, might have been created instead
|
||||
if(!node.length && urlEditPage) {
|
||||
this.jstree(
|
||||
'create_node',
|
||||
parentNode,
|
||||
'inside',
|
||||
{
|
||||
data: '',
|
||||
attr: {
|
||||
'data-class': className,
|
||||
'class': 'class-' + className,
|
||||
'data-id': id
|
||||
}
|
||||
},
|
||||
function() {
|
||||
var newNode = self.find('li[data-id='+id+']');
|
||||
// TODO Fix replacement of jstree-icon inside <a> tag
|
||||
newNode.find('a:first').html(title).attr('href', ss.i18n.sprintf(
|
||||
urlEditPage, id
|
||||
));
|
||||
self.jstree('deselect_all');
|
||||
self.jstree('select_node', newNode);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
node = this.getNodeByID(id);
|
||||
if(node.length) {
|
||||
// set correct parent (only if it has changed)
|
||||
if(parentID && parentID != node.parents('li:first').data('id')) {
|
||||
this.jstree('move_node', node, parentNode.length ? parentNode : -1, 'last');
|
||||
}
|
||||
|
||||
// Only single selection is supported on initial load
|
||||
this.jstree('deselect_all');
|
||||
this.jstree('select_node', node);
|
||||
} else {
|
||||
// If form is showing an ID that doesn't exist in the tree,
|
||||
// get it from the server
|
||||
this.updateNodesFromServer([id]);
|
||||
}
|
||||
} else {
|
||||
// If no ID exists in a form view, we're displaying the tree on its own,
|
||||
// hence to page should show as active
|
||||
this.jstree('deselect_all');
|
||||
|
||||
if(typeof origData != 'undefined') {
|
||||
var node = this.find('li[data-id='+origData.ID+']');
|
||||
if(node && node.data('id') !== 0) this.jstree('delete_node', node);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Reloads the view of one or more tree nodes
|
||||
* from the server, ensuring that their state is up to date
|
||||
* (icon, title, hierarchy, badges, etc).
|
||||
* This is easier, more consistent and more extensible
|
||||
* than trying to correct all aspects via DOM modifications,
|
||||
* based on the sparse data available in the current edit form.
|
||||
*
|
||||
* Parameters:
|
||||
* (Array) List of IDs to retrieve
|
||||
*/
|
||||
updateNodesFromServer: function(ids) {
|
||||
if(this.getIsUpdatingTree() || !this.getIsLoaded()) return;
|
||||
|
||||
var self = this, includesNewNode = false;
|
||||
this.setIsUpdatingTree(true);
|
||||
|
||||
// TODO 'initially_opened' config doesn't apply here
|
||||
self.jstree('open_node', this.getNodeByID(0));
|
||||
self.jstree('save_opened');
|
||||
self.jstree('save_selected');
|
||||
|
||||
$.ajax({
|
||||
url: this.data('urlUpdatetreenodes') + '?ids=' + ids.join(','),
|
||||
dataType: 'json',
|
||||
success: function(data, xhr) {
|
||||
$.each(data, function(nodeId, nodeData) {
|
||||
var node = self.getNodeByID(nodeId);
|
||||
|
||||
// If no node data is given, assume the node has been removed
|
||||
if(!nodeData) {
|
||||
self.jstree('delete_node', node);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if node exists, create if necessary
|
||||
if(node.length) {
|
||||
self.updateNode(node, nodeData.html, nodeData);
|
||||
setTimeout(function() {
|
||||
self.jstree('deselect_all');
|
||||
self.jstree('select_node', node);
|
||||
// Manually correct state, which checks for children and
|
||||
// removes toggle arrow (should really be done by jstree internally)
|
||||
self.jstree('correct_state', node);
|
||||
}, 500);
|
||||
} else {
|
||||
includesNewNode = true;
|
||||
self.createNode(nodeData.html, nodeData, function(newNode) {
|
||||
self.jstree('deselect_all');
|
||||
self.jstree('select_node', newNode);
|
||||
// Manually remove toggle node, see above
|
||||
self.jstree('correct_state', newNode);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if(!includesNewNode) {
|
||||
self.jstree('deselect_all');
|
||||
self.jstree('reselect');
|
||||
self.jstree('reopen');
|
||||
}
|
||||
},
|
||||
complete: function() {
|
||||
self.setIsUpdatingTree(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user