All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.gwt.user.cellview.client.CellTreeNodeView Maven / Gradle / Ivy

/*
 * 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  the type that this view contains
 */
// TODO(jlabanca): Convert this to be the type of the child and create lazily.
class CellTreeNodeView extends UIObject {

  interface Template extends SafeHtmlTemplates {
    @Template("
{2}
{4}
") SafeHtml innerDiv(SafeStyles cssString, String classes, SafeHtml image, String itemValueStyle, SafeHtml cellContents); @Template("
" + "
{2}
") 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 the child item type */ static class NodeCellList implements HasData { /** * The view used by the NodeCellList. */ private class View implements HasDataPresenter.View { private final Element childContainer; public View(Element childContainer) { this.childContainer = childContainer; } @Override public HandlerRegistration addHandler(H handler, Type type) { return handlerManger.addHandler(type, handler); } public void render(SafeHtmlBuilder sb, List values, int start, SelectionModel 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 openNodes = new HashSet(); 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 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 values, SelectionModel 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> 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 values, int start, SelectionModel selectionModel, boolean stealFocus) { // Render the children. SafeHtmlBuilder sb = new SafeHtmlBuilder(); render(sb, values, 0, selectionModel); Map> 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 values, int start, Map> savedViews) { int len = values.size(); int end = start + len; int childCount = nodeView.getChildCount(); int setSize = (childCount > len) ? childCount : end; ProvidesKey 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 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> saveChildState(List values, int start) { // Ensure that we have a children array. if (nodeView.children == null) { nodeView.children = new ArrayList>(); } // 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> openNodes = new HashMap>(); 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 keyProvider = nodeInfo.getProvidesKey(); Map> savedViews = new HashMap>(); 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 presenter; private final Cell cell; private final int defaultPageSize; private HandlerManager handlerManger = new HandlerManager(this); private final NodeInfo nodeInfo; private CellTreeNodeView nodeView; public NodeCellList(final NodeInfo nodeInfo, final CellTreeNodeView nodeView, int pageSize) { this.defaultPageSize = pageSize; this.nodeInfo = nodeInfo; this.nodeView = nodeView; cell = nodeInfo.getCell(); // Create a presenter. presenter = new HasDataPresenter(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 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 getSelectionModel() { return presenter.getSelectionModel(); } @Override public C getVisibleItem(int indexOnPage) { return presenter.getVisibleItem(indexOnPage); } @Override public int getVisibleItemCount() { return presenter.getVisibleItemCount(); } @Override public List 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 values) { presenter.setRowData(start, values); } @Override public void setSelectionModel(final SelectionModel 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("
"); 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> 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 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 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 eventsToSink = new HashSet(); // Listen for focus and blur for keyboard navigation eventsToSink.add(BrowserEvents.FOCUS); eventsToSink.add(BrowserEvents.BLUR); Set 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 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 CellTreeNodeView createTreeNodeView(NodeInfo nodeInfo, Element childElem, C childValue, Object viewData) { return new CellTreeNodeView(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 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 display = (HasData) parentNode.listView; CellPreviewEvent 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 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 the child data type of the node */ protected void onOpen(final NodeInfo nodeInfo) { NodeCellList view = new NodeCellList(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 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 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 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)); } } }