Skip to content
This repository
Browse code

NEW Tree node updates after save (fixes #7450, #7389)

- 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.
commit 120de7cba2e1c45bf5f1e753d655c27c2cae1784 1 parent 36c8fc2
Ingo Schommer authored July 17, 2012 hafriedlander committed July 23, 2012
123  admin/code/LeftAndMain.php
@@ -72,6 +72,7 @@ class LeftAndMain extends Controller implements PermissionProvider {
72 72
 		'save',
73 73
 		'savetreenode',
74 74
 		'getsubtree',
  75
+		'updatetreenodes',
75 76
 		'printable',
76 77
 		'show',
77 78
 		'ping',
@@ -678,16 +679,8 @@ function getSiteTreeFor($className, $rootID = null, $childrenMethod = null, $num
678 679
 		$controller = $this;
679 680
 		$recordController = ($this->stat('tree_class') == 'SiteTree') ?  singleton('CMSPageEditController') : $this;
680 681
 		$titleFn = function(&$child) use(&$controller, &$recordController) {
681  
-			$classes = $child->CMSTreeClasses();
682  
-			if($controller->isCurrentPage($child)) $classes .= " current";
683  
-			$flags = $child->hasMethod('getStatusFlags') ? $child->getStatusFlags() : false;
684  
-			if($flags) $classes .= ' ' . implode(' ', array_keys($flags));
685  
-			return "<li id=\"record-$child->ID\" data-id=\"$child->ID\" data-pagetype=\"$child->ClassName\" class=\"" . $classes . "\">" .
686  
-				"<ins class=\"jstree-icon\">&nbsp;</ins>" .
687  
-				"<a href=\"" . Controller::join_links($recordController->Link("show"), $child->ID) . "\" title=\"" .
688  
-				_t('LeftAndMain.PAGETYPE','Page type: ') .
689  
-				"$child->class\" ><ins class=\"jstree-icon\">&nbsp;</ins><span class=\"text\">" . ($child->TreeTitle).
690  
-				"</span></a>";
  682
+			$link = Controller::join_links($recordController->Link("show"), $child->ID);
  683
+			return LeftAndMain_TreeNode::create($child, $link, $controller->isCurrentPage($child))->forTemplate();
691 684
 		};
692 685
 		$html = $obj->getChildrenAsUL(
693 686
 			"",
@@ -740,6 +733,32 @@ public function getsubtree($request) {
740 733
 		
741 734
 		return $html;
742 735
 	}
  736
+
  737
+	/**
  738
+	 * Allows requesting a view update on specific tree nodes.
  739
+	 * Similar to {@link getsubtree()}, but doesn't enforce loading
  740
+	 * all children with the node. Useful to refresh views after
  741
+	 * state modifications, e.g. saving a form.
  742
+	 * 
  743
+	 * @return String JSON
  744
+	 */
  745
+	public function updatetreenodes($request) {
  746
+		$data = array();
  747
+		$ids = explode(',', $request->getVar('ids'));
  748
+		foreach($ids as $id) {
  749
+			$record = $this->getRecord($id);
  750
+			$recordController = ($this->stat('tree_class') == 'SiteTree') ?  singleton('CMSPageEditController') : $this;
  751
+			$link = Controller::join_links($recordController->Link("show"), $record->ID);
  752
+			$html = LeftAndMain_TreeNode::create($record, $link, $this->isCurrentPage($record))->forTemplate() . '</li>';
  753
+			$data[$id] = array(
  754
+				'html' => $html, 
  755
+				'ParentID' => $record->ParentID,
  756
+				'Sort' => $record->Sort
  757
+			);
  758
+		}
  759
+		$this->response->addHeader('Content-Type', 'text/json');
  760
+		return Convert::raw2json($data);
  761
+	}
743 762
 	
744 763
 	/**
745 764
 	 * Save  handler
@@ -1499,3 +1518,87 @@ function setIsFinished($bool) {
1499 1518
 	}
1500 1519
 
1501 1520
 }
  1521
+
  1522
+/**
  1523
+ * Wrapper around objects being displayed in a tree.
  1524
+ * Caution: Volatile API.
  1525
+ *
  1526
+ * @todo Implement recursive tree node rendering
  1527
+ */
  1528
+class LeftAndMain_TreeNode extends ViewableData {
  1529
+	
  1530
+	/**
  1531
+	 * @var obj
  1532
+	 */
  1533
+	protected $obj;
  1534
+
  1535
+	/**
  1536
+	 * @var String Edit link to the current record in the CMS
  1537
+	 */
  1538
+	protected $link;
  1539
+
  1540
+	/**
  1541
+	 * @var Bool
  1542
+	 */
  1543
+	protected $isCurrent;
  1544
+
  1545
+	function __construct($obj, $link = null, $isCurrent = false) {
  1546
+		$this->obj = $obj;
  1547
+		$this->link = $link;
  1548
+		$this->isCurrent = $isCurrent;
  1549
+	}
  1550
+
  1551
+	/**
  1552
+	 * Returns template, for further processing by {@link Hierarchy->getChildrenAsUL()}.
  1553
+	 * Does not include closing tag to allow this method to inject its own children.
  1554
+	 *
  1555
+	 * @todo Remove hardcoded assumptions around returning an <li>, by implementing recursive tree node rendering
  1556
+	 * 
  1557
+	 * @return String
  1558
+	 */
  1559
+	function forTemplate() {
  1560
+		$obj = $this->obj;
  1561
+		return "<li id=\"record-$obj->ID\" data-id=\"$obj->ID\" data-pagetype=\"$obj->ClassName\" class=\"" . $this->getClasses() . "\">" .
  1562
+			"<ins class=\"jstree-icon\">&nbsp;</ins>" .
  1563
+			"<a href=\"" . $this->getLink() . "\" title=\"" .
  1564
+			_t('LeftAndMain.PAGETYPE','Page type: ') .
  1565
+			"$obj->class\" ><ins class=\"jstree-icon\">&nbsp;</ins><span class=\"text\">" . ($obj->TreeTitle). 
  1566
+			"</span></a>";
  1567
+	}
  1568
+
  1569
+	function getClasses() {
  1570
+		$classes = $this->obj->CMSTreeClasses();
  1571
+		if($this->isCurrent) $classes .= " current";
  1572
+		$flags = $this->obj->hasMethod('getStatusFlags') ? $this->obj->getStatusFlags() : false;
  1573
+		if($flags) $classes .= ' ' . implode(' ', array_keys($flags));
  1574
+		return $classes;
  1575
+	}
  1576
+
  1577
+	function getObj() {
  1578
+		return $this->obj;
  1579
+	}
  1580
+
  1581
+	function setObj($obj) {
  1582
+		$this->obj = $obj;
  1583
+		return $this;
  1584
+	}
  1585
+
  1586
+	function getLink() {
  1587
+		return $this->link;
  1588
+	}
  1589
+
  1590
+	function setLink($link) {
  1591
+		$this->link = $link;
  1592
+		return $this;
  1593
+	}
  1594
+
  1595
+	function getIsCurrent() {
  1596
+		return $this->isCurrent;
  1597
+	}
  1598
+
  1599
+	function setIsCurrent($bool) {
  1600
+		$this->isCurrent = $bool;
  1601
+		return $this;
  1602
+	}
  1603
+
  1604
+}
220  admin/javascript/LeftAndMain.Tree.js
@@ -10,6 +10,10 @@
10 10
 			
11 11
 			Hints: null,
12 12
 
  13
+			IsUpdatingTree: false,
  14
+
  15
+			IsLoaded: false,
  16
+
13 17
 			onadd: function(){
14 18
 				this._super();
15 19
 
@@ -22,7 +26,6 @@
22 26
 				/**
23 27
 				 * @todo Icon and page type hover support
24 28
 				 * @todo Sorting of sub nodes (originally placed in context menu)
25  
-				 * @todo Refresh after language <select> change (with Translatable enabled)
26 29
 				 * @todo Automatic load of full subtree via ajax on node checkbox selection (minNodeCount = 0)
27 30
 				 *  to avoid doing partial selection with "hidden nodes" (unloaded markup)
28 31
 				 * @todo Disallow drag'n'drop when node has "noChildren" set (see siteTreeHints)
@@ -37,13 +40,12 @@
37 40
 				 * @todo Context menu - to be replaced by a bezel UI
38 41
 				 * @todo Refresh form for selected tree node if affected by reordering (new parent relationship)
39 42
 				 * @todo Cancel current form load via ajax when new load is requested (synchronous loading)
40  
-				 * @todo When new edit form is loaded, automatically: Select matching node, set correct parent,
41  
-				 *  update icon and title
42 43
 				 */
43 44
 				var self = this;
44 45
 					this
45 46
 						.jstree(this.getTreeConfig())
46 47
 						.bind('loaded.jstree', function(e, data) {
  48
+							self.setIsLoaded(true);
47 49
 							self.updateFromEditForm();
48 50
 							self.css('visibility', 'visible');
49 51
 							// Add ajax settings after init period to avoid unnecessary initial ajax load
@@ -82,6 +84,8 @@
82 84
 							}
83 85
 						})
84 86
 						.bind('move_node.jstree', function(e, data) {
  87
+							if(self.getIsUpdatingTree()) return;
  88
+
85 89
 							var movedNode = data.rslt.o, newParentNode = data.rslt.np, oldParentNode = data.inst._get_parent(movedNode);
86 90
 							var siblingIDs = $.map($(movedNode).siblings().andSelf(), function(el) {
87 91
 								return $(el).data('id');
@@ -104,7 +108,7 @@
104 108
 						// Make some jstree events delegatable
105 109
 						.bind('select_node.jstree check_node.jstree uncheck_node.jstree', function(e, data) {
106 110
 							$(document).triggerHandler(e, data);
107  
-						})
  111
+						});
108 112
 			},
109 113
 			onremove: function(){
110 114
 				this.jstree('destroy');
@@ -113,11 +117,17 @@
113 117
 
114 118
 			'from .cms-container': {
115 119
 				onafterstatechange: function(e){
116  
-					this.updateFromEditForm(e.origData);
117  
-				},
  120
+					this.updateFromEditForm();
  121
+					// No need to refresh tree nodes, we assume only form submits cause state changes
  122
+				}
  123
+			},
118 124
 
  125
+			'from .cms-container form': {
119 126
 				onaftersubmitform: function(e){
120  
-					this.updateFromEditForm(e.origData);
  127
+					var id = $('.cms-edit-form :input[name=ID]').val();
  128
+					// TODO Trigger by implementing and inspecting "changed records" metadata 
  129
+					// sent by form submission response (as HTTP response headers)
  130
+					this.updateNodesFromServer([id]);
121 131
 				}
122 132
 			},
123 133
 
@@ -216,87 +226,155 @@
216 226
 			getNodeByID: function(id) {
217 227
 				return this.find('*[data-id='+id+']');
218 228
 			},
219  
-			
  229
+
220 230
 			/**
221  
-			 * Assumes to be triggered by a form element with the following input fields:
222  
-			 * ID, ParentID, TreeTitle (or Title), ClassName.
  231
+			 * Creates a new node from the given HTML.
  232
+			 * Wrapping around jstree API because we want the flexibility to define
  233
+			 * the node's <li> ourselves. Places the node in the tree
  234
+			 * according to data.ParentID
223 235
 			 * 
224  
-			 * @todo Serverside node refresh, see http://open.silverstripe.org/ticket/7450
  236
+			 * Parameters:
  237
+			 *  (String) HTML New node content (<li>)
  238
+			 *  (Object) Map of additional data, e.g. ParentID
  239
+			 *  (Function) Success callback
225 240
 			 */
226  
-			updateFromEditForm: function(origData) {
  241
+			createNode: function(html, data, callback) {
227 242
 				var self = this, 
228  
-					form = $('.cms-edit-form').get(0),
229  
-					id = form ? $(form.ID).val() : null,
230  
-					urlEditPage = this.data('urlEditpage');
231  
-
232  
-				// check if a form with a valid ID exists
233  
-				if(id) {
234  
-					var parentID = $(form.ParentID).val(), 
235  
-						parentNode = this.find('li[data-id='+parentID+']');
236  
-						node = this.find('li[data-id='+id+']'),
237  
-						title = $((form.TreeTitle) ? form.TreeTitle : form.Title).val(),
238  
-						className = $(form.ClassName).val();
  243
+					parentNode = data.ParentID ? self.find('li[data-id='+data.ParentID+']') : false,
  244
+					newNode = $(html);
  245
+				
  246
+				this.jstree(
  247
+					'create_node', 
  248
+					parentNode.length ? parentNode : -1, 
  249
+					'last', 
  250
+					'',
  251
+					function(node) {
  252
+						var origClasses = node.attr('class');
  253
+						// Copy attributes
  254
+						for(var i=0; i<newNode[0].attributes.length; i++){
  255
+							var attr = newNode[0].attributes[i];
  256
+							node.attr(attr.name, attr.value);
  257
+						}
  258
+						node.addClass(origClasses).html(newNode.html());
  259
+						callback(node);
  260
+					}
  261
+				);
  262
+			},
239 263
 
240  
-					// set title (either from TreeTitle or from Title fields)
241  
-					// Treetitle has special HTML formatting to denote the status changes.
242  
-					// only update immediate text element, we don't want to update all the nested ones
243  
-					if(title) node.find('.text:first').html(title);
  264
+			/**
  265
+			 * Updates a node's state in the tree,
  266
+			 * including all of its HTML, as well as its position.
  267
+			 * 
  268
+			 * Parameters:
  269
+			 *  (DOMElement) Existing node
  270
+			 *  (String) HTML New node content (<li>)
  271
+			 *  (Object) Map of additional data, e.g. ParentID
  272
+			 */
  273
+			updateNode: function(node, html, data) {
  274
+				var self = this, newNode = $(html), origClasses = node.attr('class'),
  275
+					parentNode = data.ParentID ? this.find('li[data-id='+data.ParentID+']') : false;
244 276
 
245  
-					// Collect flag classes and also apply to parent
246  
-					var statusFlags = [];
247  
-					node.children('a').find('.badge').each(function() {
248  
-						statusFlags = statusFlags.concat($(this).attr('class').replace('badge', '').split(' '));
249  
-					});
250  
-					// TODO Doesn't remove classes, gets too complex: Best handled through complete serverside replacement
251  
-					node.addClass(statusFlags.join(' ')); 
  277
+				// Copy attributes. We can't replace the node completely
  278
+				// without removing or detaching its children nodes.
  279
+				for(var i=0; i<newNode[0].attributes.length; i++){
  280
+					var attr = newNode[0].attributes[i];
  281
+					node.attr(attr.name, attr.value);
  282
+				}
252 283
 
253  
-					// check if node exists, might have been created instead
254  
-					if(!node.length && urlEditPage) {
255  
-						this.jstree(
256  
-							'create_node', 
257  
-							parentNode, 
258  
-							'inside', 
259  
-							{
260  
-								data: '', 
261  
-								attr: {
262  
-									'data-class': className, 
263  
-									'class': 'class-' + className, 
264  
-									'data-id': id
265  
-								}
266  
-							},
267  
-							function() {
268  
-								var newNode = self.find('li[data-id='+id+']');
269  
-								// TODO Fix replacement of jstree-icon inside <a> tag
270  
-								newNode.find('a:first').html(title).attr('href', ss.i18n.sprintf(
271  
-									urlEditPage, id
272  
-								));
273  
-								self.jstree('deselect_all');
274  
-								self.jstree('select_node', newNode);
275  
-							}
276  
-						);
277  
-					}
  284
+				// Replace inner content
  285
+				node.addClass(origClasses).html(newNode.html());
278 286
 
  287
+				// Set correct parent
  288
+				this.jstree('move_node', node, parentNode.length ? parentNode : -1, data.Sort);
  289
+			},
  290
+			
  291
+			/**
  292
+			 * Sets the current state based on the form the tree is managing.
  293
+			 */
  294
+			updateFromEditForm: function() {
  295
+				var node, id = $('.cms-edit-form :input[name=ID]').val();
  296
+				if(id) {
  297
+					node = this.getNodeByID(id);
279 298
 					if(node.length) {
280  
-						// set correct parent (only if it has changed)
281  
-						if(parentID && parentID != node.parents('li:first').data('id')) {
282  
-							this.jstree('move_node', node, parentNode.length ? parentNode : -1, 'last');
283  
-						}
284  
-
285  
-						// Only single selection is supported on initial load
286  
-						this.jstree('deselect_all');
287 299
 						this.jstree('select_node', node);
  300
+					} else {
  301
+						// If form is showing an ID that doesn't exist in the tree,
  302
+						// get it from the server
  303
+						this.updateNodesFromServer([id]);
288 304
 					}
289 305
 				} else {
290 306
 					// If no ID exists in a form view, we're displaying the tree on its own,
291 307
 					// hence to page should show as active
292 308
 					this.jstree('deselect_all');
293  
-
294  
-					if(typeof origData != 'undefined') {
295  
-						var node = this.find('li[data-id='+origData.ID+']');
296  
-						if(node && node.data('id') !== 0) this.jstree('delete_node', node);
297  
-					}
298 309
 				}
  310
+			},
299 311
 
  312
+			/**
  313
+			 * Reloads the view of one or more tree nodes
  314
+			 * from the server, ensuring that their state is up to date
  315
+			 * (icon, title, hierarchy, badges, etc).
  316
+			 * This is easier, more consistent and more extensible 
  317
+			 * than trying to correct all aspects via DOM modifications, 
  318
+			 * based on the sparse data available in the current edit form.
  319
+			 *
  320
+			 * Parameters:
  321
+			 *  (Array) List of IDs to retrieve
  322
+			 */
  323
+			updateNodesFromServer: function(ids) {
  324
+				if(this.getIsUpdatingTree() || !this.getIsLoaded()) return;
  325
+
  326
+				var self = this, includesNewNode = false;
  327
+				this.setIsUpdatingTree(true);
  328
+
  329
+				// TODO 'initially_opened' config doesn't apply here
  330
+				self.jstree('open_node', this.getNodeByID(0));
  331
+				self.jstree('save_opened');
  332
+				self.jstree('save_selected');
  333
+
  334
+				$.ajax({
  335
+					url: this.data('urlUpdatetreenodes') + '?ids=' + ids.join(','),
  336
+					dataType: 'json',
  337
+					success: function(data, xhr) {
  338
+						$.each(data, function(nodeId, nodeData) {
  339
+							var node = self.getNodeByID(nodeId);
  340
+
  341
+							// If no node data is given, assume the node has been removed
  342
+							if(!nodeData) {
  343
+								self.jstree('delete_node', node);
  344
+								return;
  345
+							}
  346
+
  347
+							// Check if node exists, create if necessary
  348
+							if(node.length) {
  349
+								self.updateNode(node, nodeData.html, nodeData);
  350
+								setTimeout(function() {
  351
+									self.jstree('deselect_all');
  352
+									self.jstree('select_node', node);
  353
+									// Manually correct state, which checks for children and
  354
+									// removes toggle arrow (should really be done by jstree internally)
  355
+									self.jstree('correct_state', node);	
  356
+								}, 500);
  357
+							} else {
  358
+								includesNewNode = true;
  359
+								self.createNode(nodeData.html, nodeData, function(newNode) {
  360
+									self.jstree('deselect_all');
  361
+									self.jstree('select_node', newNode);
  362
+									// Manually remove toggle node, see above
  363
+									self.jstree('correct_state', newNode);
  364
+								});
  365
+							}
  366
+						});
  367
+
  368
+						if(!includesNewNode) {
  369
+							self.jstree('deselect_all');
  370
+							self.jstree('reselect');
  371
+							self.jstree('reopen');
  372
+						}
  373
+					},
  374
+					complete: function() {
  375
+						self.setIsUpdatingTree(false);
  376
+					}
  377
+				});				
300 378
 			}
301 379
 
302 380
 		});

0 notes on commit 120de7c

Please sign in to comment.
Something went wrong with that request. Please try again.