/*
* Copyright 2016 JBoss, by Red Hat, 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 org.uberfire.ext.widgets.table.client;
import java.util.ArrayList;
import java.util.List;
import com.google.gwt.cell.client.AbstractCell;
import com.google.gwt.cell.client.Cell.Context;
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.NativeEvent;
import com.google.gwt.dom.client.SpanElement;
import com.google.gwt.dom.client.Style;
import com.google.gwt.dom.client.Style.Cursor;
import com.google.gwt.dom.client.Style.Position;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
import com.google.gwt.safehtml.shared.SafeHtmlUtils;
import com.google.gwt.user.cellview.client.Column;
import com.google.gwt.user.cellview.client.Header;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.Event.NativePreviewEvent;
import com.google.gwt.user.client.Event.NativePreviewHandler;
import org.gwtbootstrap3.client.ui.gwt.DataGrid;
import org.uberfire.commons.validation.PortablePreconditions;
import static com.google.gwt.dom.client.Style.Unit.PX;
/**
* A column header that supports resizing and moving
* See https://github.com/gchatelet/GwtResizableDraggableColumns/blob/master/src/fr/mikrosimage/gwt/client/ResizableHeader.java
* @param <T>
*/
public abstract class ResizableMovableHeader<T> extends Header<String> {
private static final Cursor MOVE_CURSOR = Cursor.MOVE;
private static final String MOVE_COLOR = "gray";
private static final int MOVE_HANDLE_WIDTH = 32;
private static final Cursor RESIZE_CURSOR = Cursor.COL_RESIZE;
private static final String RESIZE_COLOR = "gray";
private static final int RESIZE_HANDLE_WIDTH = 8;
private static final double GHOST_OPACITY = .3;
private static final int MINIMUM_COLUMN_WIDTH = 30;
private final Document document = Document.get();
private final String title;
private final DataGrid<T> table;
private final UberfireColumnPicker columnPicker;
private final Column<T, ?> column;
private final Element tableElement;
private HeaderHelper current;
private List<ColumnChangedHandler> columnChangedHandlers = new ArrayList<ColumnChangedHandler>();
public ResizableMovableHeader(final String title,
final DataGrid<T> table,
final UberfireColumnPicker columnPicker,
final Column<T, ?> column) {
super(new HeaderCell());
this.title = PortablePreconditions.checkNotNull("title",
title);
this.table = PortablePreconditions.checkNotNull("table",
table);
this.columnPicker = PortablePreconditions.checkNotNull("columnPicker",
columnPicker);
this.column = PortablePreconditions.checkNotNull("column",
column);
this.tableElement = table.getElement();
}
private static NativeEvent getEventAndPreventPropagation(final NativePreviewEvent event) {
final NativeEvent nativeEvent = event.getNativeEvent();
nativeEvent.preventDefault();
nativeEvent.stopPropagation();
return nativeEvent;
}
private static void setLine(final Style style,
final int width,
final int top,
final int height,
final String color) {
style.setPosition(Position.ABSOLUTE);
style.setTop(top,
PX);
style.setHeight(height,
PX);
style.setWidth(width,
PX);
style.setBackgroundColor(color);
style.setZIndex(Integer.MAX_VALUE);
}
@Override
public String getValue() {
return title;
}
@Override
public void onBrowserEvent(final Context context,
final Element target,
final NativeEvent event) {
if (current == null) {
current = new HeaderHelper(target,
event);
}
}
protected void columnResized(final int newWidth) {
table.setColumnWidth(column,
newWidth + "px");
columnPicker.adjustColumnWidths();
for (ColumnChangedHandler handler : columnChangedHandlers) {
handler.afterColumnChanged();
}
}
protected void columnMoved(final int fromIndex,
final int beforeIndex) {
columnPicker.columnMoved(fromIndex,
beforeIndex);
table.removeColumn(fromIndex);
table.insertColumn(beforeIndex,
column,
this);
for (ColumnChangedHandler handler : columnChangedHandlers) {
handler.afterColumnChanged();
}
}
protected abstract int getTableBodyHeight();
public void addColumnChangedHandler(ColumnChangedHandler handler) {
if (handler != null) {
columnChangedHandlers.add(handler);
}
}
interface IDragCallback {
void dragFinished();
}
private static class HeaderCell extends AbstractCell<String> {
public HeaderCell() {
super("mousemove");
}
@Override
public void render(final Context context,
final String value,
final SafeHtmlBuilder sb) {
sb.append(SafeHtmlUtils.fromString(value));
}
}
private class HeaderHelper implements NativePreviewHandler,
IDragCallback {
private final HandlerRegistration handler = Event.addNativePreviewHandler(this);
private final Element source;
private final Element handles;
private final Element moveHandle;
private final Element resizeHandle;
private boolean dragging;
public HeaderHelper(final Element target,
final NativeEvent event) {
event.preventDefault();
event.stopPropagation();
this.source = target;
this.handles = document.createDivElement();
final int leftBound = target.getOffsetLeft() + target.getOffsetWidth();
this.moveHandle = createSpanElement(MOVE_CURSOR,
leftBound - RESIZE_HANDLE_WIDTH - MOVE_HANDLE_WIDTH,
MOVE_HANDLE_WIDTH);
this.resizeHandle = createSpanElement(RESIZE_CURSOR,
leftBound - RESIZE_HANDLE_WIDTH,
RESIZE_HANDLE_WIDTH);
handles.appendChild(moveHandle);
handles.appendChild(resizeHandle);
source.appendChild(handles);
}
private SpanElement createSpanElement(final Cursor cursor,
final double left,
final double width) {
final SpanElement span = document.createSpanElement();
span.setAttribute("title",
title);
final Style style = span.getStyle();
style.setCursor(cursor);
style.setPosition(Position.ABSOLUTE);
style.setBottom(0,
PX);
style.setHeight(source.getOffsetHeight(),
PX);
style.setTop(source.getOffsetTop(),
PX);
style.setWidth(width,
PX);
style.setLeft(left,
PX);
return span;
}
@Override
public void onPreviewNativeEvent(final NativePreviewEvent event) {
final NativeEvent natEvent = event.getNativeEvent();
final Element element = natEvent.getEventTarget().cast();
final String eventType = natEvent.getType();
if (!(element == moveHandle || element == resizeHandle)) {
if ("mousedown".equals(eventType)) {
//No need to do anything, the event will be passed on to the column sort handler
} else if (!dragging && "mouseover".equals(eventType)) {
cleanUp();
}
return;
}
final NativeEvent nativeEvent = getEventAndPreventPropagation(event);
if ("mousedown".equals(eventType)) {
if (element == resizeHandle) {
moveHandle.removeFromParent();
new ColumnResizeHelper(this,
source,
nativeEvent);
} else {
new ColumnMoverHelper(this,
source,
nativeEvent);
}
dragging = true;
}
}
private void cleanUp() {
handler.removeHandler();
handles.removeFromParent();
current = null;
}
public void dragFinished() {
dragging = false;
cleanUp();
}
}
private class ColumnResizeHelper implements NativePreviewHandler {
private final HandlerRegistration handler = Event.addNativePreviewHandler(this);
private final DivElement resizeLine = document.createDivElement();
private final Style resizeLineStyle = resizeLine.getStyle();
private final Element header;
private final IDragCallback dragCallback;
private ColumnResizeHelper(final IDragCallback dragCallback,
final Element header,
final NativeEvent event) {
this.dragCallback = dragCallback;
this.header = header;
setLine(resizeLineStyle,
2,
0,
getTableBodyHeight(),
RESIZE_COLOR);
moveLine(event.getClientX());
tableElement.appendChild(resizeLine);
}
@Override
public void onPreviewNativeEvent(final NativePreviewEvent event) {
final NativeEvent nativeEvent = getEventAndPreventPropagation(event);
final int clientX = nativeEvent.getClientX();
final String eventType = nativeEvent.getType();
if ("mousemove".equals(eventType)) {
moveLine(clientX);
} else if ("mouseup".equals(eventType)) {
handler.removeHandler();
resizeLine.removeFromParent();
dragCallback.dragFinished();
columnResized(Math.max(clientX - header.getAbsoluteLeft(),
MINIMUM_COLUMN_WIDTH));
}
}
private void moveLine(final int clientX) {
final int xPos = clientX - table.getAbsoluteLeft();
resizeLineStyle.setLeft(xPos,
PX);
}
}
private class ColumnMoverHelper implements NativePreviewHandler {
private static final int ghostLineWidth = 4;
private final HandlerRegistration handler = Event.addNativePreviewHandler(this);
private final DivElement ghostLine = document.createDivElement();
private final Style ghostLineStyle = ghostLine.getStyle();
private final DivElement ghostColumn = document.createDivElement();
private final Style ghostColumnStyle = ghostColumn.getStyle();
private final int columnWidth;
private final int[] columnXPositions;
private final IDragCallback dragCallback;
private int fromIndex = -1;
private int toIndex;
private ColumnMoverHelper(final IDragCallback dragCallback,
final Element target,
final NativeEvent event) {
final int clientX = event.getClientX();
final Element tr = getRowElement(target);
final int columns = tr.getChildCount();
this.dragCallback = dragCallback;
this.columnWidth = target.getOffsetWidth();
this.columnXPositions = new int[columns + 1];
this.columnXPositions[0] = tr.getAbsoluteLeft();
for (int i = 0; i < columns; ++i) {
final int xPos = columnXPositions[i] + ((Element) tr.getChild(i)).getOffsetWidth();
if (xPos > clientX && fromIndex == -1) {
fromIndex = i;
}
columnXPositions[i + 1] = xPos;
}
toIndex = fromIndex;
final int bodyHeight = getTableBodyHeight();
setLine(ghostColumnStyle,
columnWidth,
0,
bodyHeight,
MOVE_COLOR);
setLine(ghostLineStyle,
ghostLineWidth,
0,
bodyHeight,
RESIZE_COLOR);
ghostColumnStyle.setOpacity(GHOST_OPACITY);
moveColumn(clientX);
tableElement.appendChild(ghostColumn);
tableElement.appendChild(ghostLine);
}
protected Element getRowElement(Element target) {
Element parent = target.getParentElement();
while (parent != null) {
if (parent.getTagName().equalsIgnoreCase("tr")) {
return parent;
}
parent = parent.getParentElement();
}
return target.getParentElement();
}
@Override
public void onPreviewNativeEvent(final NativePreviewEvent event) {
final NativeEvent nativeEvent = getEventAndPreventPropagation(event);
final String eventType = nativeEvent.getType();
if ("mousemove".equals(eventType)) {
moveColumn(nativeEvent.getClientX());
} else if ("mouseup".equals(eventType)) {
handler.removeHandler();
ghostColumn.removeFromParent();
ghostLine.removeFromParent();
if (fromIndex != toIndex) {
columnMoved(fromIndex,
toIndex);
}
dragCallback.dragFinished();
}
}
private void moveColumn(final int clientX) {
final int pointer = clientX - columnWidth / 2;
ghostColumnStyle.setLeft(pointer - table.getAbsoluteLeft(),
PX);
for (int i = 0; i < columnXPositions.length - 1; ++i) {
if (clientX < columnXPositions[i + 1]) {
final int adjustedIndex = i > fromIndex ? i + 1 : i;
int lineXPos = columnXPositions[adjustedIndex] - table.getAbsoluteLeft();
if (adjustedIndex == columnXPositions.length - 1) {
lineXPos -= ghostLineWidth;
} else if (adjustedIndex > 0) {
lineXPos -= ghostLineWidth / 2;
}
ghostLineStyle.setLeft(lineXPos,
PX);
toIndex = i;
break;
}
}
}
}
}