// 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 org.eclipse.che.ide.ui.tree;
import org.eclipse.che.ide.util.AnimationController;
import org.eclipse.che.ide.util.CssUtils;
import org.eclipse.che.ide.util.dom.DomUtils;
import org.eclipse.che.ide.util.dom.Elements;
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.
* <p/>
* Nodes with no children have no UL element.
* <p/>
* Nodes that have children, but that have never been expanded (nodes render
* lazily on expansion), have an empty UL element.
* <p/>
* <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, Tree.Resources resources) {
@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 nodeBody = Elements.createDivElement(css.treeNodeBody());
nodeBody.setAttribute("draggable", "true");
treeNode.appendChild(nodeBody);
// Attach expand node element
DivElement expand = Elements.createDivElement();
Elements.addClassName(css.expandControl(), expand);
nodeBody.appendChild(expand);
expand.setInnerHTML(resources.collapsedIcon().getSvg().getElement().getString() +
resources.expandedIcon().getSvg().getElement().getString());
((Element)expand.getChildNodes().item(1)).getStyle().setDisplay("none");
SpanElement nodeContent = nodeRenderer.renderNodeContents(data);
Elements.addClassName(css.treeNodeLabel(), nodeContent);
nodeBody.appendChild(nodeContent);
// Attach the Tree node children.
treeNode.ensureChildrenContainer(dataAdapter, css);
return treeNode;
}
protected TreeNodeElement() {
}
public final void updateLeafOffset(Element parent) {
if (!parent.hasAttribute("___depth")) {
return;
}
try {
int depth = Integer.parseInt(parent.getAttribute("___depth"));
Element expandElement = (Element)getNodeBody().getChildren().item(0);
expandElement.getStyle().setMarginLeft("" + (depth * 8) + "px");
if (! hasChildNodes()) {
return;
}
final JsUListElement childrenContainer = getChildrenContainer();
if (childrenContainer != null) {
getChildrenContainer().setAttribute("___depth", "" + (depth + 1));
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 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()) {
if (isOpen) {
((Element)getExpandControl().getChildNodes().item(0)).getStyle().setDisplay("none");
((Element)getExpandControl().getChildNodes().item(1)).getStyle().setDisplay("block");
} else {
((Element)getExpandControl().getChildNodes().item(0)).getStyle().setDisplay("block");
((Element)getExpandControl().getChildNodes().item(1)).getStyle().setDisplay("none");
}
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()) {
DomUtils.removeFromParent(getChildrenContainer());
}
}
/**
* Removes this node from the {@link Tree} and breaks the back reference from
* the underlying node data.
*/
public final void removeFromTree() {
DomUtils.removeFromParent(this);
}
// /** Sets whether or not this node has the active node styling applied. */
// public final void setActive(boolean isActive, Tree.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 selected, boolean active, Tree.Css css) {
Elements.removeClassName(css.selected(), getSelectionElement());
Elements.removeClassName(css.selectedInactive(), getSelectionElement());
if (selected) {
Elements.addClassName(active ? css.selected() : css.selectedInactive(), getSelectionElement());
getSelectionElement().setAttribute("selected", "true");
} else {
getSelectionElement().removeAttribute("selected");
}
}
/** 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);
((Element)getExpandControl().getChildNodes().item(0)).getStyle().setDisplay("block");
((Element)getExpandControl().getChildNodes().item(1)).getStyle().setDisplay("none");
} 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 expand 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;
}-*/;
}