// Copyright 2012 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package collide.client.treeview;
import collide.client.treeview.Tree.Css;
import collide.client.util.CssUtils;
import collide.client.util.Elements;
import com.google.collide.client.util.AnimationController;
import elemental.css.CSSStyleDeclaration;
import elemental.dom.Element;
import elemental.html.DivElement;
import elemental.html.SpanElement;
import elemental.js.html.JsLIElement;
import elemental.js.html.JsUListElement;
/**
* Overlay type for the base element for a Node in the tree.
*
* Nodes with no children have no UL element.
*
* Nodes that have children, but that have never been expanded (nodes render
* lazily on expansion), have an empty UL element.
*
* <pre>
*
* <li class="treeNode">
* <div class="treeNodeBody">
* <div class="expandControl"></div><span class="treeNodeLabel"></span>
* </div>
* <ul class="childrenContainer">
* </ul>
* </li>
*
* </pre>
*
*/
public class TreeNodeElement<D> extends JsLIElement {
/**
* Creates a TreeNodeElement from some data. Should only be called by
* {@link Tree}.
*
* @param <D> the type of data
* @param dataAdapter An {@link NodeDataAdapter} that allows us to visit the
* NodeData
* @return a new {@link TreeNodeElement} created from the supplied data.
*/
static <D> TreeNodeElement<D> create(
D data, NodeDataAdapter<D> dataAdapter, NodeRenderer<D> nodeRenderer, Tree.Css css) {
// Make the node base.
@SuppressWarnings("unchecked")
TreeNodeElement<D> treeNode = (TreeNodeElement<D>) Elements.createElement("li", css.treeNode());
treeNode.setData(data);
treeNode.setRenderer(nodeRenderer);
// Associate the rendered node with the underlying model data.
dataAdapter.setRenderedTreeNode(data, treeNode);
// Attach the Tree node body.
DivElement treeNodeBody = Elements.createDivElement(css.treeNodeBody());
treeNodeBody.setAttribute("draggable", "true");
DivElement expandControl = Elements.createDivElement();
SpanElement nodeContents = nodeRenderer.renderNodeContents(data);
nodeContents.addClassName(css.treeNodeLabel());
treeNodeBody.appendChild(expandControl);
treeNodeBody.appendChild(nodeContents);
treeNode.appendChild(treeNodeBody);
treeNode.ensureChildrenContainer(dataAdapter, css);
return treeNode;
}
protected TreeNodeElement() {
}
/**
* Appends the specified child to this TreeNodeElement's child container
* element.
*
* @param child The {@link TreeNodeElement} we want to append to as a child of
* this node.
*/
public final void addChild(
NodeDataAdapter<D> dataAdapter, TreeNodeElement<D> child, Tree.Css css) {
ensureChildrenContainer(dataAdapter, css);
getChildrenContainer().appendChild(child);
}
/**
* @return The associated NodeData that is a bound to this node when it was
* rendered.
*/
public final native D getData() /*-{
return this.__nodeData;
}-*/;
/**
* Nodes with no children have no UL element, only a DIV for the Node body.
*
* @return whether or not this node has children.
*/
public final boolean hasChildrenContainer() {
int length = this.getChildren().getLength();
assert (length < 3) : "TreeNodeElement has more than 2 children of its root element!";
return (length == 2);
}
public final boolean isActive(Tree.Css css) {
return CssUtils.containsClassName(getSelectionElement(), css.active());
}
/**
* Checks whether or not this node is open.
*/
public final native boolean isOpen() /*-{
return !!this.__nodeOpen;
}-*/;
private void setOpen(Tree.Css css, boolean isOpen) {
if (isOpen != isOpen()) {
CssUtils.setClassNameEnabled(getExpandControl(), css.openedIcon(), isOpen);
CssUtils.setClassNameEnabled(getExpandControl(), css.closedIcon(), !isOpen);
setOpenImpl(isOpen);
getRenderer().updateNodeContents(this);
}
}
private native void setOpenImpl(boolean isOpen) /*-{
this.__nodeOpen = isOpen;
}-*/;
public final boolean isSelected(Tree.Css css) {
return CssUtils.containsClassName(getSelectionElement(), css.selected());
}
/**
* Makes this node into a leaf node.
*/
public final void makeLeafNode(Tree.Css css) {
getExpandControl().setClassName(css.expandControl() + " " + css.leafIcon());
if (hasChildrenContainer()) {
getChildrenContainer().removeFromParent();
}
}
/**
* Removes this node from the {@link Tree} and breaks the back reference from
* the underlying node data.
*/
public final void removeFromTree() {
removeFromParent();
}
/**
* Sets whether or not this node has the active node styling applied.
*/
public final void setActive(boolean isActive, Css css) {
// Show the selection on the element returned by the node renderer
Element selectionElement = getSelectionElement();
CssUtils.setClassNameEnabled(selectionElement, css.active(), isActive);
if (isActive) {
selectionElement.focus();
}
}
/**
* Sets whether or not this node has the selected styling applied.
*/
public final void setSelected(boolean isSelected, Tree.Css css) {
CssUtils.setClassNameEnabled(getSelectionElement(), css.selected(), isSelected);
}
/**
* Sets whether or not this node is the active drop target.
*/
public final void setIsDropTarget(boolean isDropTarget, Tree.Css css) {
CssUtils.setClassNameEnabled(this, css.isDropTarget(), isDropTarget);
}
/**
* Closes the current node. Must have children if you call this!
*
* @param css The {@link Tree.Css} instance that contains relevant selector
* names.
* @param shouldAnimate whether to do the animation or not
*/
final void closeNode(NodeDataAdapter<D> dataAdapter, Tree.Css css, AnimationController closer,
boolean shouldAnimate) {
ensureChildrenContainer(dataAdapter, css);
Element expandControl = getExpandControl();
assert (hasChildrenContainer() && CssUtils.containsClassName(expandControl,
css.expandControl())) : "Tried to close a node that didn't have an expand control";
setOpen(css, false);
Element childrenContainer = getChildrenContainer();
if (shouldAnimate) {
closer.hide(childrenContainer);
} else {
closer.hideWithoutAnimating(childrenContainer);
}
}
/**
* You should call hasChildren() before calling this method. This will throw
* an exception if a Node is a leaf node.
*
* @return The UL element containing children of this TreeNodeElement.
*/
final JsUListElement getChildrenContainer() {
return (JsUListElement) this.getChildren().item(1);
}
public final SpanElement getNodeLabel() {
return (SpanElement) getNodeBody().getChildren().item(1);
}
/**
* Expands the current node. Must have children if you call this!
*
* @param css The {@link Tree.Css} instance that contains relevant selector
* names.
* @param shouldAnimate whether to do the animation or not
*/
final void openNode(NodeDataAdapter<D> dataAdapter, Tree.Css css, AnimationController opener,
boolean shouldAnimate) {
ensureChildrenContainer(dataAdapter, css);
Element expandControl = getExpandControl();
assert (hasChildrenContainer() && CssUtils.containsClassName(expandControl,
css.expandControl())) : "Tried to open a node that didn't have an expand control";
setOpen(css, true);
Element childrenContainer = getChildrenContainer();
if (shouldAnimate) {
opener.show(childrenContainer);
} else {
opener.showWithoutAnimating(childrenContainer);
}
}
/**
* If this node does not have a children container, but has children data,
* then we coerce a children container into existence.
*/
final void ensureChildrenContainer(NodeDataAdapter<D> dataAdapter, Tree.Css css) {
if (!hasChildrenContainer()) {
D data = getData();
if (dataAdapter.hasChildren(data)) {
Element childrenContainer = Elements.createElement("ul", css.childrenContainer());
this.appendChild(childrenContainer);
childrenContainer.getStyle().setDisplay(CSSStyleDeclaration.Display.NONE);
getExpandControl().setClassName(css.expandControl() + " " + css.closedIcon());
} else {
getExpandControl().setClassName(css.expandControl() + " " + css.leafIcon());
}
}
}
private Element getExpandControl() {
return (Element)getNodeBody().getChildren().item(0);
}
/**
* @return The node body element that contains the expansion control and the
* node contents.
*/
private Element getNodeBody() {
return (Element)getChildren().item(0);
}
final Element getSelectionElement() {
return getNodeBody();
}
/**
* Stashes associate NodeData as an expando on our element, and also sets up a
* reverse mapping.
*
* @param data The NodeData we want to associate with this node element.
*/
private native void setData(D data) /*-{
this.__nodeData = data;
}-*/;
private native NodeRenderer<D> getRenderer() /*-{
return this.__nodeRenderer;
}-*/;
private native void setRenderer(NodeRenderer<D> renderer) /*-{
this.__nodeRenderer = renderer;
}-*/;
}