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 }