/*******************************************************************************
* Copyright (c) 2012-2017 Codenvy, S.A.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Codenvy, S.A. - initial API and implementation
*******************************************************************************/
package org.eclipse.che.ide.ui.list;
import com.google.gwt.dom.client.DivElement;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.SpanElement;
import com.google.gwt.dom.client.Style;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.KeyDownEvent;
import com.google.gwt.event.dom.client.KeyDownHandler;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.FocusPanel;
import com.google.gwt.user.client.ui.UIObject;
import org.eclipse.che.ide.util.AnimationController;
import java.util.HashMap;
/**
* Overlay type for the base element for a Category Node in the list.
* Nodes that have children, but that have never been expanded (nodes render
* lazily on expansion), have an empty DIV element.
* <p/>
* <pre>
*
* <li class="treeNode">
* <div class="treeNodeBody">
* <span class="treeNodeLabel"></span><div class="expandControl"></div>
* </div>
* <ul class="childrenContainer">
* </ul>
* </li>
*
* </pre>
*
* @author Evgen Vidolob
*/
public class CategoryNodeElement extends FlowPanel {
private final Category category;
private CategoriesList.SelectionManager selectionManager;
private final FocusPanel container;
private final AnimationController animator;
private CategoriesList.Resources resources;
private boolean expanded;
private final DivElement expandControl;
private HashMap<Object, Element> elementsMap;
private Element selectedElement;
@SuppressWarnings("unchecked")
CategoryNodeElement(final Category category,
boolean renderChildren,
CategoriesList.SelectionManager selectionManager,
CategoriesList.Resources resources) {
this.category = category;
this.selectionManager = selectionManager;
CategoryRenderer renderer = category.getRenderer();
this.resources = resources;
setStyleName(resources.defaultCategoriesListCss().category());
FlowPanel header = new FlowPanel();
header.sinkEvents(Event.ONCLICK);
header.addDomHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
if (category.getData().isEmpty()) {
return;
}
expandOrCollapse();
}
}, ClickEvent.getType());
header.setStyleName(resources.defaultCategoriesListCss().categoryHeader());
SpanElement label = Document.get().createSpanElement();
label.setClassName(resources.defaultCategoriesListCss().categoryLabel());
label.appendChild(renderer.renderCategory(category));
header.getElement().appendChild(label);
header.ensureDebugId("categoryHeader-" + category.getTitle());
expandControl = Document.get().createDivElement();
expandControl.appendChild(resources.arrowExpansionImage().getSvg().getElement());
expandControl.setClassName(resources.defaultCategoriesListCss().expandControl());
header.getElement().appendChild(expandControl);
container = new FocusPanel();
container.setTabIndex(1);
container.setStyleName(resources.defaultCategoriesListCss().itemContainer());
container.sinkEvents(Event.ONCLICK);
container.addDomHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
selectElement(Element.as(event.getNativeEvent().getEventTarget()));
}
}, ClickEvent.getType());
container.sinkEvents(Event.ONKEYDOWN);
container.addHandler(new KeyDownHandler() {
@Override
public void onKeyDown(KeyDownEvent keyDownEvent) {
if (selectedElement == null) {
return;
}
Element element = null;
if (keyDownEvent.isDownArrow()) {
element = selectedElement.getNextSiblingElement();
if (element == null) {
return;
}
}
if (keyDownEvent.isUpArrow()) {
element = selectedElement.getPreviousSiblingElement();
if (element.getClassName().equals("")) {
return;
}
}
if (keyDownEvent.isUpArrow() || keyDownEvent.isDownArrow()) {
keyDownEvent.preventDefault();
element.scrollIntoView();
selectElement(element);
}
}
}, KeyDownEvent.getType());
add(header);
add(container);
animator = new AnimationController.Builder().setCollapse(false).setFade(false).build();
expanded = true;
renderChildren();
if (renderChildren) {
expandControl.addClassName(resources.defaultCategoriesListCss().expandedImage());
} else {
expandOrCollapse();
}
}
@SuppressWarnings("unchecked")
private void selectElement(Element eventTarget) {
selectedElement = eventTarget;
selectionManager.selectItem(eventTarget);
category.getEventDelegate().onListItemClicked(eventTarget, ListItem.cast(eventTarget).getData());
}
private void expandOrCollapse() {
if (!expanded) {
expanded = true;
if (container.getElement().getChildCount() == 0) {
renderChildren();
}
animator.show((elemental.dom.Element)container.getElement());
expandControl.addClassName(resources.defaultCategoriesListCss().expandedImage());
} else {
animator.hide((elemental.dom.Element)container.getElement());
expandControl.removeClassName(resources.defaultCategoriesListCss().expandedImage());
expanded = false;
}
}
@SuppressWarnings("unchecked")
private void renderChildren() {
elementsMap = new HashMap<>();
CategoryRenderer categoryRenderer = category.getRenderer();
for (Object o : category.getData()) {
ListItem<?> element = ListItem.create(categoryRenderer, resources.defaultCategoriesListCss(), o);
categoryRenderer.renderElement(element, o);
elementsMap.put(o, element);
if(element.getId().isEmpty()) {
UIObject.ensureDebugId(element, "projectWizard-" + element.getInnerText());
}
container.getElement().appendChild(element);
}
if(elementsMap.isEmpty()) {
expandControl.getStyle().setVisibility(Style.Visibility.HIDDEN);
} else {
expandControl.getStyle().setVisibility(Style.Visibility.VISIBLE);
}
}
/**
* Checks whether the category contains the pointed item.
*
* @param item
* item to find
* @return boolean <code>true</code> if contains
*/
public boolean containsItem(Object item) {
if (elementsMap == null || elementsMap.isEmpty()) {
return false;
}
return elementsMap.containsKey(item);
}
/**
* Selects the item in the category list.
*
* @param item
*/
public void selectItem(Object item) {
if (elementsMap == null || elementsMap.isEmpty()) {
return;
}
if (elementsMap.containsKey(item)) {
selectElement(elementsMap.get(item));
}
}
/**
* A javascript overlay object which ties a list item's DOM element to its
* associated data.
*/
final static class ListItem<M> extends Element {
/**
* Creates a new ListItem overlay object by creating a div element,
* assigning it the listItem css class, and associating it to its data.
*/
public static <M> ListItem<M> create(CategoryRenderer<M> factory, CategoriesList.Css css, M data) {
Element element = factory.createElement();
element.addClassName(css.categoryItem());
ListItem<M> item = ListItem.cast(element);
item.setData(data);
return item;
}
/**
* Casts an element to its ListItem representation. This is an unchecked
* cast so we extract it into this static factory method so we don't have to
* suppress warnings all over the place.
*/
@SuppressWarnings("unchecked")
public static <M> ListItem<M> cast(Element element) {
return (ListItem<M>)element;
}
protected ListItem() {
// Unused constructor
}
public final native M getData() /*-{
return this.__data;
}-*/;
public final native void setData(M data) /*-{
this.__data = data;
}-*/;
}
}