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 &lt;ul&gt; 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    }