package er.ajax; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.webobjects.appserver.WOActionResults; import com.webobjects.appserver.WOComponent; import com.webobjects.appserver.WOContext; import com.webobjects.appserver.WORequest; import com.webobjects.appserver.WOResponse; import com.webobjects.foundation.NSArray; import com.webobjects.foundation.NSMutableArray; import er.extensions.appserver.ERXWOContext; import er.extensions.components.ERXComponentUtilities; /** * AjaxTree provides an Ajax-refreshing tree view. AjaxTree acts like a WOComponentContent where the content you provide * will be the renderer for the tree nodes. The "item" binding provides you access to the current tree node as it * iterates over the tree. * * If your node objects are homogenous in type, you can define parentKeyPath and childrenKeyPath. If your node objects * are heterogenous, you can instead define a delegate, as defined in the AjaxTreeModel.Delegate interface. * * @binding root the root node of the tree * @binding item the current tree node (equivalent to "item" on WORepetition) * @binding itemClass the class of the current item * @binding itemID the id of the current item * @binding rootExpanded if true, the tree will require the root node to be open; ignored if showRoot = false * @binding allExpanded if true, the tree defaults to have all its nodes expanded * @binding parentKeyPath the keypath to call on a node to get its parent node (ignored if delegate is set) * @binding childrenKeyPath the keypath to call on a node to get its children NSArray (ignored if delegate is set) * @binding isLeafKeyPath the keypath to call on a node to determine if it is a leaf node (ignored if delegate, and * defaults to return childrenKeyPath.count() == 0 if not set) * @binding id the html id of the tree * @binding class the html class of the tree * @binding treeModel the treeModel to use (one will be created by default) * @binding collapsedImage the icon to use for a collapsed node * @binding collapsedImageFramework the framework to load the collapsed icon from * @binding expandedImage the icon to use for an expanded node * @binding expandedImageFramework the framework to load the expanded icon from * @binding leafImage the icon to use for a leaf node * @binding leafImageFramework the framework to load the leaf icon from * @binding delegate the delegate to use instead of keypaths (see AjaxTreeModel.Delegate) * @binding showRoot if false, the root node will be skipped and the tree will begin with its children * @binding cache whether to cache the nodes or determine them every time from the model (default: true) * * @author mschrag */ public class AjaxTree extends WOComponent { /** * Do I need to update serialVersionUID? * See section 5.6 <cite>Type Changes Affecting Serialization</cite> on page 51 of the * <a href="http://java.sun.com/j2se/1.4/pdf/serial-spec.pdf">Java Object Serialization Spec</a> */ private static final long serialVersionUID = 1L; private static final Logger log = LoggerFactory.getLogger(AjaxTree.class); private AjaxTreeModel _treeModel; private NSArray _nodes; private int _level; private int _closeCount; private Object _lastParent; private Object _item; private String _id = null; private Object _lastRootNode; public AjaxTree(WOContext context) { super(context); } @Override public boolean synchronizesVariablesWithBindings() { return false; } public NSArray nodes() { Object rootNode = treeModel().rootTreeNode(); boolean useCache = ERXComponentUtilities.booleanValueForBinding("cache", true, _keyAssociations, parent()); if (_nodes == null || rootNode == null || !rootNode.equals(_lastRootNode) || !useCache) { NSMutableArray nodes = new NSMutableArray(); boolean showRoot = ERXComponentUtilities.booleanValueForBinding("showRoot", true, _keyAssociations, parent()); _fillInOpenNodes(treeModel().rootTreeNode(), nodes, showRoot); _nodes = nodes; _lastRootNode = rootNode; } return _nodes; } protected void _fillInOpenNodes(Object node, NSMutableArray nodes, boolean showNode) { if (showNode) { nodes.addObject(node); } if (treeModel().isExpanded(node)) { NSArray childrenTreeNodes = treeModel().childrenTreeNodes(node); if (childrenTreeNodes != null) { int childTreeNodeCount = childrenTreeNodes.count(); for (int childTreeNodeNum = 0; childTreeNodeNum < childTreeNodeCount; childTreeNodeNum++) { Object childNode = childrenTreeNodes.objectAtIndex(childTreeNodeNum); _fillInOpenNodes(childNode, nodes, true); } } } } @Override public void reset() { super.reset(); } protected void resetTree() { _level = 0; _closeCount = 0; _lastParent = null; _item = null; treeModel().setDelegate(valueForBinding("delegate")); if (hasBinding("allExpanded")) { treeModel().setAllExpanded(ERXComponentUtilities.booleanValueForBinding("allExpanded", false, _keyAssociations, parent())); } if (hasBinding("rootExpanded") || hasBinding("showRoot")) { treeModel().setRootExpanded(ERXComponentUtilities.booleanValueForBinding("rootExpanded", false, _keyAssociations, parent()) || !ERXComponentUtilities.booleanValueForBinding("showRoot", true, _keyAssociations, parent())); } treeModel().setIsLeafKeyPath(stringValueForBinding("isLeafKeyPath", null)); treeModel().setParentTreeNodeKeyPath(stringValueForBinding("parentKeyPath", null)); treeModel().setChildrenTreeNodesKeyPath(stringValueForBinding("childrenKeyPath", null)); treeModel().setRootTreeNode(valueForBinding("root")); setItem(treeModel().rootTreeNode()); } @Override public void appendToResponse(WOResponse aResponse, WOContext aContext) { resetTree(); super.appendToResponse(aResponse, aContext); resetTree(); } @Override public void takeValuesFromRequest(WORequest aRequest, WOContext aContext) { resetTree(); super.takeValuesFromRequest(aRequest, aContext); resetTree(); } @Override public WOActionResults invokeAction(WORequest aRequest, WOContext aContext) { resetTree(); WOActionResults results = super.invokeAction(aRequest, aContext); resetTree(); return results; } public void setItem(Object item) { if (item != _item) { Object parent = treeModel().parentTreeNode(item); int level; if (parent == null) { level = 0; } else if (parent == _item) { level = _level + 1; } else if (parent != _lastParent) { level = treeModel().level(item); } else { level = _level; } if (_level > level) { _closeCount = (_level - level); } else { _closeCount = 0; } _lastParent = parent; _level = level; _item = item; log.debug("AjaxTree item at level {} with close count {}: {}", _level, _closeCount, _item); setValueForBinding(item, "item"); } } public Object item() { return _item; } public boolean isLeaf() { return treeModel().isLeaf(_item); } public boolean isExpanded() { return treeModel().isExpanded(_item); } public int _closeCount() { return _closeCount; } /** * Count of /ul /li close elements at the end of the tree. * If showRoot is "false" all displayed nodes have an internal "level" one greater than is really displayed, * e.g. the first level after the root has a level of "1", but is displayed at level "0". CloseCount is used * to close any open elements. When showRoot is "false" the jump from level 1 to level 0 at the end would compute * to a closeCount of 1, but the last close is not necessary (as we are really displaying at level 0) and must be * avoided. * @return the last close count */ public int lastCloseCount() { if (ERXComponentUtilities.booleanValueForBinding("showRoot", true, _keyAssociations, parent()) || _closeCount < 1) { return _closeCount; } return _closeCount - 1; } public void setTreeModel(AjaxTreeModel treeModel) { _treeModel = treeModel; } public AjaxTreeModel treeModel() { if (_treeModel == null) { if (canGetValueForBinding("treeModel") && valueForBinding("treeModel") != null) { _treeModel = (AjaxTreeModel) valueForBinding("treeModel"); } else { _treeModel = new AjaxTreeModel(); if (canSetValueForBinding("treeModel")) { setValueForBinding(_treeModel, "treeModel"); } } } return _treeModel; } public String id() { if (_id == null) { if (hasBinding("id")) { _id = (String) valueForBinding("id"); } else { _id = ERXWOContext.safeIdentifierName(context(), true); } } return _id; } protected String stringValueForBinding(String bindingName, String defaultValue) { String value = defaultValue; if (hasBinding(bindingName)) { value = (String) valueForBinding(bindingName); } return value; } public String collapsedImage() { return stringValueForBinding("collapsedImage", "collapsed.gif"); } public String collapsedImageFramework() { return stringValueForBinding("collapsedImageFramework", "Ajax"); } public String expandedImage() { return stringValueForBinding("expandedImage", "expanded.gif"); } public String expandedImageFramework() { return stringValueForBinding("expandedImageFramework", "Ajax"); } public String leafImage() { return stringValueForBinding("leafImage", "leaf.gif"); } public String leafImageFramework() { return stringValueForBinding("leafImageFramework", "Ajax"); } public String imageLinkClass() { return stringValueForBinding("imageLinkClass", ""); } public String nodeItem() { StringBuilder nodeItem = new StringBuilder(); nodeItem.append("<li"); if (hasBinding("itemID")) { String itemID = (String) valueForBinding("itemID"); if (itemID != null) { nodeItem.append(" id = \""); nodeItem.append(itemID); nodeItem.append("\""); } } if (hasBinding("itemClass")) { String itemClass = (String) valueForBinding("itemClass"); if (itemClass != null) { nodeItem.append(" class = \""); nodeItem.append(itemClass); nodeItem.append("\""); } } nodeItem.append('>'); return nodeItem.toString(); } public String _toggleFunctionName() { return id() + "Toggle"; } public WOActionResults expand() { treeModel().setExpanded(_item, true); _nodes = null; return null; } public WOActionResults collapse() { treeModel().setExpanded(_item, false); _nodes = null; return null; } }