    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    }