/*
* Copyright 2015 Explicatis GmbH <ext-token-field@explicatis.com>
*
* Author: Florian Schmitt
*
* 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.explicatis.ext_token_field.client;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.explicatis.ext_token_field.shared.DropTargetType;
import com.explicatis.ext_token_field.shared.ExtTokenFieldServerRpc;
import com.explicatis.ext_token_field.shared.Token;
import com.explicatis.ext_token_field.shared.TokenAction;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.dom.client.Node;
import com.google.gwt.dom.client.NodeList;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyDownHandler;
import com.google.gwt.user.client.ui.Anchor;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.HasEnabled;
import com.vaadin.client.ApplicationConnection;
import com.vaadin.client.ComponentConnector;
import com.vaadin.client.ui.Icon;
import com.vaadin.client.ui.VButton;
import com.vaadin.client.ui.VComboBox;
import com.vaadin.shared.Connector;
import elemental.events.KeyboardEvent.KeyCode;
public class ExtTokenFieldWidget extends FlowPanel implements HasEnabled
{
public static final String TOKEN_FIELD_CLASS_NAME = "exttokenfield";
private List<TokenWidget> tokenWidgets = new LinkedList<TokenWidget>();
private ExtTokenFieldServerRpc serverRpc;
private VComboBox inputFilterSelect;
private VButton inputButton;
private Token tokenToTheRight;
private List<TokenAction> tokenActions;
private Map<TokenAction, String> icons;
private ApplicationConnection applicationConnection;
private boolean isReadOnly = false;
private boolean isEnabled = true;
private boolean tokenDragAndDropEnabled = false;
private int tokenCount = 0;
public ExtTokenFieldWidget()
{
getElement().setClassName(TOKEN_FIELD_CLASS_NAME);
}
public void setApplicationConnection(ApplicationConnection applicationConnection)
{
this.applicationConnection = applicationConnection;
}
public void setIconResourceUrl(TokenAction tokenAction, String url)
{
if (icons == null)
{
icons = new HashMap<>();
}
icons.put(tokenAction, url);
}
public void setTokenActions(Set<TokenAction> tokenActions)
{
List<TokenAction> sortedList = new LinkedList<>(tokenActions);
Collections.sort(sortedList);
this.tokenActions = sortedList;
}
public void setInputButton(Connector inputButton)
{
if (inputButton != null)
{
this.inputButton = (VButton) ((ComponentConnector) inputButton).getWidget();
this.inputButton.addKeyDownHandler(initKeyDownHandler());
add(this.inputButton);
}
}
public boolean getTokenDragAndDropEnabled()
{
return this.tokenDragAndDropEnabled;
}
public void setTokenDragAndDropEnabled(boolean value)
{
this.tokenDragAndDropEnabled = value;
}
public void setInputField(Connector inputField)
{
if (inputField != null)
{
inputFilterSelect = (VComboBox) ((ComponentConnector) inputField).getWidget();
/**
* add key down handler, to select the token at the very left of the filter select when left key was pressed
*
* TODO: more work to do... make sure suggestion box is closed etc.
*/
inputFilterSelect.tb.addKeyDownHandler(initKeyDownHandler());
add(inputFilterSelect);
}
}
private KeyDownHandler initKeyDownHandler()
{
return event -> {
if (event.getNativeKeyCode() == KeyCode.LEFT)
{
if (ExtTokenFieldWidget.this.tokenWidgets.size() > 0)
{
tokenWidgets.get(tokenWidgets.size() - 1).setFocus(true);
}
}
};
}
public void updateTokens(List<Token> tokens)
{
// TODO: register changes, not recreate everything
removeAllTokens();
removeTokenDropTargets();
addTokens(tokens);
int currentTokenCount = tokens.size();
if (tokenToTheRight != null)
{
final TokenWidget tokenWidget = findTokenWidget(tokenToTheRight);
Scheduler.get().scheduleDeferred(() -> {
if (tokenWidget != null)
{
tokenWidget.setFocus(true);
}
tokenToTheRight = null;
});
}
else
{
boolean lastTokenWasRemoved = (currentTokenCount == 0) && (tokenCount == 1);
if (lastTokenWasRemoved)
{
Scheduler.get().scheduleDeferred(() -> {
if (inputFilterSelect != null)
inputFilterSelect.tb.setFocus(true);
else if (inputButton != null)
inputButton.setFocus(true);
});
}
}
tokenCount = tokens.size();
}
protected TokenWidget buildTokenWidget(final Token token)
{
final TokenWidget widget = new TokenWidget(this, token, tokenActions)
{
@Override
protected void onTokenActionClicked(TokenAction tokenAction)
{
tokenActionClicked(this, tokenAction);
}
@Override
protected void buildIcon(final TokenAction action, final Anchor actionAnchor)
{
if (icons != null && icons.containsKey(action))
{
Icon icon = applicationConnection.getIcon(icons.get(action));
actionAnchor.getElement().insertBefore(icon.getElement(), null);
}
}
};
widget.addFocusHandler(event -> widget.getElement().addClassName(TokenWidget.FOCUS_CLASS_NAME));
widget.addBlurHandler(event -> widget.getElement().removeClassName(TokenWidget.FOCUS_CLASS_NAME));
widget.addKeyDownHandler(event -> {
if (event.getNativeKeyCode() == KeyCodes.KEY_LEFT)
{
leftKeyDown(widget);
}
else if (event.getNativeKeyCode() == KeyCodes.KEY_RIGHT)
{
rightKeyDown(widget);
}
else if (event.getNativeKeyCode() == KeyCodes.KEY_DELETE)
{
TokenAction deleteTokenAction = findTokenAction(TokenAction.DELETE_TOKEN_ACTION_IDENTIFIER);
if (deleteTokenAction != null)
{
if (isEnabled() && !isReadOnly())
{
tokenActionClicked(widget, deleteTokenAction);
}
}
}
});
return widget;
}
protected TokenAction findTokenAction(String identifier)
{
for (TokenAction action : tokenActions)
{
if (action.identifier.equals(identifier))
{
return action;
}
}
return null;
}
protected void tokenActionClicked(final TokenWidget widget, final TokenAction tokenAction)
{
if (tokenAction.identifier.equals(TokenAction.DELETE_TOKEN_ACTION_IDENTIFIER))
{
TokenWidget tokenWidgetToTheRight = getTokenToTheRight(widget);
if (tokenWidgetToTheRight != null)
{
tokenToTheRight = tokenWidgetToTheRight.getToken();
}
}
serverRpc.tokenActionClicked(widget.getToken(), tokenAction);
}
protected void rightKeyDown(TokenWidget token)
{
TokenWidget tokenToTheRight = getTokenToTheRight(token);
if (tokenToTheRight != null)
{
tokenToTheRight.setFocus(true);
}
else
{
if (inputFilterSelect != null)
inputFilterSelect.tb.setFocus(true);
else if (inputButton != null)
inputButton.setFocus(true);
}
}
protected void leftKeyDown(TokenWidget token)
{
TokenWidget tokenToTheLeft = getTokenToTheLeft(token);
if (tokenToTheLeft != null)
{
tokenToTheLeft.setFocus(true);
}
}
protected TokenWidget getTokenToTheLeft(TokenWidget token)
{
if (!hasMoreTokensLeft(token))
{
return null;
}
int indexOf = tokenWidgets.indexOf(token);
TokenWidget tokenWidget = tokenWidgets.get(indexOf - 1);
return tokenWidget;
}
protected TokenWidget getTokenToTheRight(TokenWidget token)
{
if (!hasMoreTokensRight(token))
{
return null;
}
int indexOf = tokenWidgets.indexOf(token);
TokenWidget tokenWidget = tokenWidgets.get(indexOf + 1);
return tokenWidget;
}
protected boolean hasMoreTokensLeft(TokenWidget token)
{
int indexOf = tokenWidgets.indexOf(token);
return indexOf > 0;
}
protected boolean hasMoreTokensRight(TokenWidget token)
{
int indexOf = tokenWidgets.indexOf(token);
return indexOf < tokenWidgets.size() - 1;
}
protected void addTokens(List<Token> tokens)
{
for (int i = 0; i < tokens.size(); i++)
{
Token t = tokens.get(i);
insertTokenAtPosition(i, t);
}
if (tokenDragAndDropEnabled)
{
int lastTokenIndex = tokens.size() - 1;
Token lastToken = tokens.get(lastTokenIndex);
lastTokenIndex *= 2;
addDropTargetAfter(lastTokenIndex, lastToken);
}
}
private void insertTokenAtPosition(int i, Token t)
{
TokenWidget widget = buildTokenWidget(t);
tokenWidgets.add(widget);
if (tokenDragAndDropEnabled)
{
i *= 2;
}
insert(widget, i);
if (tokenDragAndDropEnabled)
{
addDropTargetBefore(i, t);
}
}
private void addDropTargetBefore(int i, Token t)
{
DropTargetWidget dropTarget = new DropTargetWidget(this, DropTargetType.BEFORE, t);
insert(dropTarget, i);
}
private void addDropTargetAfter(int i, Token t)
{
DropTargetWidget dropTarget = new DropTargetWidget(this, DropTargetType.AFTER, t);
insert(dropTarget, i + 2);
}
protected void removeAllTokens()
{
for (TokenWidget t : tokenWidgets)
{
remove(t);
}
tokenWidgets.clear();
}
protected void removeTokenDropTargets()
{
NodeList<Node> childNodes = getElement().getChildNodes();
Set<Node> toBeRemoved = new HashSet<Node>();
for (int i = 0; i < childNodes.getLength(); i++)
{
Node item = childNodes.getItem(i);
com.google.gwt.dom.client.Element element = (com.google.gwt.dom.client.Element) item.cast();
boolean hasDropTargetClass = DropTargetWidget.DROP_TARGET_CLASS_NAME.equals(element.getClassName());
if (hasDropTargetClass)
{
toBeRemoved.add(item);
}
}
for (Node node : toBeRemoved)
{
getElement().removeChild(node);
}
}
/**
* maybe a map would be a better idea
*/
public Token findTokenById(long tokenId)
{
for (TokenWidget t : tokenWidgets)
{
if (t.getToken().id == tokenId)
{
return t.getToken();
}
}
return null;
}
/**
* maybe a map would be a better idea
*/
private TokenWidget findTokenWidget(Token token)
{
for (TokenWidget t : tokenWidgets)
{
if (t.getToken().equals(token))
{
return t;
}
}
return null;
}
public void setServerRpc(ExtTokenFieldServerRpc serverRpc)
{
this.serverRpc = serverRpc;
}
public ExtTokenFieldServerRpc getServerRpc()
{
return this.serverRpc;
}
@Override
public boolean isEnabled()
{
return isEnabled;
}
@Override
public void setEnabled(boolean enabled)
{
isEnabled = enabled;
}
public boolean isReadOnly()
{
return isReadOnly;
}
public void setReadOnly(boolean readOnly)
{
getElement().setPropertyBoolean("readOnly", readOnly);
String readOnlyStyle = "readonly";
if (readOnly)
{
addStyleDependentName(readOnlyStyle);
}
else
{
removeStyleDependentName(readOnlyStyle);
}
this.isReadOnly = readOnly;
}
}