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            this.nodes = F.flow(nodes).reverse();
259        }
260
261        public void render(MarkupWriter writer, final RenderQueue queue)
262        {
263            writer.element("ul");
264            queue.push(RENDER_CLOSE_TAG);
265
266            // TAP5-2745: Support empty tree model
267            if (nodes.isEmpty()) {
268                return;
269            }
270
271            queue.push(toRenderCommand(nodes.first(), true));
272
273            nodes.rest().each(new Worker<TreeNode>()
274            {
275                public void work(TreeNode element)
276                {
277                    queue.push(toRenderCommand(element, false));
278                }
279            });
280        }
281
282    }
283
284    public String getContainerClass()
285    {
286        return className == null ? "tree-container" : "tree-container " + className;
287    }
288
289    public Link getTreeActionLink()
290    {
291        return resources.createEventLink("treeAction");
292    }
293
294    Object onTreeAction(@RequestParameter("t:nodeid") String nodeId,
295                        @RequestParameter("t:action") String action)
296    {
297        if (action.equalsIgnoreCase("expand"))
298        {
299            return doExpandChildren(nodeId);
300        }
301
302        if (action.equalsIgnoreCase("markExpanded"))
303        {
304            return doMarkExpanded(nodeId);
305        }
306
307        if (action.equalsIgnoreCase("markCollapsed"))
308        {
309            return doMarkCollapsed(nodeId);
310        }
311
312        if (action.equalsIgnoreCase("select"))
313        {
314            return doUpdateSelected(nodeId, true);
315        }
316
317        if (action.equalsIgnoreCase("deselect"))
318        {
319            return doUpdateSelected(nodeId, false);
320        }
321
322        throw new IllegalArgumentException(String.format("Unexpected action: '%s' for Tree component.", action));
323    }
324
325    Object doExpandChildren(String nodeId)
326    {
327        TreeNode container = model.getById(nodeId);
328
329        expansionModel.markExpanded(container);
330
331        return new RenderNodes(container.getChildren());
332    }
333
334    Object doMarkExpanded(String nodeId)
335    {
336        expansionModel.markExpanded(model.getById(nodeId));
337
338        return new JSONObject();
339    }
340
341
342    Object doMarkCollapsed(String nodeId)
343    {
344        expansionModel.markCollapsed(model.getById(nodeId));
345
346        return new JSONObject();
347    }
348
349    Object doUpdateSelected(String nodeId, boolean selected)
350    {
351        TreeNode node = model.getById(nodeId);
352
353        String event;
354
355        if (selected)
356        {
357            selectionModel.select(node);
358
359            event = EventConstants.NODE_SELECTED;
360        } else
361        {
362            selectionModel.unselect(node);
363
364            event = EventConstants.NODE_UNSELECTED;
365        }
366
367        CaptureResultCallback<Object> callback = CaptureResultCallback.create();
368
369        resources.triggerEvent(event, new Object[]{nodeId}, callback);
370
371        final Object result = callback.getResult();
372
373        if (result != null)
374        {
375            return result;
376        }
377
378        return new JSONObject();
379    }
380
381    public TreeExpansionModel getDefaultTreeExpansionModel()
382    {
383        if (defaultTreeExpansionModel == null)
384        {
385            defaultTreeExpansionModel = new DefaultTreeExpansionModel();
386        }
387
388        return defaultTreeExpansionModel;
389    }
390
391    /**
392     * Returns the actual {@link TreeExpansionModel} in use for this Tree component,
393     * as per the expansionModel parameter. This is often, but not always, the same
394     * as {@link #getDefaultTreeExpansionModel()}.
395     */
396    public TreeExpansionModel getExpansionModel()
397    {
398        return expansionModel;
399    }
400
401    /**
402     * Returns the actual {@link TreeSelectionModel} in use for this Tree component,
403     * as per the {@link #selectionModel} parameter.
404     */
405    public TreeSelectionModel getSelectionModel()
406    {
407        return selectionModel;
408    }
409
410    public Object getRenderRootNodes()
411    {
412        return new RenderNodes(model.getRootNodes());
413    }
414
415    /**
416     * Clears the tree's {@link TreeExpansionModel}.
417     */
418    public void clearExpansions()
419    {
420        expansionModel.clear();
421    }
422
423    public Boolean getSelectionEnabled()
424    {
425        return selectionModel != null ? true : null;
426    }
427}