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