blob: 2d93044cc7300a7e5c670f26e74404a536b9c418 [file] [log] [blame]
/*
* Copyright 2008 Google Inc.
*
* 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 com.google.gwt.user.client.ui;
import com.google.gwt.animation.client.Animation;
import com.google.gwt.aria.client.Roles;
import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.i18n.client.LocaleInfo;
import com.google.gwt.safehtml.client.HasSafeHtml;
import com.google.gwt.safehtml.shared.SafeHtml;
import com.google.gwt.safehtml.shared.annotations.IsSafeHtml;
import com.google.gwt.user.client.DOM;
import java.util.ArrayList;
import java.util.List;
/**
* An item that can be contained within a
* {@link com.google.gwt.user.client.ui.Tree}.
*
* Each tree item is assigned a unique DOM id in order to support ARIA. See
* {@link com.google.gwt.user.client.ui.Accessibility} for more information.
*
* <p>
* <h3>Example</h3>
* {@example com.google.gwt.examples.TreeExample}
* </p>
*/
public class TreeItem extends UIObject implements IsTreeItem, HasTreeItems,
HasHTML, HasSafeHtml {
/*
* For compatibility with UiBinder interface HasTreeItems should be declared
* before HasHTML, so that children items and widgets are processed before
* interpreting HTML.
*/
/**
* The margin applied to child items.
*/
private static final double CHILD_MARGIN = 16.0;
/**
* Implementation class for {@link TreeItem}.
*/
public static class TreeItemImpl {
public TreeItemImpl() {
initializeClonableElements();
}
void convertToFullNode(TreeItem item) {
if (item.imageHolder == null) {
// Extract the Elements from the object
Element itemTable = DOM.clone(BASE_INTERNAL_ELEM, true);
DOM.appendChild(item.getElement(), itemTable);
Element tr = DOM.getFirstChild(DOM.getFirstChild(itemTable));
Element tdImg = DOM.getFirstChild(tr);
Element tdContent = DOM.getNextSibling(tdImg);
// Undoes padding from table element.
item.getElement().getStyle().setProperty("padding", "0px");
DOM.appendChild(tdContent, item.contentElem);
item.imageHolder = tdImg;
}
}
/**
* Setup clonable elements.
*/
void initializeClonableElements() {
if (GWT.isClient()) {
// Create the base table element that will be cloned.
BASE_INTERNAL_ELEM = DOM.createTable();
Element contentElem = DOM.createDiv();
Element tbody = DOM.createTBody(), tr = DOM.createTR();
Element tdImg = DOM.createTD(), tdContent = DOM.createTD();
DOM.appendChild(BASE_INTERNAL_ELEM, tbody);
DOM.appendChild(tbody, tr);
DOM.appendChild(tr, tdImg);
DOM.appendChild(tr, tdContent);
tdImg.getStyle().setProperty("verticalAlign", "middle");
tdContent.getStyle().setProperty("verticalAlign", "middle");
DOM.appendChild(tdContent, contentElem);
contentElem.getStyle().setProperty("display", "inline");
setStyleName(contentElem, "gwt-TreeItem");
BASE_INTERNAL_ELEM.getStyle().setProperty("whiteSpace", "nowrap");
// Create the base element that will be cloned
BASE_BARE_ELEM = DOM.createDiv();
// Simulates padding from table element.
BASE_BARE_ELEM.getStyle().setProperty("padding", "3px");
DOM.appendChild(BASE_BARE_ELEM, contentElem);
Roles.getTreeitemRole().set(contentElem);
}
}
}
/**
* IE specific implementation class for {@link TreeItem}.
*/
public static class TreeItemImplIE8ToIE10 extends TreeItemImpl {
@Override
void convertToFullNode(TreeItem item) {
super.convertToFullNode(item);
item.getElement().getStyle().setProperty("marginBottom", "0px");
}
}
/**
* An {@link Animation} used to open the child elements. If a {@link TreeItem}
* is in the process of opening, it will immediately be opened and the new
* {@link TreeItem} will use this animation.
*/
private static class TreeItemAnimation extends Animation {
/**
* The {@link TreeItem} currently being affected.
*/
private TreeItem curItem = null;
/**
* Whether the item is being opened or closed.
*/
private boolean opening = true;
/**
* The target height of the child items.
*/
private int scrollHeight = 0;
/**
* Open the specified {@link TreeItem}.
*
* @param item the {@link TreeItem} to open
* @param animate true to animate, false to open instantly
*/
public void setItemState(TreeItem item, boolean animate) {
// Immediately complete previous open
cancel();
// Open the new item
if (animate) {
curItem = item;
opening = item.open;
run(Math.min(ANIMATION_DURATION, ANIMATION_DURATION_PER_ITEM
* curItem.getChildCount()));
} else {
UIObject.setVisible(item.childSpanElem, item.open);
}
}
@Override
protected void onComplete() {
if (curItem != null) {
if (opening) {
UIObject.setVisible(curItem.childSpanElem, true);
onUpdate(1.0);
curItem.childSpanElem.getStyle().setProperty("height", "auto");
} else {
UIObject.setVisible(curItem.childSpanElem, false);
}
curItem.childSpanElem.getStyle().setProperty("overflow", "visible");
curItem.childSpanElem.getStyle().setProperty("width", "auto");
curItem = null;
}
}
@Override
protected void onStart() {
scrollHeight = 0;
// If the TreeItem is already open, we can get its scrollHeight
// immediately.
if (!opening) {
scrollHeight = curItem.childSpanElem.getScrollHeight();
}
curItem.childSpanElem.getStyle().setProperty("overflow", "hidden");
// If the TreeItem is already open, onStart will set its height to its
// natural height. If the TreeItem is currently closed, onStart will set
// its height to 1px (see onUpdate below), and then we make the TreeItem
// visible so we can get its correct scrollHeight.
super.onStart();
// If the TreeItem is currently closed, we need to make it visible before
// we can get its height.
if (opening) {
UIObject.setVisible(curItem.childSpanElem, true);
scrollHeight = curItem.childSpanElem.getScrollHeight();
}
}
@Override
protected void onUpdate(double progress) {
int height = (int) (progress * scrollHeight);
if (!opening) {
height = scrollHeight - height;
}
// Issue 2338: If the height is 0px, IE7 will display all of the children
// instead of hiding them completely.
height = Math.max(height, 1);
curItem.childSpanElem.getStyle().setProperty("height", height + "px");
// We need to set the width explicitly of the item might be cropped
int scrollWidth = curItem.childSpanElem.getPropertyInt("scrollWidth");
curItem.childSpanElem.getStyle().setProperty("width", scrollWidth + "px");
}
}
// By not overwriting the default tree padding and spacing, we traditionally
// added 7 pixels between our image and content.
// <2>|<1>image<1>|<2>|<1>content
// So to preserve the current spacing we must add a 7 pixel pad when no image
// is supplied.
static final int IMAGE_PAD = 7;
/**
* The duration of the animation.
*/
private static final int ANIMATION_DURATION = 200;
/**
* The duration of the animation per child {@link TreeItem}. If the per item
* duration times the number of child items is less than the duration above,
* the smaller duration will be used.
*/
private static final int ANIMATION_DURATION_PER_ITEM = 75;
/**
* The static animation used to open {@link TreeItem TreeItems}.
*/
private static TreeItemAnimation itemAnimation = new TreeItemAnimation();
/**
* The structured table to hold images.
*/
private static Element BASE_INTERNAL_ELEM;
/**
* The base tree item element that will be cloned.
*/
private static Element BASE_BARE_ELEM;
private static TreeItemImpl impl = GWT.create(TreeItemImpl.class);
private ArrayList<TreeItem> children;
private Element contentElem, childSpanElem, imageHolder;
/**
* Indicates that this item is a root item in a tree.
*/
private boolean isRoot;
private boolean open;
private TreeItem parent;
private boolean selected;
private Object userObject;
private Tree tree;
private Widget widget;
/**
* Creates an empty tree item.
*/
public TreeItem() {
this(false);
}
/**
* Constructs a tree item with the given HTML.
*
* @param html the item's HTML
*/
public TreeItem(SafeHtml html) {
this();
setHTML(html);
}
/**
* Constructs a tree item with the given <code>Widget</code>.
*
* @param widget the item's widget
*/
public TreeItem(Widget widget) {
this();
setWidget(widget);
}
/**
* Creates an empty tree item.
*
* @param isRoot true if this item is the root of a tree
*/
TreeItem(boolean isRoot) {
this.isRoot = isRoot;
Element elem = DOM.clone(BASE_BARE_ELEM, true);
setElement(elem);
contentElem = DOM.getFirstChild(elem);
contentElem.setAttribute("id", DOM.createUniqueId());
// The root item always has children.
if (isRoot) {
initChildren();
}
}
/**
* Adds a child tree item containing the specified html.
*
* @param itemHtml the item's HTML
* @return the item that was added
*/
@Override
public TreeItem addItem(SafeHtml itemHtml) {
TreeItem ret = new TreeItem(itemHtml);
addItem(ret);
return ret;
}
/**
* Adds another item as a child to this one.
*
* @param item the item to be added
*/
@Override
public void addItem(TreeItem item) {
// If this is the item's parent, removing the item will affect the child
// count.
maybeRemoveItemFromParent(item);
insertItem(getChildCount(), item);
}
/**
* Adds another item as a child to this one.
*
* @param isItem the wrapper of item to be added
*/
@Override
public void addItem(IsTreeItem isItem) {
TreeItem item = isItem.asTreeItem();
addItem(item);
}
/**
* Adds a child tree item containing the specified widget.
*
* @param widget the widget to be added
* @return the item that was added
*/
@Override
public TreeItem addItem(Widget widget) {
TreeItem ret = new TreeItem(widget);
addItem(ret);
return ret;
}
/**
* Adds a child tree item containing the specified text.
*
* @param itemText the text of the item to be added
* @return the item that was added
*/
@Override
public TreeItem addTextItem(String itemText) {
TreeItem ret = new TreeItem();
ret.setText(itemText);
addItem(ret);
return ret;
}
@Override
public TreeItem asTreeItem() {
return this;
}
/**
* Gets the child at the specified index.
*
* @param index the index to be retrieved
* @return the item at that index
*/
public TreeItem getChild(int index) {
if ((index < 0) || (index >= getChildCount())) {
return null;
}
return children.get(index);
}
/**
* Gets the number of children contained in this item.
*
* @return this item's child count.
*/
public int getChildCount() {
if (children == null) {
return 0;
}
return children.size();
}
/**
* Gets the index of the specified child item.
*
* @param child the child item to be found
* @return the child's index, or <code>-1</code> if none is found
*/
public int getChildIndex(TreeItem child) {
if (children == null) {
return -1;
}
return children.indexOf(child);
}
@Override
public String getHTML() {
return contentElem.getInnerHTML();
}
/**
* Gets this item's parent.
*
* @return the parent item
*/
public TreeItem getParentItem() {
return parent;
}
/**
* Gets whether this item's children are displayed.
*
* @return <code>true</code> if the item is open
*/
public boolean getState() {
return open;
}
@Override
public String getText() {
return contentElem.getInnerText();
}
/**
* Gets the tree that contains this item.
*
* @return the containing tree
*/
public final Tree getTree() {
return tree;
}
/**
* Gets the user-defined object associated with this item.
*
* @return the item's user-defined object
*/
public Object getUserObject() {
return userObject;
}
/**
* Gets the <code>Widget</code> associated with this tree item.
*
* @return the widget
*/
public Widget getWidget() {
return widget;
}
@Override
public String getTitle() {
return contentElem.getTitle();
}
@Override
public void setTitle(String title) {
contentElem.setTitle(title);
}
/**
* Inserts a child tree item at the specified index containing the specified
* html.
*
* @param beforeIndex the index where the item will be inserted
* @param itemHtml the item's HTML
* @return the item that was added
* @throws IndexOutOfBoundsException if the index is out of range
*/
public TreeItem insertItem(int beforeIndex, SafeHtml itemHtml)
throws IndexOutOfBoundsException {
TreeItem ret = new TreeItem(itemHtml);
insertItem(beforeIndex, ret);
return ret;
}
/**
* Inserts an item as a child to this one.
*
* @param beforeIndex the index where the item will be inserted
* @param item the item to be added
* @throws IndexOutOfBoundsException if the index is out of range
*/
public void insertItem(int beforeIndex, TreeItem item)
throws IndexOutOfBoundsException {
// Detach item from existing parent.
maybeRemoveItemFromParent(item);
// Check the index after detaching in case this item was already the parent.
int childCount = getChildCount();
if (beforeIndex < 0 || beforeIndex > childCount) {
throw new IndexOutOfBoundsException();
}
if (children == null) {
initChildren();
}
// Set the margin.
// Use no margin on top-most items.
double margin = isRoot ? 0.0 : CHILD_MARGIN;
if (LocaleInfo.getCurrentLocale().isRTL()) {
item.getElement().getStyle().setMarginRight(margin, Unit.PX);
} else {
item.getElement().getStyle().setMarginLeft(margin, Unit.PX);
}
// Physical attach.
Element childContainer = isRoot ? tree.getElement() : childSpanElem;
if (beforeIndex == childCount) {
childContainer.appendChild(item.getElement());
} else {
Element beforeElem = getChild(beforeIndex).getElement();
childContainer.insertBefore(item.getElement(), beforeElem);
}
// Logical attach.
// Explicitly set top-level items' parents to null if this is root.
item.setParentItem(isRoot ? null : this);
children.add(beforeIndex, item);
// Adopt.
item.setTree(tree);
if (!isRoot && children.size() == 1) {
updateState(false, false);
}
}
/**
* Inserts a child tree item at the specified index containing the specified
* widget.
*
* @param beforeIndex the index where the item will be inserted
* @param widget the widget to be added
* @return the item that was added
* @throws IndexOutOfBoundsException if the index is out of range
*/
public TreeItem insertItem(int beforeIndex, Widget widget)
throws IndexOutOfBoundsException {
TreeItem ret = new TreeItem(widget);
insertItem(beforeIndex, ret);
return ret;
}
/**
* Inserts a child tree item at the specified index containing the specified
* text.
*
* @param beforeIndex the index where the item will be inserted
* @param itemText the item's text
* @return the item that was added
* @throws IndexOutOfBoundsException if the index is out of range
*/
public TreeItem insertTextItem(int beforeIndex, String itemText) {
TreeItem ret = new TreeItem();
ret.setText(itemText);
insertItem(beforeIndex, ret);
return ret;
}
/**
* Determines whether this item is currently selected.
*
* @return <code>true</code> if it is selected
*/
public boolean isSelected() {
return selected;
}
/**
* Removes this item from its tree.
*/
public void remove() {
if (parent != null) {
// If this item has a parent, remove self from it.
parent.removeItem(this);
} else if (tree != null) {
// If the item has no parent, but is in the Tree, it must be a top-level
// element.
tree.removeItem(this);
}
}
/**
* Removes one of this item's children.
*
* @param item the item to be removed
*/
@Override
public void removeItem(TreeItem item) {
// Validate.
if (children == null || !children.contains(item)) {
return;
}
// Orphan.
Tree oldTree = tree;
item.setTree(null);
// Physical detach.
if (isRoot) {
oldTree.getElement().removeChild(item.getElement());
} else {
childSpanElem.removeChild(item.getElement());
}
// Logical detach.
item.setParentItem(null);
children.remove(item);
if (!isRoot && children.size() == 0) {
updateState(false, false);
}
}
/**
* Removes one of this item's children.
*
* @param isItem the wrapper of item to be removed
*/
@Override
public void removeItem(IsTreeItem isItem) {
if (isItem != null) {
TreeItem item = isItem.asTreeItem();
removeItem(item);
}
}
/**
* Removes all of this item's children.
*/
@Override
public void removeItems() {
while (getChildCount() > 0) {
removeItem(getChild(0));
}
}
@Override
public void setHTML(@IsSafeHtml String html) {
setWidget(null);
contentElem.setInnerHTML(html);
}
@Override
public void setHTML(SafeHtml html) {
setHTML(html.asString());
}
/**
* Selects or deselects this item.
*
* @param selected <code>true</code> to select the item, <code>false</code> to
* deselect it
*/
public void setSelected(boolean selected) {
if (this.selected == selected) {
return;
}
this.selected = selected;
setStyleName(getContentElem(), "gwt-TreeItem-selected", selected);
}
/**
* Sets whether this item's children are displayed.
*
* @param open whether the item is open
*/
public void setState(boolean open) {
setState(open, true);
}
/**
* Sets whether this item's children are displayed.
*
* @param open whether the item is open
* @param fireEvents <code>true</code> to allow open/close events to be
*/
public void setState(boolean open, boolean fireEvents) {
if (open && getChildCount() == 0) {
return;
}
// Only do the physical update if it changes
if (this.open != open) {
this.open = open;
updateState(true, true);
if (fireEvents && tree != null) {
tree.fireStateChanged(this, open);
}
}
}
@Override
public void setText(String text) {
setWidget(null);
contentElem.setInnerText(text);
}
/**
* Sets the user-defined object associated with this item.
*
* @param userObj the item's user-defined object
*/
public void setUserObject(Object userObj) {
userObject = userObj;
}
/**
* Sets the current widget. Any existing child widget will be removed.
*
* @param newWidget Widget to set
*/
public void setWidget(Widget newWidget) {
// Detach new child from old parent.
if (newWidget != null) {
newWidget.removeFromParent();
}
// Detach old child from tree.
if (widget != null) {
try {
if (tree != null) {
tree.orphan(widget);
}
} finally {
// Physical detach old child.
contentElem.removeChild(widget.getElement());
widget = null;
}
}
// Clear out any existing content before adding a widget.
contentElem.setInnerHTML("");
// Logical detach old/attach new.
widget = newWidget;
if (newWidget != null) {
// Physical attach new.
DOM.appendChild(contentElem, newWidget.getElement());
// Attach child to tree.
if (tree != null) {
tree.adopt(widget, this);
}
// Set tabIndex on the widget to -1, so that it doesn't mess up the tab
// order of the entire tree
if (Tree.shouldTreeDelegateFocusToElement(widget.getElement())) {
widget.getElement().setAttribute("tabIndex", "-1");
}
}
}
/**
* Returns a suggested {@link Focusable} instance to use when this tree item
* is selected. The tree maintains focus if this method returns null. By
* default, if the tree item contains a focusable widget, that widget is
* returned.
*
* Note, the {@link Tree} will ignore this value if the user clicked on an
* input element such as a button or text area when selecting this item.
*
* @return the focusable item
*/
protected Focusable getFocusable() {
Focusable focus = getFocusableWidget();
if (focus == null) {
Widget w = getWidget();
if (w instanceof Focusable) {
focus = (Focusable) w;
}
}
return focus;
}
/**
* Returns the widget, if any, that should be focused on if this TreeItem is
* selected.
*
* @return widget to be focused.
* @deprecated use {@link #getFocusable()} instead
*/
@Deprecated
protected HasFocus getFocusableWidget() {
Widget w = getWidget();
if (w instanceof HasFocus) {
return (HasFocus) w;
} else {
return null;
}
}
/**
* <b>Affected Elements:</b>
* <ul>
* <li>-content = The text or {@link Widget} next to the image.</li>
* <li>-child# = The child at the specified index.</li>
* </ul>
*
* @see UIObject#onEnsureDebugId(String)
*/
@Override
protected void onEnsureDebugId(String baseID) {
super.onEnsureDebugId(baseID);
ensureDebugId(contentElem, baseID, "content");
if (imageHolder != null) {
// The image itself may or may not exist.
ensureDebugId(imageHolder, baseID, "image");
}
if (children != null) {
int childCount = 0;
for (TreeItem child : children) {
child.ensureDebugId(baseID + "-child" + childCount);
childCount++;
}
}
}
void addTreeItems(List<TreeItem> accum) {
int size = getChildCount();
for (int i = 0; i < size; i++) {
TreeItem item = children.get(i);
accum.add(item);
item.addTreeItems(accum);
}
}
ArrayList<TreeItem> getChildren() {
return children;
}
Element getContentElem() {
return contentElem;
}
Element getImageElement() {
return DOM.getFirstChild(getImageHolderElement());
}
Element getImageHolderElement() {
if (!isFullNode()) {
convertToFullNode();
}
return imageHolder;
}
void initChildren() {
convertToFullNode();
childSpanElem = DOM.createDiv();
DOM.appendChild(getElement(), childSpanElem);
childSpanElem.getStyle().setProperty("whiteSpace", "nowrap");
children = new ArrayList<TreeItem>();
}
boolean isFullNode() {
return imageHolder != null;
}
/**
* Remove a tree item from its parent if it has one.
*
* @param item the tree item to remove from its parent
*/
void maybeRemoveItemFromParent(TreeItem item) {
if ((item.getParentItem() != null) || (item.getTree() != null)) {
item.remove();
}
}
void setParentItem(TreeItem parent) {
this.parent = parent;
}
void setTree(Tree newTree) {
// Early out.
if (tree == newTree) {
return;
}
// Remove this item from existing tree.
if (tree != null) {
if (tree.getSelectedItem() == this) {
tree.setSelectedItem(null);
}
if (widget != null) {
tree.orphan(widget);
}
}
tree = newTree;
for (int i = 0, n = getChildCount(); i < n; ++i) {
children.get(i).setTree(newTree);
}
updateState(false, true);
if (newTree != null) {
if (widget != null) {
// Add my widget to the new tree.
newTree.adopt(widget, this);
}
}
}
void updateState(boolean animate, boolean updateTreeSelection) {
// If the tree hasn't been set, there is no visual state to update.
// If the tree is not attached, then update will be called on attach.
if (tree == null || tree.isAttached() == false) {
return;
}
if (getChildCount() == 0) {
if (childSpanElem != null) {
UIObject.setVisible(childSpanElem, false);
}
tree.showLeafImage(this);
return;
}
// We must use 'display' rather than 'visibility' here,
// or the children will always take up space.
if (animate && (tree != null) && (tree.isAttached())) {
itemAnimation.setItemState(this, tree.isAnimationEnabled());
} else {
itemAnimation.setItemState(this, false);
}
// Change the status image
if (open) {
tree.showOpenImage(this);
} else {
tree.showClosedImage(this);
}
// We may need to update the tree's selection in response to a tree state
// change. For example, if the tree's currently selected item is a
// descendant of an item whose branch was just collapsed, then the item
// itself should become the newly-selected item.
if (updateTreeSelection) {
tree.maybeUpdateSelection(this, this.open);
}
}
void updateStateRecursive() {
updateStateRecursiveHelper();
tree.maybeUpdateSelection(this, this.open);
}
private void convertToFullNode() {
impl.convertToFullNode(this);
}
private void updateStateRecursiveHelper() {
updateState(false, false);
for (int i = 0, n = getChildCount(); i < n; ++i) {
children.get(i).updateStateRecursiveHelper();
}
}
}