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