001 // Copyright 2011 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 015 package org.apache.tapestry5.corelib.components; 016 017 import org.apache.tapestry5.*; 018 import org.apache.tapestry5.annotations.*; 019 import org.apache.tapestry5.dom.Element; 020 import org.apache.tapestry5.func.F; 021 import org.apache.tapestry5.func.Flow; 022 import org.apache.tapestry5.func.Worker; 023 import org.apache.tapestry5.internal.util.CaptureResultCallback; 024 import org.apache.tapestry5.ioc.annotations.Inject; 025 import org.apache.tapestry5.json.JSONObject; 026 import org.apache.tapestry5.runtime.RenderCommand; 027 import org.apache.tapestry5.runtime.RenderQueue; 028 import org.apache.tapestry5.services.javascript.JavaScriptSupport; 029 import org.apache.tapestry5.tree.*; 030 031 import 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 * The Tree component uses special tricks to support recursive rendering of the Tree as necessary. 040 * 041 * @since 5.3 042 */ 043 @SuppressWarnings( 044 {"rawtypes", "unchecked", "unused"}) 045 @Events({EventConstants.NODE_SELECTED, EventConstants.NODE_UNSELECTED}) 046 public class Tree 047 { 048 /** 049 * The model that drives the tree, determining top level nodes and making revealing the overall structure of the 050 * tree. 051 */ 052 @Parameter(required = true, autoconnect = true) 053 private TreeModel model; 054 055 /** 056 * Allows the container to specify additional CSS class names for the outer DIV element. The outer DIV 057 * always has the class name "t-tree-container"; the additional class names are typically used to apply 058 * a specific size and width to the component. 059 */ 060 @Parameter(name = "class", defaultPrefix = BindingConstants.LITERAL) 061 private String className; 062 063 /** 064 * Optional parameter used to inform the container about what TreeNode is currently rendering; this 065 * is primarily used when the label parameter is bound. 066 */ 067 @Property 068 @Parameter 069 private TreeNode node; 070 071 /** 072 * Used to control the Tree's expansion model. By default, a persistent field inside the Tree 073 * component stores a {@link DefaultTreeExpansionModel}. This parameter may be bound when more 074 * control over the implementation of the expansion model, or how it is stored, is 075 * required. 076 */ 077 @Parameter(allowNull = false, value = "defaultTreeExpansionModel") 078 private TreeExpansionModel expansionModel; 079 080 /** 081 * Used to control the Tree's selections. When this parameter is bound, then the client-side Tree 082 * will track what is selected or not selected, and communicate this (via Ajax requests) up to 083 * the server, where it will be recorded into the model. On the client-side, the Tree component will 084 * add or remove the {@code t-selected-leaf-node-label} CSS class from {@code span.t-tree-label} 085 * for the node. 086 */ 087 @Parameter 088 private TreeSelectionModel selectionModel; 089 090 /** 091 * Optional parameter used to inform the container about the value of the currently rendering TreeNode; this 092 * is often preferable to the TreeNode, and like the node parameter, is primarily used when the label parameter 093 * it bound. 094 */ 095 @Parameter 096 private Object value; 097 098 /** 099 * A renderable (usually a {@link Block}) that can render the label for a tree node. 100 * This will be invoked after the {@link #value} parameter has been updated. 101 */ 102 @Property 103 @Parameter(value = "block:defaultRenderTreeNodeLabel") 104 private RenderCommand label; 105 106 @Environmental 107 private JavaScriptSupport jss; 108 109 @Inject 110 private ComponentResources resources; 111 112 @Persist 113 private TreeExpansionModel defaultTreeExpansionModel; 114 115 private static RenderCommand RENDER_CLOSE_TAG = new RenderCommand() 116 { 117 public void render(MarkupWriter writer, RenderQueue queue) 118 { 119 writer.end(); 120 } 121 }; 122 123 private static RenderCommand RENDER_LABEL_SPAN = new RenderCommand() 124 { 125 public void render(MarkupWriter writer, RenderQueue queue) 126 { 127 writer.element("span", "class", "t-tree-label"); 128 } 129 }; 130 131 /** 132 * Renders a single node (which may be the last within its containing node). 133 * This is a mix of immediate rendering, and queuing up various Blocks and Render commands 134 * to do the rest. May recursively render child nodes of the active node. 135 * 136 * @param node to render 137 * @param isLast if true, add "t-last" attribute to the LI element 138 * @return command to render the node 139 */ 140 private RenderCommand toRenderCommand(final TreeNode node, final boolean isLast) 141 { 142 return new RenderCommand() 143 { 144 public void render(MarkupWriter writer, RenderQueue queue) 145 { 146 // Inform the component's container about what value is being rendered 147 // (this may be necessary to generate the correct label for the node). 148 Tree.this.node = node; 149 150 value = node.getValue(); 151 152 writer.element("li"); 153 154 if (isLast) 155 writer.attributes("class", "t-last"); 156 157 Element e = writer.element("span", "class", "t-tree-icon"); 158 159 if (node.isLeaf()) 160 e.addClassName("t-leaf-node"); 161 else if (!node.getHasChildren()) 162 e.addClassName("t-empty-node"); 163 164 boolean hasChildren = !node.isLeaf() && node.getHasChildren(); 165 boolean expanded = hasChildren && expansionModel.isExpanded(node); 166 167 String clientId = jss.allocateClientId(resources); 168 169 JSONObject spec = new JSONObject("clientId", clientId); 170 171 e.attribute("id", clientId); 172 173 spec.put("leaf", node.isLeaf()); 174 175 if (hasChildren) 176 { 177 Link expandChildren = resources.createEventLink("expandChildren", node.getId()); 178 Link markExpanded = resources.createEventLink("markExpanded", node.getId()); 179 Link markCollapsed = resources.createEventLink("markCollapsed", node.getId()); 180 181 spec.put("expandChildrenURL", expandChildren.toString()) 182 .put("markExpandedURL", markExpanded.toString()) 183 .put("markCollapsedURL", markCollapsed.toString()); 184 185 if (expanded) 186 spec.put("expanded", true); 187 } else 188 { 189 if (selectionModel != null) 190 { 191 // May need to address this in the future; in other tree implementations I've constructed, 192 // folders are selectable, and selections even propagate up and down the tree. 193 194 Link selectLeaf = resources.createEventLink("select", node.getId()); 195 196 spec.put("selectURL", selectLeaf.toString()); 197 if (selectionModel.isSelected(node)) 198 { 199 spec.put("selected", true); 200 } 201 } 202 } 203 204 jss.addInitializerCall("treeNode", spec); 205 206 writer.end(); // span.tx-tree-icon 207 208 // From here on in, we're pushing things onto the queue. Remember that 209 // execution order is reversed from order commands are pushed. 210 211 queue.push(RENDER_CLOSE_TAG); // li 212 213 if (expanded) 214 215 { 216 queue.push(new RenderNodes(node.getChildren())); 217 } 218 219 queue.push(RENDER_CLOSE_TAG); 220 queue.push(label); 221 queue.push(RENDER_LABEL_SPAN); 222 223 } 224 } 225 226 ; 227 } 228 229 /** 230 * Renders an <ul> element and renders each node recursively inside the element. 231 */ 232 private class RenderNodes implements RenderCommand 233 { 234 private final Flow<TreeNode> nodes; 235 236 public RenderNodes(List<TreeNode> nodes) 237 { 238 assert !nodes.isEmpty(); 239 240 this.nodes = F.flow(nodes).reverse(); 241 } 242 243 public void render(MarkupWriter writer, final RenderQueue queue) 244 { 245 writer.element("ul"); 246 queue.push(RENDER_CLOSE_TAG); 247 248 queue.push(toRenderCommand(nodes.first(), true)); 249 250 nodes.rest().each(new Worker<TreeNode>() 251 { 252 public void work(TreeNode element) 253 { 254 queue.push(toRenderCommand(element, false)); 255 } 256 }); 257 } 258 259 } 260 261 public String getContainerClass() 262 { 263 return className == null ? "t-tree-container" : "t-tree-container " + className; 264 } 265 266 Object onExpandChildren(String nodeId) 267 { 268 TreeNode container = model.getById(nodeId); 269 270 expansionModel.markExpanded(container); 271 272 return new RenderNodes(container.getChildren()); 273 } 274 275 Object onMarkExpanded(String nodeId) 276 { 277 expansionModel.markExpanded(model.getById(nodeId)); 278 279 return new JSONObject(); 280 } 281 282 Object onMarkCollapsed(String nodeId) 283 { 284 expansionModel.markCollapsed(model.getById(nodeId)); 285 286 return new JSONObject(); 287 } 288 289 Object onSelect(String nodeId, @RequestParameter("t:selected") boolean selected) 290 { 291 TreeNode node = model.getById(nodeId); 292 293 String event; 294 295 if (selected) 296 { 297 selectionModel.select(node); 298 299 event = EventConstants.NODE_SELECTED; 300 } else 301 { 302 selectionModel.unselect(node); 303 304 event = EventConstants.NODE_UNSELECTED; 305 } 306 307 CaptureResultCallback<Object> callback = CaptureResultCallback.create(); 308 309 resources.triggerEvent(event, new Object[]{nodeId}, callback); 310 311 final Object result = callback.getResult(); 312 313 if (result != null) 314 return result; 315 316 return new JSONObject(); 317 } 318 319 public TreeExpansionModel getDefaultTreeExpansionModel() 320 { 321 if (defaultTreeExpansionModel == null) 322 defaultTreeExpansionModel = new DefaultTreeExpansionModel(); 323 324 return defaultTreeExpansionModel; 325 } 326 327 /** 328 * Returns the actual {@link TreeExpansionModel} in use for this Tree component, 329 * as per the expansionModel parameter. This is often, but not always, the same 330 * as {@link #getDefaultTreeExpansionModel()}. 331 */ 332 public TreeExpansionModel getExpansionModel() 333 { 334 return expansionModel; 335 } 336 337 /** 338 * Returns the actual {@link TreeSelectionModel} in use for this Tree component, 339 * as per the {@link #selectionModel} parameter. 340 */ 341 public TreeSelectionModel getSelectionModel() 342 { 343 return selectionModel; 344 } 345 346 public Object getRenderRootNodes() 347 { 348 return new RenderNodes(model.getRootNodes()); 349 } 350 351 /** 352 * Clears the tree's {@link TreeExpansionModel}. 353 */ 354 public void clearExpansions() 355 { 356 expansionModel.clear(); 357 } 358 }