/* * 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; import java.io.Serializable; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; import com.explicatis.ext_token_field.events.TokenAddedEvent; import com.explicatis.ext_token_field.events.TokenAddedListener; import com.explicatis.ext_token_field.events.TokenRemovedEvent; import com.explicatis.ext_token_field.events.TokenRemovedListener; import com.explicatis.ext_token_field.events.TokenReorderedEvent; import com.explicatis.ext_token_field.events.TokenReorderedListener; import com.explicatis.ext_token_field.shared.DropTargetType; import com.explicatis.ext_token_field.shared.ExtTokenFieldServerRpc; import com.explicatis.ext_token_field.shared.ExtTokenFieldState; import com.explicatis.ext_token_field.shared.Token; import com.explicatis.ext_token_field.shared.TokenAction; import com.vaadin.icons.VaadinIcons; import com.vaadin.shared.Registration; import com.vaadin.ui.AbstractField; import com.vaadin.ui.AbstractSingleComponentContainer; import com.vaadin.ui.Button; import com.vaadin.ui.ComboBox; import com.vaadin.ui.Component; import com.vaadin.ui.HasComponents; public class ExtTokenField extends AbstractField<List<Tokenizable>> implements HasComponents { private ExtTokenFieldServerRpc serverRpc = new ExtTokenFieldServerRpc() { @Override public void tokenActionClicked(Token token, TokenAction tokenAction) { if (identifierToTokenizableAction.containsKey(tokenAction.identifier)) { Tokenizable tokenizable = identifierToTokenizable.get(token.id); identifierToTokenizableAction.get(tokenAction.identifier).onClick(tokenizable); } } @Override public void tokenDroped(Token sourceToken, Token targetToken, DropTargetType type) { handleDroppedToken(sourceToken, targetToken, type); } }; private Map<Long, Tokenizable> identifierToTokenizable = new HashMap<>(); private Map<String, TokenizableAction> identifierToTokenizableAction = new HashMap<>(); private List<Tokenizable> value; public ExtTokenField() { registerRpc(serverRpc); addAttachListener(event -> { if (!hasInputButton() && !hasInputField()) throw new RuntimeException("no input field nor input button set"); }); } public void setTokenDragDropEnabled(boolean value) { getState().tokenDragAndDropEnabled = value; } public void setEnableDefaultDeleteTokenAction(boolean value) { DefaultDeleteTokenAction defaultDeleteTokenAction = new DefaultDeleteTokenAction(); if (value) { if (!hasTokenizableAction(defaultDeleteTokenAction)) addTokenAction(defaultDeleteTokenAction); } else { if (hasTokenizableAction(defaultDeleteTokenAction)) removeTokenizableAction(defaultDeleteTokenAction); } } protected Token convertTokenizableToToken(Tokenizable value) { Token result = new Token(); result.id = value.getIdentifier(); result.value = value.getStringValue(); return result; } public void addTokenAction(TokenizableAction tokenizableAction) { for (TokenAction ta : getState().tokenActions) { if (ta.identifier.equals(tokenizableAction.getIdentifier())) { throw new RuntimeException("TokenAction identifier is not unique"); } } if (tokenizableAction.icon != null) { setResource(tokenizableAction.getIdentifier() + "-icon", tokenizableAction.icon); } TokenAction tokenAction = fromTokenizableActionToTokenAction(tokenizableAction); identifierToTokenizableAction.put(tokenizableAction.getIdentifier(), tokenizableAction); getState().tokenActions.add(tokenAction); } public void removeTokenizableAction(TokenizableAction tokenizableAction) { boolean containsKey = identifierToTokenizableAction.containsKey(tokenizableAction.getIdentifier()); if (!containsKey) { throw new NoSuchElementException(String.format("TokenizableAction with identifier %s not found", tokenizableAction.getIdentifier())); } findTokenActionByTokenizableAction(tokenizableAction).ifPresent(toRemove -> { getState().tokenActions.remove(toRemove); identifierToTokenizableAction.remove(tokenizableAction.getIdentifier()); }); } protected TokenAction fromTokenizableActionToTokenAction(TokenizableAction a) { TokenAction b = new TokenAction(); b.identifier = a.getIdentifier(); b.label = a.getLabel(); b.viewOrder = a.getViewOrder(); b.inheritsReadOnlyAndEnabled = a.getInheritsReadOnlyAndEnabled(); return b; } public void addTokenizable(Tokenizable tokenizable) { Objects.requireNonNull(tokenizable, () -> "tokenizable must not be null"); if (identifierToTokenizable.keySet().contains(tokenizable.getIdentifier())) { return; } Token token = convertTokenizableToToken(tokenizable); identifierToTokenizable.put(tokenizable.getIdentifier(), tokenizable); addToken(token); List<Tokenizable> currentValue = getOptionalValue() .orElseGet(() -> new LinkedList<Tokenizable>()); List<Tokenizable> copy = new LinkedList<>(currentValue); copy.add(tokenizable); setValue(copy); fireEvent(new TokenAddedEvent(this, tokenizable)); } public void removeTokenizable(Tokenizable tokenizable) { Objects.requireNonNull(tokenizable, () -> "tokenizable must not be null"); Token token = findTokenByTokenizable(tokenizable)// .orElseThrow(() -> new NoSuchElementException(String.format("tokenizable %s could not be found", tokenizable.getStringValue()))); removeToken(token); List<Tokenizable> newList = getValue().stream() .filter(t -> t.getIdentifier() != tokenizable.getIdentifier()) .collect(Collectors.toList()); identifierToTokenizable.remove(token.id); setValue(newList); fireEvent(new TokenRemovedEvent(this, tokenizable)); } protected void handleDroppedToken(Token sourceToken, Token targetToken, DropTargetType type) { reorderToken(sourceToken, targetToken, type); List<Tokenizable> currentValue = getOptionalValue()// .orElseThrow(() -> new IllegalStateException("value cannot be null, if token was dropped")); List<Tokenizable> copy = new LinkedList<>(currentValue); Tokenizable sourceTokenizable = findTokenizableInListByToken(currentValue, sourceToken); Tokenizable targetTokenizable = findTokenizableInListByToken(currentValue, targetToken); int targetIndex = currentValue.indexOf(targetTokenizable); targetIndex = DropTargetType.BEFORE.equals(type) ? targetIndex : targetIndex + 1; boolean afterLast = targetIndex == currentValue.size() && DropTargetType.AFTER.equals(type); if (isIndexOfALowerThanB(currentValue, sourceTokenizable, targetTokenizable)) targetIndex--; copy.remove(sourceTokenizable); if (afterLast) { copy.add(sourceTokenizable); } else { copy.add(targetIndex, sourceTokenizable); } setValue(copy); fireEvent(new TokenReorderedEvent(this, sourceTokenizable, targetTokenizable, type)); } private Tokenizable findTokenizableInListByToken(List<Tokenizable> list, Token token) { return list.stream() .filter(tokenizable -> tokenizable.getIdentifier() == token.id) .findAny() .orElseThrow(() -> new NoSuchElementException("could not find token")); } public boolean hasTokenizableAction(TokenizableAction tokenizableAction) { return findTokenActionByTokenizableAction(tokenizableAction).isPresent(); } protected Optional<TokenAction> findTokenActionByTokenizableAction(TokenizableAction tokenizableAction) { Objects.requireNonNull(tokenizableAction, () -> "tokenizableAction must not be null"); return getState().tokenActions.stream() .filter(tokenAction -> tokenAction.identifier.equals(tokenizableAction.getIdentifier())) .findAny(); } protected Optional<Token> findTokenByTokenizable(Tokenizable tokenizable) { return getState().tokens.stream() .filter(token -> token.id == tokenizable.getIdentifier()) .findAny(); } private void addToken(Token token) { getState().tokens.add(token); } private void removeToken(Token token) { getState().tokens.remove(token); } private void reorderToken(Token source, Token target, DropTargetType type) { List<Token> tokens = getState().tokens; int targetIndex = tokens.indexOf(target); targetIndex = DropTargetType.BEFORE.equals(type) ? targetIndex : targetIndex + 1; boolean afterLast = targetIndex == tokens.size() && DropTargetType.AFTER.equals(type); if (isIndexOfALowerThanB(tokens, source, target)) targetIndex--; tokens.remove(source); if (afterLast) { tokens.add(source); } else { tokens.add(targetIndex, source); } getState().tokens = tokens; } public void setInputField(ComboBox<?> field) { if (field != null) { removeFieldOrButton(); addComponent(field); getState().inputField = field; } } public void setInputButton(Button button) { if (button != null) { removeFieldOrButton(); addComponent(button); getState().inputButton = button; } } public boolean hasInputField() { return getInputField() != null; } public boolean hasInputButton() { return getInputButton() != null; } private void removeFieldOrButton() { if (iterator().hasNext()) { removeComponent(iterator().next()); getState().inputButton = null; getState().inputField = null; } } public ComboBox<?> getInputField() { return (ComboBox<?>) getState().inputField; } public Button getInputButton() { return (Button) getState().inputButton; } @Override protected ExtTokenFieldState getState() { return (ExtTokenFieldState) super.getState(); } @Override public Iterator<Component> iterator() { if (getInputField() == null && getInputButton() == null) return emptyIterator(); return new ComponentIterator(); } protected Optional<Focusable> internalGetInputComponent() { if (hasInputField()) return Optional.of(getInputField()); else if (hasInputButton()) return Optional.of(getInputButton()); else return Optional.empty(); } protected void updateComponentVisibleState() { boolean readOnly = isReadOnly(); boolean enabled = isEnabled(); internalGetInputComponent()// .ifPresent(component -> component.setVisible(enabled && !readOnly)); } @Override public void setReadOnly(boolean readOnly) { super.setReadOnly(readOnly); updateComponentVisibleState(); } @Override public void setEnabled(boolean enabled) { super.setEnabled(enabled); updateComponentVisibleState(); } @Override public void focus() { internalGetInputComponent()// .ifPresent(Focusable::focus); } @Override public boolean isEmpty() { return getValue().isEmpty(); } /** * Returns the current List of Tokenizable of the field. If no token is added, empty list is returned. */ @Override public List<Tokenizable> getValue() { if (value == null) { return getEmptyValue(); } return value; } @Override protected void doSetValue(List<Tokenizable> value) { this.value = value; identifierToTokenizable.clear(); List<Token> newList = new ArrayList<>(); if (value != null && !value.isEmpty()) { for (Tokenizable t : value) { Token token = convertTokenizableToToken(t); identifierToTokenizable.put(t.getIdentifier(), t); newList.add(token); } } getState().tokens = newList; } @Override public List<Tokenizable> getEmptyValue() { return Collections.emptyList(); } public Registration addTokenAddedListener(TokenAddedListener listener) { return addListener(TokenAddedEvent.class, listener, TokenAddedEvent.EVENT_METHOD); } public Registration addTokenRemovedListener(TokenRemovedListener listener) { return addListener(TokenRemovedEvent.class, listener, TokenRemovedEvent.EVENT_METHOD); } public Registration addTokenReorderedListener(TokenReorderedListener listener) { return addListener(TokenReorderedEvent.class, listener, TokenReorderedEvent.EVENT_METHOD); } private static <V> boolean isIndexOfALowerThanB(List<V> list, V a, V b) { int indexOfA = list.indexOf(a); int indexOfB = list.indexOf(b); return indexOfA < indexOfB; } /** * copied from AbstractComponentContainer * * @param c */ public void addComponent(Component c) { // Make sure we're not adding the component inside it's own content if (isOrHasAncestor(c)) { throw new IllegalArgumentException("Component cannot be added inside it's own content"); } if (c.getParent() != null) { // If the component already has a parent, try to remove it AbstractSingleComponentContainer.removeFromParent(c); } c.setParent(this); fireComponentAttachEvent(c); markAsDirty(); } /** * copied from AbstractComponentContainer * */ public void removeComponent(Component c) { if (equals(c.getParent())) { c.setParent(null); fireComponentDetachEvent(c); markAsDirty(); } } /** * copied from AbstractComponentContainer * */ protected void fireComponentAttachEvent(Component component) { fireEvent(new ComponentAttachEvent(this, component)); } /** * copied from AbstractComponentContainer * */ protected void fireComponentDetachEvent(Component component) { fireEvent(new ComponentDetachEvent(this, component)); } /** * copied from AbstractComponentContainer * */ private class ComponentIterator implements Iterator<Component>, Serializable { boolean first = (hasInputButton()) || (hasInputField()); @Override public boolean hasNext() { return first; } @Override public Component next() { first = false; if (hasInputField()) return getInputField(); else if (hasInputButton()) return getInputButton(); return null; } @Override public void remove() { throw new UnsupportedOperationException(); } } @SuppressWarnings("unchecked") public static <T> Iterator<T> emptyIterator() { return (Iterator<T>) EmptyIterator.EMPTY_ITERATOR; } private static class EmptyIterator<E> implements Iterator<E> { static final EmptyIterator<Object> EMPTY_ITERATOR = new EmptyIterator<Object>(); @Override public boolean hasNext() { return false; } @Override public E next() { throw new NoSuchElementException(); } @Override public void remove() { throw new IllegalStateException(); } } public class DefaultDeleteTokenAction extends TokenizableAction { public DefaultDeleteTokenAction() { super(TokenAction.DELETE_TOKEN_ACTION_IDENTIFIER, Integer.MAX_VALUE, VaadinIcons.MINUS_CIRCLE); } @Override public void onClick(Tokenizable tokenizable) { ExtTokenField.this.removeTokenizable(tokenizable); } } }