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