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
015package org.apache.tapestry5.corelib.components;
016
017import org.apache.tapestry5.*;
018import org.apache.tapestry5.annotations.*;
019import org.apache.tapestry5.dom.Element;
020import org.apache.tapestry5.func.F;
021import org.apache.tapestry5.func.Flow;
022import org.apache.tapestry5.func.Worker;
023import org.apache.tapestry5.internal.util.CaptureResultCallback;
024import org.apache.tapestry5.ioc.annotations.Inject;
025import org.apache.tapestry5.json.JSONObject;
026import org.apache.tapestry5.runtime.RenderCommand;
027import org.apache.tapestry5.runtime.RenderQueue;
028import org.apache.tapestry5.services.javascript.JavaScriptSupport;
029import org.apache.tapestry5.tree.*;
030
031import 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})
047public 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}