/*
* Copyright 2000-2016 Vaadin Ltd.
*
* 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.vaadin.client.renderers;
import java.util.function.BiConsumer;
import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.Element;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.HasClickHandlers;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.Widget;
import com.vaadin.client.WidgetUtil;
import com.vaadin.client.connectors.treegrid.TreeGridConnector;
import com.vaadin.client.widget.grid.RendererCellReference;
import com.vaadin.client.widget.treegrid.HierarchyRendererCellReferenceWrapper;
import com.vaadin.shared.ui.treegrid.TreeGridCommunicationConstants;
import elemental.json.JsonObject;
/**
* A renderer for displaying hierarchical columns in TreeGrid.
*
* @author Vaadin Ltd
* @since 8.1
*/
public class HierarchyRenderer extends ClickableRenderer<Object, Widget> {
private String nodeStyleName;
private String expanderStyleName;
private String cellContentStyleName;
private static final String CLASS_COLLAPSED = "collapsed";
private static final String CLASS_COLLAPSE_DISABLED = "collapse-disabled";
private static final String CLASS_EXPANDED = "expanded";
private static final String CLASS_DEPTH = "depth-";
private Renderer innerRenderer;
/**
* Constructs a HierarchyRenderer with given collapse callback. Callback is
* called when user clicks on the expander of a row. Callback is given the
* row index and the target collapsed state.
*
* @param collapseCallback
* the callback for collapsing nodes with row index
* @param styleName
* the style name of the widget this renderer is used in
*/
public HierarchyRenderer(BiConsumer<Integer, Boolean> collapseCallback,
String styleName) {
addClickHandler(event -> {
try {
JsonObject row = (JsonObject) event.getRow();
// Row needs to have hierarchy description
if (!hasHierarchyData(row)) {
return;
}
JsonObject hierarchyData = getHierarchyData(row);
if ((!isCollapsed(hierarchyData)
&& !TreeGridConnector.isCollapseAllowed(hierarchyData))
|| isLeaf(hierarchyData)) {
return;
}
collapseCallback.accept(event.getCell().getRowIndex(),
!isCollapsed(hierarchyData));
} finally {
event.stopPropagation();
event.preventDefault();
}
});
setStyleNames(styleName);
}
public void setStyleNames(String primaryStyleName) {
nodeStyleName = primaryStyleName + "-node";
expanderStyleName = primaryStyleName + "-expander";
cellContentStyleName = primaryStyleName + "-cell-content";
}
@Override
public Widget createWidget() {
return new HierarchyItem(nodeStyleName);
}
@Override
public void render(RendererCellReference cell, Object data, Widget widget) {
JsonObject row = (JsonObject) cell.getRow();
int depth = 0;
boolean leaf = false;
boolean collapsed = false;
boolean collapseAllowed = true;
if (hasHierarchyData(row)) {
JsonObject rowDescription = getHierarchyData(row);
depth = getDepth(rowDescription);
leaf = isLeaf(rowDescription);
if (!leaf) {
collapsed = isCollapsed(rowDescription);
collapseAllowed = TreeGridConnector
.isCollapseAllowed(rowDescription);
}
}
HierarchyItem cellWidget = (HierarchyItem) widget;
cellWidget.setDepth(depth);
if (leaf) {
cellWidget.setExpanderState(ExpanderState.LEAF);
} else if (collapsed) {
cellWidget.setExpanderState(ExpanderState.COLLAPSED);
} else {
cellWidget.setExpanderState(ExpanderState.EXPANDED);
}
cellWidget.setCollapseAllowed(collapseAllowed);
// Render the contents of the inner renderer. For non widget
// renderers
// the cell reference needs to be wrapped so that its getElement
// method
// returns the correct element we want to render.
if (innerRenderer instanceof WidgetRenderer) {
((WidgetRenderer) innerRenderer).render(cell, data,
((HierarchyItem) widget).content);
} else {
innerRenderer.render(
new HierarchyRendererCellReferenceWrapper(cell,
((HierarchyItem) widget).content.getElement()),
data);
}
}
private int getDepth(JsonObject rowDescription) {
return (int) rowDescription
.getNumber(TreeGridCommunicationConstants.ROW_DEPTH);
}
private JsonObject getHierarchyData(JsonObject row) {
return row.getObject(
TreeGridCommunicationConstants.ROW_HIERARCHY_DESCRIPTION);
}
private boolean hasHierarchyData(JsonObject row) {
return row.hasKey(
TreeGridCommunicationConstants.ROW_HIERARCHY_DESCRIPTION);
}
private boolean isLeaf(JsonObject rowDescription) {
boolean leaf;
leaf = rowDescription
.getBoolean(TreeGridCommunicationConstants.ROW_LEAF);
return leaf;
}
private boolean isCollapsed(JsonObject rowDescription) {
boolean collapsed;
collapsed = rowDescription
.getBoolean(TreeGridCommunicationConstants.ROW_COLLAPSED);
return collapsed;
}
/**
* Sets the renderer to be wrapped. This is the original renderer before
* hierarchy is applied.
*
* @param innerRenderer
* Renderer to be wrapped.
*/
public void setInnerRenderer(Renderer innerRenderer) {
this.innerRenderer = innerRenderer;
}
/**
* Returns the wrapped renderer.
*
* @return Wrapped renderer.
*/
public Renderer getInnerRenderer() {
return innerRenderer;
}
/**
* Decides whether the element was rendered by {@link HierarchyRenderer}
*/
public static boolean isElementInHierarchyWidget(Element element) {
Widget w = WidgetUtil.findWidget(element);
while (w != null) {
if (w instanceof HierarchyItem) {
return true;
}
w = w.getParent();
}
return false;
}
private class HierarchyItem extends Composite {
private FlowPanel panel;
private Expander expander;
private Widget content;
private HierarchyItem(String className) {
panel = new FlowPanel();
panel.getElement().addClassName(className);
expander = new Expander();
expander.getElement().addClassName(expanderStyleName);
if (innerRenderer instanceof WidgetRenderer) {
content = ((WidgetRenderer) innerRenderer).createWidget();
} else {
// TODO: 20/09/16 create more general widget?
content = GWT.create(HTML.class);
}
content.getElement().addClassName(cellContentStyleName);
panel.add(expander);
panel.add(content);
expander.addClickHandler(HierarchyRenderer.this);
initWidget(panel);
}
private void setDepth(int depth) {
String classNameToBeReplaced = getFullClassName(CLASS_DEPTH,
panel.getElement().getClassName());
if (classNameToBeReplaced == null) {
panel.getElement().addClassName(CLASS_DEPTH + depth);
} else {
panel.getElement().replaceClassName(classNameToBeReplaced,
CLASS_DEPTH + depth);
}
}
private String getFullClassName(String prefix, String classNameList) {
int start = classNameList.indexOf(prefix);
int end = start + prefix.length();
if (start > -1) {
while (end < classNameList.length()
&& classNameList.charAt(end) != ' ') {
end++;
}
return classNameList.substring(start, end);
}
return null;
}
private void setExpanderState(ExpanderState state) {
switch (state) {
case EXPANDED:
expander.getElement().removeClassName(CLASS_COLLAPSED);
expander.getElement().addClassName(CLASS_EXPANDED);
break;
case COLLAPSED:
expander.getElement().removeClassName(CLASS_EXPANDED);
expander.getElement().addClassName(CLASS_COLLAPSED);
break;
case LEAF:
default:
expander.getElement().removeClassName(CLASS_COLLAPSED);
expander.getElement().removeClassName(CLASS_EXPANDED);
}
}
private void setCollapseAllowed(boolean collapseAllowed) {
if (expander.getElement().hasClassName(CLASS_EXPANDED)
&& !collapseAllowed) {
expander.getElement().addClassName(CLASS_COLLAPSE_DISABLED);
} else {
expander.getElement().removeClassName(CLASS_COLLAPSE_DISABLED);
}
}
private class Expander extends Widget implements HasClickHandlers {
private Expander() {
Element span = DOM.createSpan();
setElement(span);
}
@Override
public HandlerRegistration addClickHandler(ClickHandler handler) {
return addDomHandler(handler, ClickEvent.getType());
}
}
}
enum ExpanderState {
EXPANDED, COLLAPSED, LEAF;
}
}