001// Licensed under the Apache License, Version 2.0 (the "License");
002// you may not use this file except in compliance with the License.
003// You may obtain a copy of the License at
004//
005// http://www.apache.org/licenses/LICENSE-2.0
006//
007// Unless required by applicable law or agreed to in writing, software
008// distributed under the License is distributed on an "AS IS" BASIS,
009// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
010// See the License for the specific language governing permissions and
011// limitations under the License.
012
013package org.apache.tapestry5.corelib.components;
014
015import org.apache.tapestry5.*;
016import org.apache.tapestry5.annotations.*;
017import org.apache.tapestry5.dom.Element;
018import org.apache.tapestry5.func.F;
019import org.apache.tapestry5.func.Flow;
020import org.apache.tapestry5.func.Worker;
021import org.apache.tapestry5.http.Link;
022import org.apache.tapestry5.internal.util.CaptureResultCallback;
023import org.apache.tapestry5.ioc.annotations.Inject;
024import org.apache.tapestry5.json.JSONObject;
025import org.apache.tapestry5.runtime.RenderCommand;
026import org.apache.tapestry5.runtime.RenderQueue;
027import org.apache.tapestry5.services.Heartbeat;
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 *
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 propagated immediately back to the server.
041 *
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    @Environmental
145    private Heartbeat heartbeat;
146
147    /**
148     * Renders a single node (which may be the last within its containing node).
149     * This is a mix of immediate rendering, and queuing up various Blocks and Render commands
150     * to do the rest. May recursively render child nodes of the active node. The label part
151     * of the node is rendered inside a {@linkplain org.apache.tapestry5.services.Heartbeat heartbeat}.
152     *
153     * @param node
154     *         to render
155     * @param isLast
156     *         if true, add "last" attribute to the LI element
157     * @return command to render the node
158     */
159    private RenderCommand toRenderCommand(final TreeNode node, final boolean isLast)
160    {
161        return new RenderCommand()
162        {
163            public void render(MarkupWriter writer, RenderQueue queue)
164            {
165                // Inform the component's container about what value is being rendered
166                // (this may be necessary to generate the correct label for the node).
167                Tree.this.node = node;
168
169                value = node.getValue();
170
171                boolean isLeaf = node.isLeaf();
172
173                writer.element("li");
174
175                if (isLast)
176                {
177                    writer.attributes("class", "last");
178                }
179
180                if (isLeaf)
181                {
182                    writer.getElement().attribute("class", "leaf-node");
183                }
184
185                Element e = writer.element("span", "class", "tree-icon");
186
187                if (!isLeaf && !node.getHasChildren())
188                {
189                    e.addClassName("empty-node");
190                }
191
192                boolean hasChildren = !isLeaf && node.getHasChildren();
193                boolean expanded = hasChildren && expansionModel.isExpanded(node);
194
195                writer.attributes("data-node-id", node.getId());
196
197                if (expanded)
198                {
199                    // Inform the client side, so it doesn't try to fetch it a second time.
200                    e.addClassName("tree-expanded");
201                }
202
203                writer.end(); // span.tree-icon
204
205                // From here on in, we're pushing things onto the queue. Remember that
206                // execution order is reversed from order commands are pushed.
207
208                queue.push(RENDER_CLOSE_TAG); // li
209
210                if (expanded)
211                {
212                    queue.push(new RenderNodes(node.getChildren()));
213                }
214
215                queue.push(RENDER_CLOSE_TAG);
216                final RenderCommand startHeartbeat = new RenderCommand() {
217
218                    @Override
219                    public void render(MarkupWriter writer, RenderQueue queue) {
220                        heartbeat.begin();
221                    }
222                };
223
224                final RenderCommand endHeartbeat = new RenderCommand() {
225
226                    @Override
227                    public void render(MarkupWriter writer, RenderQueue queue) {
228                        heartbeat.end();
229                    }
230                };
231
232                queue.push(endHeartbeat);
233
234                queue.push(label);
235
236                queue.push(startHeartbeat);
237
238                if (isLeaf && selectionModel != null && selectionModel.isSelected(node))
239                {
240                    queue.push(MARK_SELECTED);
241                }
242
243                queue.push(RENDER_LABEL_SPAN);
244
245            }
246        };
247    }
248
249    /**
250     * Renders an &lt;ul&gt; element and renders each node recursively inside the element.
251     */
252    private class RenderNodes implements RenderCommand
253    {
254        private final Flow<TreeNode> nodes;
255
256        public RenderNodes(List<TreeNode> nodes)
257        {
258            assert !nodes.isEmpty();
259
260            this.nodes = F.flow(nodes).reverse();
261        }
262
263        public void render(MarkupWriter writer, final RenderQueue queue)
264        {
265            writer.element("ul");
266            queue.push(RENDER_CLOSE_TAG);
267
268            queue.push(toRenderCommand(nodes.first(), true));
269
270            nodes.rest().each(new Worker<TreeNode>()
271            {
272                public void work(TreeNode element)
273                {
274                    queue.push(toRenderCommand(element, false));
275                }
276            });
277        }
278
279    }
280
281    public String getContainerClass()
282    {
283        return className == null ? "tree-container" : "tree-container " + className;
284    }
285
286    public Link getTreeActionLink()
287    {
288        return resources.createEventLink("treeAction");
289    }
290
291    Object onTreeAction(@RequestParameter("t:nodeid") String nodeId,
292                        @RequestParameter("t:action") String action)
293    {
294        if (action.equalsIgnoreCase("expand"))
295        {
296            return doExpandChildren(nodeId);
297        }
298
299        if (action.equalsIgnoreCase("markExpanded"))
300        {
301            return doMarkExpanded(nodeId);
302        }
303
304        if (action.equalsIgnoreCase("markCollapsed"))
305        {
306            return doMarkCollapsed(nodeId);
307        }
308
309        if (action.equalsIgnoreCase("select"))
310        {
311            return doUpdateSelected(nodeId, true);
312        }
313
314        if (action.equalsIgnoreCase("deselect"))
315        {
316            return doUpdateSelected(nodeId, false);
317        }
318
319        throw new IllegalArgumentException(String.format("Unexpected action: '%s' for Tree component.", action));
320    }
321
322    Object doExpandChildren(String nodeId)
323    {
324        TreeNode container = model.getById(nodeId);
325
326        expansionModel.markExpanded(container);
327
328        return new RenderNodes(container.getChildren());
329    }
330
331    Object doMarkExpanded(String nodeId)
332    {
333        expansionModel.markExpanded(model.getById(nodeId));
334
335        return new JSONObject();
336    }
337
338
339    Object doMarkCollapsed(String nodeId)
340    {
341        expansionModel.markCollapsed(model.getById(nodeId));
342
343        return new JSONObject();
344    }
345
346    Object doUpdateSelected(String nodeId, boolean selected)
347    {
348        TreeNode node = model.getById(nodeId);
349
350        String event;
351
352        if (selected)
353        {
354            selectionModel.select(node);
355
356            event = EventConstants.NODE_SELECTED;
357        } else
358        {
359            selectionModel.unselect(node);
360
361            event = EventConstants.NODE_UNSELECTED;
362        }
363
364        CaptureResultCallback<Object> callback = CaptureResultCallback.create();
365
366        resources.triggerEvent(event, new Object[]{nodeId}, callback);
367
368        final Object result = callback.getResult();
369
370        if (result != null)
371        {
372            return result;
373        }
374
375        return new JSONObject();
376    }
377
378    public TreeExpansionModel getDefaultTreeExpansionModel()
379    {
380        if (defaultTreeExpansionModel == null)
381        {
382            defaultTreeExpansionModel = new DefaultTreeExpansionModel();
383        }
384
385        return defaultTreeExpansionModel;
386    }
387
388    /**
389     * Returns the actual {@link TreeExpansionModel} in use for this Tree component,
390     * as per the expansionModel parameter. This is often, but not always, the same
391     * as {@link #getDefaultTreeExpansionModel()}.
392     */
393    public TreeExpansionModel getExpansionModel()
394    {
395        return expansionModel;
396    }
397
398    /**
399     * Returns the actual {@link TreeSelectionModel} in use for this Tree component,
400     * as per the {@link #selectionModel} parameter.
401     */
402    public TreeSelectionModel getSelectionModel()
403    {
404        return selectionModel;
405    }
406
407    public Object getRenderRootNodes()
408    {
409        return new RenderNodes(model.getRootNodes());
410    }
411
412    /**
413     * Clears the tree's {@link TreeExpansionModel}.
414     */
415    public void clearExpansions()
416    {
417        expansionModel.clear();
418    }
419
420    public Boolean getSelectionEnabled()
421    {
422        return selectionModel != null ? true : null;
423    }
424}