001// Copyright 2011-2013 The Apache Software Foundation 002// 003// Licensed under the Apache License, Version 2.0 (the "License"); 004// you may not use this file except in compliance with the License. 005// You may obtain a copy of the License at 006// 007// http://www.apache.org/licenses/LICENSE-2.0 008// 009// Unless required by applicable law or agreed to in writing, software 010// distributed under the License is distributed on an "AS IS" BASIS, 011// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 012// See the License for the specific language governing permissions and 013// limitations under the License. 014 015package org.apache.tapestry5.corelib.components; 016 017import org.apache.tapestry5.*; 018import org.apache.tapestry5.annotations.*; 019import org.apache.tapestry5.dom.Element; 020import org.apache.tapestry5.func.F; 021import org.apache.tapestry5.func.Flow; 022import org.apache.tapestry5.func.Worker; 023import org.apache.tapestry5.internal.util.CaptureResultCallback; 024import org.apache.tapestry5.ioc.annotations.Inject; 025import org.apache.tapestry5.json.JSONObject; 026import org.apache.tapestry5.runtime.RenderCommand; 027import org.apache.tapestry5.runtime.RenderQueue; 028import org.apache.tapestry5.services.javascript.JavaScriptSupport; 029import org.apache.tapestry5.tree.*; 030 031import java.util.List; 032 033/** 034 * A component used to render a recursive tree structure, with expandable/collapsable/selectable nodes. The data that is displayed 035 * by the component is provided as a {@link TreeModel}. A secondary model, the {@link TreeExpansionModel}, is used 036 * to track which nodes have been expanded. The optional {@link TreeSelectionModel} is used to track node selections (as currently 037 * implemented, only leaf nodes may be selected). 038 * <p/> 039 * Tree is <em>not</em> a form control component; all changes made to the tree on the client 040 * (expansions, collapsing, and selections) are propogated immediately back to the server. 041 * <p/> 042 * The Tree component uses special tricks to support recursive rendering of the Tree as necessary. 043 * 044 * @tapestrydoc 045 * @since 5.3 046 */ 047@SuppressWarnings( 048 {"rawtypes", "unchecked", "unused"}) 049@Events({EventConstants.NODE_SELECTED, EventConstants.NODE_UNSELECTED}) 050@Import(module = "t5/core/tree") 051public class Tree 052{ 053 /** 054 * The model that drives the tree, determining top level nodes and making revealing the overall structure of the 055 * tree. 056 */ 057 @Parameter(required = true, autoconnect = true) 058 private TreeModel model; 059 060 /** 061 * Allows the container to specify additional CSS class names for the outer DIV element. The outer DIV 062 * always has the class name "tree-container"; the additional class names are typically used to apply 063 * a specific size and width to the component. 064 */ 065 @Parameter(name = "class", defaultPrefix = BindingConstants.LITERAL) 066 private String className; 067 068 /** 069 * Optional parameter used to inform the container about what TreeNode is currently rendering; this 070 * is primarily used when the label parameter is bound. 071 */ 072 @Property 073 @Parameter 074 private TreeNode node; 075 076 /** 077 * Used to control the Tree's expansion model. By default, a persistent field inside the Tree 078 * component stores a {@link DefaultTreeExpansionModel}. This parameter may be bound when more 079 * control over the implementation of the expansion model, or how it is stored, is 080 * required. 081 */ 082 @Parameter(allowNull = false, value = "defaultTreeExpansionModel") 083 private TreeExpansionModel expansionModel; 084 085 /** 086 * Used to control the Tree's selections. When this parameter is bound, then the client-side Tree 087 * will track what is selected or not selected, and communicate this (via Ajax requests) up to 088 * the server, where it will be recorded into the model. On the client-side, the Tree component will 089 * add or remove the {@code selected-leaf-node-label} CSS class from {@code span.tree-label} 090 * for the node. 091 */ 092 @Parameter 093 private TreeSelectionModel selectionModel; 094 095 /** 096 * Optional parameter used to inform the container about the value of the currently rendering TreeNode; this 097 * is often preferable to the TreeNode, and like the node parameter, is primarily used when the label parameter 098 * is bound. 099 */ 100 @Parameter 101 private Object value; 102 103 /** 104 * A renderable (usually a {@link Block}) that can render the label for a tree node. 105 * This will be invoked after the {@link #value} parameter has been updated. 106 */ 107 @Property 108 @Parameter(value = "block:defaultRenderTreeNodeLabel") 109 private RenderCommand label; 110 111 @Environmental 112 private JavaScriptSupport jss; 113 114 @Inject 115 private ComponentResources resources; 116 117 @Persist 118 private TreeExpansionModel defaultTreeExpansionModel; 119 120 private static RenderCommand RENDER_CLOSE_TAG = new RenderCommand() 121 { 122 public void render(MarkupWriter writer, RenderQueue queue) 123 { 124 writer.end(); 125 } 126 }; 127 128 private static RenderCommand RENDER_LABEL_SPAN = new RenderCommand() 129 { 130 public void render(MarkupWriter writer, RenderQueue queue) 131 { 132 writer.element("span", "class", "tree-label"); 133 } 134 }; 135 136 private static RenderCommand MARK_SELECTED = new RenderCommand() 137 { 138 public void render(MarkupWriter writer, RenderQueue queue) 139 { 140 writer.getElement().attribute("class", "selected-leaf-node"); 141 } 142 }; 143 144 /** 145 * Renders a single node (which may be the last within its containing node). 146 * This is a mix of immediate rendering, and queuing up various Blocks and Render commands 147 * to do the rest. May recursively render child nodes of the active node. 148 * 149 * @param node 150 * to render 151 * @param isLast 152 * if true, add "last" attribute to the LI element 153 * @return command to render the node 154 */ 155 private RenderCommand toRenderCommand(final TreeNode node, final boolean isLast) 156 { 157 return new RenderCommand() 158 { 159 public void render(MarkupWriter writer, RenderQueue queue) 160 { 161 // Inform the component's container about what value is being rendered 162 // (this may be necessary to generate the correct label for the node). 163 Tree.this.node = node; 164 165 value = node.getValue(); 166 167 boolean isLeaf = node.isLeaf(); 168 169 writer.element("li"); 170 171 if (isLast) 172 { 173 writer.attributes("class", "last"); 174 } 175 176 if (isLeaf) 177 { 178 writer.getElement().attribute("class", "leaf-node"); 179 } 180 181 Element e = writer.element("span", "class", "tree-icon"); 182 183 if (!isLeaf && !node.getHasChildren()) 184 { 185 e.addClassName("empty-node"); 186 } 187 188 boolean hasChildren = !isLeaf && node.getHasChildren(); 189 boolean expanded = hasChildren && expansionModel.isExpanded(node); 190 191 writer.attributes("data-node-id", node.getId()); 192 193 if (expanded) 194 { 195 // Inform the client side, so it doesn't try to fetch it a second time. 196 e.addClassName("tree-expanded"); 197 } 198 199 writer.end(); // span.tree-icon 200 201 // From here on in, we're pushing things onto the queue. Remember that 202 // execution order is reversed from order commands are pushed. 203 204 queue.push(RENDER_CLOSE_TAG); // li 205 206 if (expanded) 207 { 208 queue.push(new RenderNodes(node.getChildren())); 209 } 210 211 queue.push(RENDER_CLOSE_TAG); 212 queue.push(label); 213 214 if (isLeaf && selectionModel != null && selectionModel.isSelected(node)) 215 { 216 queue.push(MARK_SELECTED); 217 } 218 219 queue.push(RENDER_LABEL_SPAN); 220 221 } 222 }; 223 } 224 225 /** 226 * Renders an <ul> element and renders each node recursively inside the element. 227 */ 228 private class RenderNodes implements RenderCommand 229 { 230 private final Flow<TreeNode> nodes; 231 232 public RenderNodes(List<TreeNode> nodes) 233 { 234 assert !nodes.isEmpty(); 235 236 this.nodes = F.flow(nodes).reverse(); 237 } 238 239 public void render(MarkupWriter writer, final RenderQueue queue) 240 { 241 writer.element("ul"); 242 queue.push(RENDER_CLOSE_TAG); 243 244 queue.push(toRenderCommand(nodes.first(), true)); 245 246 nodes.rest().each(new Worker<TreeNode>() 247 { 248 public void work(TreeNode element) 249 { 250 queue.push(toRenderCommand(element, false)); 251 } 252 }); 253 } 254 255 } 256 257 public String getContainerClass() 258 { 259 return className == null ? "tree-container" : "tree-container " + className; 260 } 261 262 public Link getTreeActionLink() 263 { 264 return resources.createEventLink("treeAction"); 265 } 266 267 Object onTreeAction(@RequestParameter("t:nodeid") String nodeId, 268 @RequestParameter("t:action") String action) 269 { 270 if (action.equalsIgnoreCase("expand")) 271 { 272 return doExpandChildren(nodeId); 273 } 274 275 if (action.equalsIgnoreCase("markExpanded")) 276 { 277 return doMarkExpanded(nodeId); 278 } 279 280 if (action.equalsIgnoreCase("markCollapsed")) 281 { 282 return doMarkCollapsed(nodeId); 283 } 284 285 if (action.equalsIgnoreCase("select")) 286 { 287 return doUpdateSelected(nodeId, true); 288 } 289 290 if (action.equalsIgnoreCase("deselect")) 291 { 292 return doUpdateSelected(nodeId, false); 293 } 294 295 throw new IllegalArgumentException(String.format("Unexpected action: '%s' for Tree component.", action)); 296 } 297 298 Object doExpandChildren(String nodeId) 299 { 300 TreeNode container = model.getById(nodeId); 301 302 expansionModel.markExpanded(container); 303 304 return new RenderNodes(container.getChildren()); 305 } 306 307 Object doMarkExpanded(String nodeId) 308 { 309 expansionModel.markExpanded(model.getById(nodeId)); 310 311 return new JSONObject(); 312 } 313 314 315 Object doMarkCollapsed(String nodeId) 316 { 317 expansionModel.markCollapsed(model.getById(nodeId)); 318 319 return new JSONObject(); 320 } 321 322 Object doUpdateSelected(String nodeId, boolean selected) 323 { 324 TreeNode node = model.getById(nodeId); 325 326 String event; 327 328 if (selected) 329 { 330 selectionModel.select(node); 331 332 event = EventConstants.NODE_SELECTED; 333 } else 334 { 335 selectionModel.unselect(node); 336 337 event = EventConstants.NODE_UNSELECTED; 338 } 339 340 CaptureResultCallback<Object> callback = CaptureResultCallback.create(); 341 342 resources.triggerEvent(event, new Object[]{nodeId}, callback); 343 344 final Object result = callback.getResult(); 345 346 if (result != null) 347 { 348 return result; 349 } 350 351 return new JSONObject(); 352 } 353 354 public TreeExpansionModel getDefaultTreeExpansionModel() 355 { 356 if (defaultTreeExpansionModel == null) 357 { 358 defaultTreeExpansionModel = new DefaultTreeExpansionModel(); 359 } 360 361 return defaultTreeExpansionModel; 362 } 363 364 /** 365 * Returns the actual {@link TreeExpansionModel} in use for this Tree component, 366 * as per the expansionModel parameter. This is often, but not always, the same 367 * as {@link #getDefaultTreeExpansionModel()}. 368 */ 369 public TreeExpansionModel getExpansionModel() 370 { 371 return expansionModel; 372 } 373 374 /** 375 * Returns the actual {@link TreeSelectionModel} in use for this Tree component, 376 * as per the {@link #selectionModel} parameter. 377 */ 378 public TreeSelectionModel getSelectionModel() 379 { 380 return selectionModel; 381 } 382 383 public Object getRenderRootNodes() 384 { 385 return new RenderNodes(model.getRootNodes()); 386 } 387 388 /** 389 * Clears the tree's {@link TreeExpansionModel}. 390 */ 391 public void clearExpansions() 392 { 393 expansionModel.clear(); 394 } 395 396 public Boolean getSelectionEnabled() 397 { 398 return selectionModel != null ? true : null; 399 } 400}