blob: 878d80a9162d3e6543fb85f75b35dbc45389023b [file] [log] [blame]
/*
* Copyright 2010 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.cellview.client;
import com.google.gwt.aria.client.ExpandedValue;
import com.google.gwt.aria.client.Roles;
import com.google.gwt.cell.client.Cell;
import com.google.gwt.cell.client.Cell.Context;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.dom.client.AnchorElement;
import com.google.gwt.dom.client.BrowserEvents;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.dom.client.Style.Display;
import com.google.gwt.dom.client.Style.Overflow;
import com.google.gwt.event.logical.shared.CloseEvent;
import com.google.gwt.event.logical.shared.OpenEvent;
import com.google.gwt.event.shared.EventHandler;
import com.google.gwt.event.shared.GwtEvent;
import com.google.gwt.event.shared.GwtEvent.Type;
import com.google.gwt.event.shared.HandlerManager;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.i18n.client.LocaleInfo;
import com.google.gwt.safecss.shared.SafeStyles;
import com.google.gwt.safecss.shared.SafeStylesUtils;
import com.google.gwt.safehtml.client.SafeHtmlTemplates;
import com.google.gwt.safehtml.shared.SafeHtml;
import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
import com.google.gwt.safehtml.shared.SafeHtmlUtils;
import com.google.gwt.user.cellview.client.CellTree.CellTreeMessages;
import com.google.gwt.user.cellview.client.HasKeyboardSelectionPolicy.KeyboardSelectionPolicy;
import com.google.gwt.user.cellview.client.LoadingStateChangeEvent.LoadingState;
import com.google.gwt.user.client.ui.UIObject;
import com.google.gwt.user.client.ui.impl.FocusImpl;
import com.google.gwt.view.client.CellPreviewEvent;
import com.google.gwt.view.client.CellPreviewEvent.Handler;
import com.google.gwt.view.client.HasData;
import com.google.gwt.view.client.ProvidesKey;
import com.google.gwt.view.client.Range;
import com.google.gwt.view.client.RangeChangeEvent;
import com.google.gwt.view.client.RowCountChangeEvent;
import com.google.gwt.view.client.SelectionModel;
import com.google.gwt.view.client.TreeViewModel;
import com.google.gwt.view.client.TreeViewModel.NodeInfo;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* A view of a tree node.
*
* @param <T> the type that this view contains
*/
// TODO(jlabanca): Convert this to be the type of the child and create lazily.
class CellTreeNodeView<T> extends UIObject {
interface Template extends SafeHtmlTemplates {
@Template("<div style=\"{0}position:relative;\""
+ " class=\"{1}\">{2}<div class=\"{3}\">{4}</div></div>")
SafeHtml innerDiv(SafeStyles cssString, String classes, SafeHtml image, String itemValueStyle,
SafeHtml cellContents);
@Template("<div aria-selected=\"{3}\">"
+ "<div style=\"{0}\" class=\"{1}\">{2}</div></div>")
SafeHtml outerDiv(SafeStyles cssString, String classes, SafeHtml content, String ariaSelected);
}
/**
* The {@link com.google.gwt.view.client.HasData} used to show children. This
* class is intentionally static because we might move it to a new
* {@link CellTreeNodeView}, and we don't want non-static references to the
* old {@link CellTreeNodeView}.
*
* @param <C> the child item type
*/
static class NodeCellList<C> implements HasData<C> {
/**
* The view used by the NodeCellList.
*/
private class View implements HasDataPresenter.View<C> {
private final Element childContainer;
public View(Element childContainer) {
this.childContainer = childContainer;
}
@Override
public <H extends EventHandler> HandlerRegistration addHandler(H handler, Type<H> type) {
return handlerManger.addHandler(type, handler);
}
public void render(SafeHtmlBuilder sb, List<C> values, int start,
SelectionModel<? super C> selectionModel) {
// Cache the style names that will be used for each child.
CellTree.Style style = nodeView.tree.getStyle();
String itemValueStyle = style.cellTreeItemValue();
String selectedStyle = " " + style.cellTreeSelectedItem();
String itemStyle = style.cellTreeItem();
String itemImageValueStyle = " " + style.cellTreeItemImageValue();
String openStyle = " " + style.cellTreeOpenItem();
String topStyle = " " + style.cellTreeTopItem();
String topImageValueStyle = " " + style.cellTreeTopItemImageValue();
boolean isRootNode = nodeView.isRootNode();
SafeHtml openImage = nodeView.tree.getOpenImageHtml(isRootNode);
SafeHtml closedImage = nodeView.tree.getClosedImageHtml(isRootNode);
int imageWidth = nodeView.tree.getImageWidth();
String paddingDirection = LocaleInfo.getCurrentLocale().isRTL() ? "right" : "left";
int paddingAmount = imageWidth * nodeView.depth;
// Create a set of currently open nodes.
Set<Object> openNodes = new HashSet<Object>();
int childCount = nodeView.getChildCount();
int end = start + values.size();
for (int i = start; i < end && i < childCount; i++) {
CellTreeNodeView<?> child = nodeView.getChildNode(i);
// Ignore child nodes that are closed.
if (child.isOpen()) {
openNodes.add(child.getValueKey());
}
}
// Render the child nodes.
ProvidesKey<C> keyProvider = nodeInfo.getProvidesKey();
TreeViewModel model = nodeView.tree.getTreeViewModel();
for (int i = start; i < end; i++) {
C value = values.get(i - start);
Object key = keyProvider.getKey(value);
boolean isOpen = openNodes.contains(key);
// Outer div contains image, value, and children (when open)
StringBuilder outerClasses = new StringBuilder(itemStyle);
if (isOpen) {
outerClasses.append(openStyle);
}
if (isRootNode) {
outerClasses.append(topStyle);
}
boolean isSelected = (selectionModel != null && selectionModel.isSelected(value));
String ariaSelected = String.valueOf(isSelected);
if (isSelected) {
outerClasses.append(selectedStyle);
}
// Inner div contains image and value
StringBuilder innerClasses = new StringBuilder(itemStyle);
innerClasses.append(itemImageValueStyle);
if (isRootNode) {
innerClasses.append(topImageValueStyle);
}
// Add the open/close icon.
SafeHtml image;
if (isOpen) {
image = openImage;
} else if (model.isLeaf(value)) {
image = LEAF_IMAGE;
} else {
image = closedImage;
}
// Render cell contents
SafeHtmlBuilder cellBuilder = new SafeHtmlBuilder();
Context context = new Context(i, 0, key);
cell.render(context, value, cellBuilder);
SafeStyles innerPadding =
SafeStylesUtils.fromTrustedString("padding-" + paddingDirection + ": " + imageWidth
+ "px;");
SafeHtml innerDiv =
template.innerDiv(innerPadding, innerClasses.toString(), image, itemValueStyle,
cellBuilder.toSafeHtml());
SafeStyles outerPadding =
SafeStylesUtils.fromTrustedString("padding-" + paddingDirection + ": "
+ paddingAmount + "px;");
sb.append(template.outerDiv(outerPadding, outerClasses.toString(), innerDiv,
ariaSelected));
}
}
@Override
public void replaceAllChildren(List<C> values, SelectionModel<? super C> selectionModel,
boolean stealFocus) {
// Render the children.
SafeHtmlBuilder sb = new SafeHtmlBuilder();
render(sb, values, 0, selectionModel);
// Hide the child container so we can animate it.
if (nodeView.tree.isAnimationEnabled()) {
nodeView.ensureAnimationFrame().getStyle().setDisplay(Display.NONE);
}
// Replace the child nodes.
nodeView.tree.isRefreshing = true;
Map<Object, CellTreeNodeView<?>> savedViews = saveChildState(values, 0);
AbstractHasData.replaceAllChildren(nodeView.tree, childContainer, sb.toSafeHtml());
nodeView.tree.isRefreshing = false;
// Trim the list of children.
int size = values.size();
int childCount = nodeView.children.size();
while (childCount > size) {
childCount--;
CellTreeNodeView<?> deleted = nodeView.children.remove(childCount);
deleted.cleanup(true);
}
// Reattach the open nodes.
loadChildState(values, 0, savedViews);
// If this is the root node, move keyboard focus to the first child.
if (nodeView.isRootNode() && nodeView.tree.getKeyboardSelectedNode() == nodeView
&& values.size() > 0) {
nodeView.tree.keyboardSelect(nodeView.children.get(0), false);
}
// Animate the child container open.
if (nodeView.tree.isAnimationEnabled()) {
nodeView.tree.maybeAnimateTreeNode(nodeView);
}
}
@Override
public void replaceChildren(List<C> values, int start,
SelectionModel<? super C> selectionModel, boolean stealFocus) {
// Render the children.
SafeHtmlBuilder sb = new SafeHtmlBuilder();
render(sb, values, 0, selectionModel);
Map<Object, CellTreeNodeView<?>> savedViews = saveChildState(values, start);
nodeView.tree.isRefreshing = true;
SafeHtml html = sb.toSafeHtml();
Element newChildren = AbstractHasData.convertToElements(nodeView.tree, getTmpElem(), html);
AbstractHasData
.replaceChildren(nodeView.tree, childContainer, newChildren, start, html);
nodeView.tree.isRefreshing = false;
loadChildState(values, start, savedViews);
}
@Override
public void resetFocus() {
nodeView.tree.resetFocus();
}
@Override
public void setKeyboardSelected(int index, boolean selected, boolean stealFocus) {
// Keyboard selection is handled by CellTree.
Element elem = childContainer.getChild(index).cast();
setStyleName(getSelectionElement(elem), nodeView.tree.getStyle()
.cellTreeKeyboardSelectedItem(), selected);
}
@Override
public void setLoadingState(LoadingState state) {
nodeView.updateImage(state == LoadingState.LOADING);
showOrHide(nodeView.emptyMessageElem, state == LoadingState.LOADED && presenter.isEmpty());
}
/**
* Reload the open children after rendering new items in this node.
*
* @param values the values being replaced
* @param start the start index
* @param savedViews the open nodes
*/
private void loadChildState(List<C> values, int start,
Map<Object, CellTreeNodeView<?>> savedViews) {
int len = values.size();
int end = start + len;
int childCount = nodeView.getChildCount();
int setSize = (childCount > len) ? childCount : end;
ProvidesKey<C> keyProvider = nodeInfo.getProvidesKey();
Element container = nodeView.ensureChildContainer();
Element childElem = (values.size() == 0) ? null : Element.as(container.getChild(start));
CellTreeNodeView<?> keyboardSelected = nodeView.tree.getKeyboardSelectedNode();
for (int i = start; i < end; i++) {
C childValue = values.get(i - start);
CellTreeNodeView<C> child =
nodeView.createTreeNodeView(nodeInfo, childElem, childValue, null);
CellTreeNodeView<?> savedChild = savedViews.remove(keyProvider.getKey(childValue));
// Copy the saved child's state into the new child
if (savedChild != null) {
child.animationFrame = savedChild.animationFrame;
child.contentContainer = savedChild.contentContainer;
child.childContainer = savedChild.childContainer;
child.children = savedChild.children;
child.emptyMessageElem = savedChild.emptyMessageElem;
child.nodeInfo = savedChild.nodeInfo;
child.nodeInfoLoaded = savedChild.nodeInfoLoaded;
child.open = savedChild.open;
child.showMoreElem = savedChild.showMoreElem;
// Transfer the tree node so that if the user has a handle to it, it
// won't be destroyed.
child.treeNode = savedChild.treeNode;
if (child.treeNode != null) {
child.treeNode.nodeView = child;
}
// Swap the node view in the child. We reuse the same NodeListView
// so that we don't have to unset and register a new view with the
// NodeInfo, which would inevitably cause the NodeInfo to push
// new data.
child.listView = savedChild.listView;
if (child.listView != null) {
child.listView.nodeView = child;
}
// Set the new parent of the grandchildren.
if (child.children != null) {
for (CellTreeNodeView<?> grandchild : child.children) {
grandchild.parentNode = child;
}
}
// Transfer the keyboard selected node.
if (keyboardSelected == savedChild) {
keyboardSelected = child;
}
// Copy the child container element to the new child
child.getElement().appendChild(savedChild.ensureAnimationFrame());
// Mark the old child as destroy without actually destroying it.
savedChild.isDestroyed = true;
}
if (childCount > i) {
nodeView.children.set(i, child);
} else {
nodeView.children.add(child);
}
child.updateAriaAttributes(setSize);
childElem = childElem.getNextSiblingElement();
}
// Move the keyboard selected node if it is this node or a child of this
// node.
CellTreeNodeView<?> curNode = keyboardSelected;
while (curNode != null) {
if (curNode == nodeView) {
nodeView.tree.keyboardSelect(keyboardSelected, false);
break;
}
curNode = curNode.parentNode;
}
}
/**
* Save the state of the open child nodes within the range of the
* specified values. Use {@link #loadChildState(List, int, Map)} to
* re-attach the open nodes after they have been replaced.
*
* @param values the values being replaced
* @param start the start index
* @return the map of open nodes
*/
private Map<Object, CellTreeNodeView<?>> saveChildState(List<C> values, int start) {
// Ensure that we have a children array.
if (nodeView.children == null) {
nodeView.children = new ArrayList<CellTreeNodeView<?>>();
}
// Construct a map of former child views based on their value keys.
int len = values.size();
int end = start + len;
int childCount = nodeView.getChildCount();
CellTreeNodeView<?> keyboardSelected = nodeView.tree.getKeyboardSelectedNode();
Map<Object, CellTreeNodeView<?>> openNodes = new HashMap<Object, CellTreeNodeView<?>>();
for (int i = start; i < end && i < childCount; i++) {
CellTreeNodeView<?> child = nodeView.getChildNode(i);
if (child.isOpen() || child == keyboardSelected) {
// Save child nodes that are open or keyboard selected.
openNodes.put(child.getValueKey(), child);
} else {
// Cleanup child nodes that are closed.
child.cleanup(true);
}
}
// Trim the saved views down to the children that still exists.
ProvidesKey<C> keyProvider = nodeInfo.getProvidesKey();
Map<Object, CellTreeNodeView<?>> savedViews = new HashMap<Object, CellTreeNodeView<?>>();
for (C childValue : values) {
// Remove any child elements that correspond to prior children
// so the call to setInnerHtml will not destroy them
Object key = keyProvider.getKey(childValue);
CellTreeNodeView<?> savedView = openNodes.remove(key);
if (savedView != null) {
savedView.ensureAnimationFrame().removeFromParent();
savedViews.put(key, savedView);
}
}
// Cleanup the remaining open nodes that are not in the new data set.
for (CellTreeNodeView<?> lostNode : openNodes.values()) {
lostNode.cleanup(true);
}
return savedViews;
}
}
final HasDataPresenter<C> presenter;
private final Cell<C> cell;
private final int defaultPageSize;
private HandlerManager handlerManger = new HandlerManager(this);
private final NodeInfo<C> nodeInfo;
private CellTreeNodeView<?> nodeView;
public NodeCellList(final NodeInfo<C> nodeInfo, final CellTreeNodeView<?> nodeView, int pageSize) {
this.defaultPageSize = pageSize;
this.nodeInfo = nodeInfo;
this.nodeView = nodeView;
cell = nodeInfo.getCell();
// Create a presenter.
presenter =
new HasDataPresenter<C>(this, new View(nodeView.ensureChildContainer()), pageSize,
nodeInfo.getProvidesKey());
// Disable keyboard selection because it is handled by CellTree.
presenter.setKeyboardSelectionPolicy(KeyboardSelectionPolicy.DISABLED);
// Use a pager to update buttons.
presenter.addRowCountChangeHandler(new RowCountChangeEvent.Handler() {
@Override
public void onRowCountChange(RowCountChangeEvent event) {
int rowCount = event.getNewRowCount();
boolean isExact = event.isNewRowCountExact();
int pageSize = getVisibleRange().getLength();
showOrHide(nodeView.showMoreElem, isExact && rowCount > pageSize);
}
});
}
@Override
public HandlerRegistration addCellPreviewHandler(Handler<C> handler) {
return presenter.addCellPreviewHandler(handler);
}
@Override
public HandlerRegistration addRangeChangeHandler(RangeChangeEvent.Handler handler) {
return presenter.addRangeChangeHandler(handler);
}
@Override
public HandlerRegistration addRowCountChangeHandler(RowCountChangeEvent.Handler handler) {
return presenter.addRowCountChangeHandler(handler);
}
/**
* Cleanup this node view.
*/
public void cleanup() {
presenter.clearSelectionModel();
}
@Override
public void fireEvent(GwtEvent<?> event) {
handlerManger.fireEvent(event);
}
public int getDefaultPageSize() {
return defaultPageSize;
}
@Override
public int getRowCount() {
return presenter.getRowCount();
}
@Override
public SelectionModel<? super C> getSelectionModel() {
return presenter.getSelectionModel();
}
@Override
public C getVisibleItem(int indexOnPage) {
return presenter.getVisibleItem(indexOnPage);
}
@Override
public int getVisibleItemCount() {
return presenter.getVisibleItemCount();
}
@Override
public List<C> getVisibleItems() {
return presenter.getVisibleItems();
}
@Override
public Range getVisibleRange() {
return presenter.getVisibleRange();
}
@Override
public boolean isRowCountExact() {
return presenter.isRowCountExact();
}
@Override
public final void setRowCount(int count) {
setRowCount(count, true);
}
@Override
public void setRowCount(int size, boolean isExact) {
presenter.setRowCount(size, isExact);
}
@Override
public void setRowData(int start, List<? extends C> values) {
presenter.setRowData(start, values);
}
@Override
public void setSelectionModel(final SelectionModel<? super C> selectionModel) {
presenter.setSelectionModel(selectionModel);
}
@Override
public final void setVisibleRange(int start, int length) {
setVisibleRange(new Range(start, length));
}
@Override
public void setVisibleRange(Range range) {
presenter.setVisibleRange(range);
}
@Override
public void setVisibleRangeAndClearData(Range range, boolean forceRangeChangeEvent) {
presenter.setVisibleRangeAndClearData(range, forceRangeChangeEvent);
}
}
/**
* An implementation of {@link TreeNode} that delegates to a
* {@link CellTreeNodeView}. This class is intentionally static because we
* might move it to a new {@link CellTreeNodeView}, and we don't want
* non-static references to the old {@link CellTreeNodeView}.
*/
static class TreeNodeImpl implements TreeNode {
private CellTreeNodeView<?> nodeView;
public TreeNodeImpl(CellTreeNodeView<?> nodeView) {
this.nodeView = nodeView;
}
@Override
public int getChildCount() {
assertNotDestroyed();
flush();
return nodeView.getChildCount();
}
@Override
public Object getChildValue(int index) {
assertNotDestroyed();
checkChildBounds(index);
flush();
return nodeView.getChildNode(index).value;
}
@Override
public int getIndex() {
assertNotDestroyed();
return (nodeView.parentNode == null) ? 0 : nodeView.parentNode.children.indexOf(nodeView);
}
final CellTreeNodeView<?> getNodeView() {
return nodeView;
}
@Override
public TreeNode getParent() {
assertNotDestroyed();
return getParentImpl();
}
@Override
public Object getValue() {
return nodeView.value;
}
@Override
public boolean isChildLeaf(int index) {
assertNotDestroyed();
checkChildBounds(index);
flush();
return nodeView.getChildNode(index).isLeaf();
}
@Override
public boolean isChildOpen(int index) {
assertNotDestroyed();
checkChildBounds(index);
flush();
return nodeView.getChildNode(index).isOpen();
}
@Override
public boolean isDestroyed() {
if (!nodeView.isDestroyed) {
/*
* Flush the parent display because the user may have replaced this
* node, which would destroy it.
*/
TreeNodeImpl parent = getParentImpl();
if (parent != null && !parent.isDestroyed()) {
parent.flush();
}
}
return nodeView.isDestroyed || !nodeView.isOpen();
}
@Override
public TreeNode setChildOpen(int index, boolean open) {
return setChildOpen(index, open, true);
}
@Override
public TreeNode setChildOpen(int index, boolean open, boolean fireEvents) {
assertNotDestroyed();
checkChildBounds(index);
CellTreeNodeView<?> child = nodeView.getChildNode(index);
return child.setOpen(open, fireEvents) ? child.treeNode : null;
}
/**
* Assert that the node has not been destroyed.
*/
private void assertNotDestroyed() {
if (isDestroyed()) {
throw new IllegalStateException("TreeNode no longer exists.");
}
}
/**
* Check the child bounds.
*
* @param index the index of the child
* @throws IndexOutOfBoundsException if the child is not in range
*/
private void checkChildBounds(int index) {
if ((index < 0) || (index >= getChildCount())) {
throw new IndexOutOfBoundsException();
}
}
/**
* Flush pending changes in the view.
*/
void flush() {
if (nodeView.listView != null) {
nodeView.listView.presenter.flush();
}
}
/**
* Get the parent node without checking if this node is destroyed.
*
* @return the parent node, or null if the node has no parent
*/
private TreeNodeImpl getParentImpl() {
return nodeView.isRootNode() ? null : nodeView.parentNode.treeNode;
}
}
/**
* The element used in place of an image when a node has no children.
*/
private static final SafeHtml LEAF_IMAGE = SafeHtmlUtils
.fromSafeConstant("<div style='position:absolute;display:none;'></div>");
private static final Template template = GWT.create(Template.class);
/**
* The temporary element used to render child items.
*/
private static Element tmpElem;
/**
* Returns the element that parents the cell contents of the node.
*
* @param nodeElem the element that represents the node
* @return the cell parent within the node
*/
private static Element getCellParent(Element nodeElem) {
return getSelectionElement(nodeElem).getFirstChildElement().getChild(1).cast();
}
/**
* Returns the element that selection is applied to.
*
* @param nodeElem the element that represents the node
* @return the cell parent within the node
*/
private static Element getImageElement(Element nodeElem) {
return getSelectionElement(nodeElem).getFirstChildElement().getFirstChildElement();
}
/**
* Returns the element that selection is applied to.
*
* @param nodeElem the element that represents the node
* @return the cell parent within the node
*/
static Element getSelectionElement(Element nodeElem) {
return nodeElem.getFirstChildElement();
}
/**
* Return the temporary element used to create elements.
*/
private static Element getTmpElem() {
if (tmpElem == null) {
tmpElem = Document.get().createDivElement();
}
return tmpElem;
}
/**
* Show or hide an element.
*
* @param element the element to show or hide
* @param show true to show, false to hide
*/
private static void showOrHide(Element element, boolean show) {
if (show) {
element.getStyle().clearDisplay();
} else {
element.getStyle().setDisplay(Display.NONE);
}
}
/**
* The list view used to display the nodes.
*/
NodeCellList<?> listView;
/**
* True during the time a node should be animated.
*/
private boolean animate;
/**
* A reference to the element that is used to animate nodes. Parent of the
* contentContainer.
*/
private Element animationFrame;
/**
* A reference to the element that contains the children. Parent to the actual
* child nodes.
*/
private Element childContainer;
/**
* A list of child views.
*/
private List<CellTreeNodeView<?>> children;
/**
* A reference to the element that contains all content. Parent of the
* childContainer and the show/hide elements.
*/
private Element contentContainer;
/**
* The depth of this node in the tree.
*/
private final int depth;
/**
* The element used when there are no children to display.
*/
private Element emptyMessageElem;
/**
* Set to true when the node is destroyed.
*/
private boolean isDestroyed;
/**
* Messages used for translation.
*/
private final CellTreeMessages messages;
/**
* The info about children of this node.
*/
private NodeInfo<?> nodeInfo;
/**
* Indicates whether or not we've loaded the node info.
*/
private boolean nodeInfoLoaded;
/**
* Indicates whether or not this node is open.
*/
private boolean open;
/**
* The parent {@link CellTreeNodeView}.
*/
private CellTreeNodeView<?> parentNode;
/**
* The {@link NodeInfo} of the parent node.
*/
private final NodeInfo<T> parentNodeInfo;
/**
* The element used to display more children.
*/
private AnchorElement showMoreElem;
/**
* The {@link CellTree} that this node belongs to.
*/
private final CellTree tree;
/**
* The publicly visible tree node. The {@link CellTreeNodeView} doesn't
* implement {@link TreeNode} directly because we want to transfer the user's
* handle to the {@link TreeNode} to the new {@link CellTreeNodeView}.
*/
private TreeNodeImpl treeNode;
/**
* This node's value.
*/
private T value;
/**
* Construct a {@link CellTreeNodeView}.
*
* @param tree the parent {@link CellTreeNodeView}
* @param parent the parent {@link CellTreeNodeView}
* @param parentNodeInfo the {@link NodeInfo} of the parent
* @param elem the outer element of this {@link CellTreeNodeView}
* @param value the value of this node
* @param messages translation messages
*/
CellTreeNodeView(final CellTree tree, final CellTreeNodeView<?> parent,
NodeInfo<T> parentNodeInfo, Element elem, T value, CellTreeMessages messages) {
this.tree = tree;
this.parentNode = parent;
this.parentNodeInfo = parentNodeInfo;
this.depth = parentNode == null ? 0 : parentNode.depth + 1;
this.value = value;
this.messages = messages;
setElement(elem);
Roles.getTreeitemRole().set(getElement());
}
public int getChildCount() {
return children == null ? 0 : children.size();
}
public CellTreeNodeView<?> getChildNode(int childIndex) {
return children.get(childIndex);
}
public boolean isLeaf() {
return tree.isLeaf(value);
}
/**
* Check whether or not this node is open.
*
* @return true if open, false if closed
*/
public boolean isOpen() {
return open;
}
/**
* Sets whether this item's children are displayed.
*
* @param open whether the item is open
* @param fireEvents true to fire events if the state changes
* @return true if successfully opened, false otherwise.
*/
public boolean setOpen(boolean open, boolean fireEvents) {
// Early out.
if (this.open == open) {
return this.open;
}
// If this node is a leaf node, do not call TreeViewModel.getNodeInfo().
if (open && isLeaf()) {
return false;
}
// The animation clears the innerHtml of the childContainer. If we reopen a
// node as its closing, it is possible that the new data will be set
// synchronously, so we have to cancel the animation before attaching the
// data display to the node info.
tree.cancelTreeNodeAnimation();
this.animate = true;
this.open = open;
if (open) {
if (!nodeInfoLoaded) {
nodeInfoLoaded = true;
nodeInfo = tree.getNodeInfo(value);
// Sink events for the new node.
if (nodeInfo != null) {
Set<String> eventsToSink = new HashSet<String>();
// Listen for focus and blur for keyboard navigation
eventsToSink.add(BrowserEvents.FOCUS);
eventsToSink.add(BrowserEvents.BLUR);
Set<String> consumedEvents = nodeInfo.getCell().getConsumedEvents();
if (consumedEvents != null) {
eventsToSink.addAll(consumedEvents);
}
CellBasedWidgetImpl.get().sinkEvents(tree, eventsToSink);
}
}
// If we don't have any nodeInfo, we must be a leaf node.
if (nodeInfo != null) {
// Add a loading message.
ensureChildContainer();
showOrHide(showMoreElem, false);
showOrHide(emptyMessageElem, false);
if (!isRootNode()) {
setStyleName(getCellParent(), tree.getStyle().cellTreeOpenItem(), true);
}
ensureAnimationFrame().getStyle().setProperty("display", "");
onOpen(nodeInfo);
// Fire an event.
if (fireEvents) {
OpenEvent.fire(tree, getTreeNode());
}
} else {
this.open = false;
}
} else {
if (!isRootNode()) {
setStyleName(getCellParent(), tree.getStyle().cellTreeOpenItem(), false);
}
cleanup(false);
tree.maybeAnimateTreeNode(this);
updateImage(false);
// Keyboard select this node if the open node was a child.
CellTreeNodeView<?> keySelected = tree.getKeyboardSelectedNode();
while (keySelected != null) {
if (keySelected == this) {
tree.keyboardSelect(this, true);
break;
}
keySelected = keySelected.getParentNode();
}
// Fire an event.
if (fireEvents) {
CloseEvent.fire(tree, getTreeNode());
}
}
return this.open;
}
/**
* Unregister the list handler and destroy all child nodes.
*
* @param destroy true to destroy this node
*/
protected void cleanup(boolean destroy) {
// Unregister the list handler.
if (listView != null) {
listView.cleanup();
nodeInfo.unsetDataDisplay();
listView = null;
}
// Recursively destroy children.
if (children != null) {
for (CellTreeNodeView<?> child : children) {
child.cleanup(true);
}
children = null;
}
// Destroy this node.
if (destroy) {
isDestroyed = true;
// If this is the keyboard selected node, select the parent. The children
// have already been cleaned, so the selected node cannot be under this
// node.
if (this == tree.getKeyboardSelectedNode()) {
tree.keyboardSelect(parentNode, false);
}
}
}
protected boolean consumeAnimate() {
boolean hasAnimate = animate;
animate = false;
return hasAnimate;
}
/**
* Returns an instance of TreeNodeView of the same subclass as the calling
* object.
*
* @param <C> the data type of the node's children
* @param nodeInfo a NodeInfo object describing the child nodes
* @param childElem the DOM element used to parent the new TreeNodeView
* @param childValue the child's value
* @param viewData view data associated with the node
* @return a TreeNodeView of suitable type
*/
protected <C> CellTreeNodeView<C> createTreeNodeView(NodeInfo<C> nodeInfo, Element childElem,
C childValue, Object viewData) {
return new CellTreeNodeView<C>(tree, this, nodeInfo, childElem, childValue, messages);
}
/**
* Fire an event to the {@link com.google.gwt.cell.client.AbstractCell}.
*
* @param event the native event
*/
@SuppressWarnings("unchecked")
protected void fireEventToCell(NativeEvent event) {
if (parentNodeInfo == null) {
return;
}
Cell<T> parentCell = parentNodeInfo.getCell();
String eventType = event.getType();
Element cellParent = getCellParent();
Object key = getValueKey();
Context context = new Context(getIndex(), 0, key);
boolean cellWasEditing = parentCell.isEditing(context, cellParent, value);
// Update selection.
boolean isSelectionHandled =
parentCell.handlesSelection()
|| KeyboardSelectionPolicy.BOUND_TO_SELECTION == tree.getKeyboardSelectionPolicy();
HasData<T> display = (HasData<T>) parentNode.listView;
CellPreviewEvent<T> previewEvent =
CellPreviewEvent.fire(display, event, display, context, value, cellWasEditing,
isSelectionHandled);
// Forward the event to the cell.
if (previewEvent.isCanceled() || !cellParent.isOrHasChild(Element.as(event.getEventTarget()))) {
return;
}
Set<String> consumedEvents = parentCell.getConsumedEvents();
if (consumedEvents != null && consumedEvents.contains(eventType)) {
parentCell
.onBrowserEvent(context, cellParent, value, event, parentNodeInfo.getValueUpdater());
tree.cellIsEditing = parentCell.isEditing(context, cellParent, value);
if (cellWasEditing && !tree.cellIsEditing) {
CellBasedWidgetImpl.get().resetFocus(new Scheduler.ScheduledCommand() {
@Override
public void execute() {
tree.setFocus(true);
}
});
}
}
}
/**
* Returns the element that parents the cell contents of this node.
*/
protected Element getCellParent() {
return getCellParent(getElement());
}
/**
* Returns the element corresponding to the open/close image.
*
* @return the open/close image element
*/
protected Element getImageElement() {
return getImageElement(getElement());
}
/**
* Returns the element that selection styles are applied to. The element
* includes the open/close image and the rendered value and spans the width of
* the tree.
*
* @return the selection element
*/
protected Element getSelectionElement() {
return getSelectionElement(getElement());
}
/**
* Returns the key for the value of this node using the parent's
* implementation of NodeInfo.getKey().
*/
protected Object getValueKey() {
return parentNodeInfo.getProvidesKey().getKey(value);
}
/**
* Set up the node when it is opened.
*
* @param nodeInfo the {@link NodeInfo} that provides information about the
* child values
* @param <C> the child data type of the node
*/
protected <C> void onOpen(final NodeInfo<C> nodeInfo) {
NodeCellList<C> view = new NodeCellList<C>(nodeInfo, this, tree.getDefaultNodeSize());
listView = view;
view.setSelectionModel(nodeInfo.getSelectionModel());
nodeInfo.setDataDisplay(view);
}
boolean belongsToTree(final CellTree tree) {
return this.tree == tree;
}
/**
* Ensure that the animation frame exists and return it.
*
* @return the animation frame
*/
Element ensureAnimationFrame() {
if (animationFrame == null) {
animationFrame = Document.get().createDivElement();
animationFrame.getStyle().setOverflow(Overflow.HIDDEN);
animationFrame.getStyle().setDisplay(Display.NONE);
getElement().appendChild(animationFrame);
}
return animationFrame;
}
/**
* Ensure that the child container exists and return it.
*
* @return the child container
*/
Element ensureChildContainer() {
if (childContainer == null) {
childContainer = Document.get().createDivElement();
ensureContentContainer().insertFirst(childContainer);
}
return childContainer;
}
/**
* Ensure that the content container exists and return it.
*
* @return the content container
*/
Element ensureContentContainer() {
if (contentContainer == null) {
contentContainer = Document.get().createDivElement();
ensureAnimationFrame().appendChild(contentContainer);
emptyMessageElem = Document.get().createDivElement();
emptyMessageElem.setInnerText(messages.emptyTree());
setStyleName(emptyMessageElem, tree.getStyle().cellTreeEmptyMessage(), true);
showOrHide(emptyMessageElem, false);
contentContainer.appendChild(emptyMessageElem);
showMoreElem = Document.get().createAnchorElement();
// CellTree prevents strict-CSP violation by cancelling event default action.
showMoreElem.setHref("javascript:;");
showMoreElem.setInnerText(messages.showMore());
setStyleName(showMoreElem, tree.getStyle().cellTreeShowMoreButton(), true);
showOrHide(showMoreElem, false);
contentContainer.appendChild(showMoreElem);
}
return contentContainer;
}
/**
* Return the index of this node in its parent.
*/
int getIndex() {
return parentNode == null ? 0 : parentNode.indexOf(this);
}
/**
* Return the parent node, or null if this node is the root.
*/
CellTreeNodeView<?> getParentNode() {
return parentNode;
}
Element getShowMoreElement() {
return showMoreElem;
}
/**
* Get a {@link TreeNode} with a public API for this node view.
*
* @return the {@link TreeNode}
*/
TreeNode getTreeNode() {
if (treeNode == null) {
treeNode = new TreeNodeImpl(this);
}
return treeNode;
}
int indexOf(CellTreeNodeView<?> child) {
return children.indexOf(child);
}
boolean isDestroyed() {
return isDestroyed;
}
/**
* Check if this node is a root node.
*
* @return true if a root node
*/
boolean isRootNode() {
return parentNode == null;
}
/**
* Check if the value of this node is selected.
*
* @return true if selected, false if not
*/
boolean isSelected() {
if (parentNodeInfo != null) {
SelectionModel<? super T> selectionModel = parentNodeInfo.getSelectionModel();
if (selectionModel != null) {
return selectionModel.isSelected(value);
}
}
return false;
}
/**
* Reset focus on this node.
*
* @return true of the cell takes focus, false if not
*/
boolean resetFocusOnCell() {
if (parentNodeInfo != null) {
Context context = new Context(getIndex(), 0, getValueKey());
Cell<T> cell = parentNodeInfo.getCell();
return cell.resetFocus(context, getCellParent(), value);
}
return false;
}
/**
* Select or deselect this node with the keyboard.
*
* @param selected true if selected, false if not
* @param stealFocus true to steal focus
*/
void setKeyboardSelected(boolean selected, boolean stealFocus) {
if (tree.isKeyboardSelectionDisabled()) {
return;
}
// Apply the selected style.
if (!selected || tree.isFocused || stealFocus) {
setKeyboardSelectedStyle(selected);
}
// Make the node focusable or not.
Element cellParent = getCellParent();
if (!selected) {
// Chrome: Elements remain focusable after removing the tabIndex, so set
// it to -1 first.
cellParent.setTabIndex(-1);
cellParent.removeAttribute("tabIndex");
cellParent.removeAttribute("accessKey");
} else {
FocusImpl focusImpl = FocusImpl.getFocusImplForWidget();
focusImpl.setTabIndex(cellParent, tree.getTabIndex());
char accessKey = tree.getAccessKey();
if (accessKey != 0) {
focusImpl.setAccessKey(cellParent, accessKey);
}
if (stealFocus && !tree.cellIsEditing) {
cellParent.focus();
}
}
// Update the selection model.
if (KeyboardSelectionPolicy.BOUND_TO_SELECTION == tree.getKeyboardSelectionPolicy()) {
setSelected(selected);
}
}
/**
* Add or remove the keyboard selected style.
*
* @param selected true if selected, false if not
*/
void setKeyboardSelectedStyle(boolean selected) {
if (!isRootNode()) {
Element selectionElem = getSelectionElement(getElement());
if (selectionElem != null) {
setStyleName(selectionElem, tree.getStyle().cellTreeKeyboardSelectedItem(), selected);
}
}
}
/**
* Select or deselect this node.
*
* @param selected true to select, false to deselect
*/
void setSelected(boolean selected) {
if (parentNodeInfo != null) {
SelectionModel<? super T> selectionModel = parentNodeInfo.getSelectionModel();
if (selectionModel != null) {
selectionModel.setSelected(value, selected);
}
}
}
void showFewer() {
Range range = listView.getVisibleRange();
int defaultPageSize = listView.getDefaultPageSize();
int maxSize = Math.max(defaultPageSize, range.getLength() - defaultPageSize);
listView.setVisibleRange(range.getStart(), maxSize);
}
void showMore() {
Range range = listView.getVisibleRange();
int pageSize = range.getLength() + listView.getDefaultPageSize();
listView.setVisibleRange(range.getStart(), pageSize);
}
private void updateAriaAttributes(int setSize) {
// Early out if this is a root node.
if (isRootNode()) {
return;
}
Roles.getTreeitemRole().setAriaSetsizeProperty(getElement(), setSize);
int selectionIndex = parentNode.indexOf(this);
Roles.getTreeitemRole().setAriaPosinsetProperty(getElement(), selectionIndex + 1);
// Set 'aria-expanded' state
// don't set aria-expanded on the leaf nodes
if (isLeaf()) {
Roles.getTreeitemRole().removeAriaExpandedState(getElement());
} else {
Roles.getTreeitemRole().setAriaExpandedState(getElement(),
ExpandedValue.of(open));
}
Roles.getTreeitemRole().setAriaLevelProperty(getElement(), this.depth);
}
/**
* Update the image based on the current state.
*
* @param isLoading true if still loading data
*/
private void updateImage(boolean isLoading) {
// Early out if this is a root node.
if (isRootNode()) {
return;
}
// Replace the image element with a new one.
boolean isTopLevel = parentNode.isRootNode();
SafeHtml html = tree.getClosedImageHtml(isTopLevel);
if (open) {
html = isLoading ? tree.getLoadingImageHtml() : tree.getOpenImageHtml(isTopLevel);
}
if (nodeInfoLoaded && nodeInfo == null) {
html = LEAF_IMAGE;
}
Element tmp = Document.get().createDivElement();
tmp.setInnerSafeHtml(html);
Element imageElem = tmp.getFirstChildElement();
Element oldImg = getImageElement();
oldImg.getParentElement().replaceChild(imageElem, oldImg);
// Set 'aria-expanded' state
// don't set aria-expanded on the leaf nodes
if (isLeaf()) {
Roles.getTreeitemRole().removeAriaExpandedState(getElement());
} else {
Roles.getTreeitemRole().setAriaExpandedState(getElement(),
ExpandedValue.of(open));
}
}
}