Java tutorial
/* * Copyright 2015 Max Schuster. * * 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 eu.maxschuster.vaadin.autocompletetextfield; import com.vaadin.annotations.JavaScript; import com.vaadin.annotations.StyleSheet; import com.vaadin.event.FieldEvents; import com.vaadin.server.AbstractJavaScriptExtension; import com.vaadin.server.ClientConnector; import com.vaadin.server.JsonCodec; import com.vaadin.server.Resource; import com.vaadin.ui.AbstractTextField; import com.vaadin.ui.JavaScriptFunction; import com.vaadin.ui.TextField; import elemental.json.Json; import elemental.json.JsonArray; import elemental.json.JsonObject; import elemental.json.JsonValue; import eu.maxschuster.vaadin.autocompletetextfield.shared.AutocompleteTextFieldExtensionState; import eu.maxschuster.vaadin.autocompletetextfield.shared.ScrollBehavior; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import java.util.StringTokenizer; import java.util.logging.Logger; /** * Extends an {@link AbstractTextField} with autocomplete (aka word completion) * functionality. * <p> * Uses a modified version of * <a href="https://goodies.pixabay.com/javascript/auto-complete/demo.html"> * autoComplete</a> originally developed by * <a href="https://pixabay.com/users/Simon/">Simon Steinberger</a> * </p> * <p> * {@code autoComplete} is released under the MIT License. * </p> * * @author Max Schuster * @see <a href="https://github.com/Pixabay/JavaScript-autoComplete"> * https://github.com/Pixabay/JavaScript-autoComplete</a> * @see <a href="https://github.com/maxschuster/JavaScript-autoComplete"> * https://github.com/maxschuster/JavaScript-autoComplete</a> */ @JavaScript({ "vaadin://addons/autocompletetextfield/dist/AutocompleteTextFieldExtension.min.js" }) @StyleSheet({ "vaadin://addons/autocompletetextfield/dist/AutocompleteTextFieldExtension.css" }) public class AutocompleteTextFieldExtension extends AbstractJavaScriptExtension { private static final long serialVersionUID = 1L; /** * A dummy {@link FieldEvents.TextChangeListener} to prevent the * {@link TextField} from reseting to an old value on the client-side. */ private final FieldEvents.TextChangeListener textChangeListener = new FieldEvents.TextChangeListener() { @Override public void textChange(FieldEvents.TextChangeEvent event) { } }; /** * Receives a search term from the client-side, executes the query and sends * the results to the JavaScript method "setSuggestions". * <p> * <b>Parameters:</b> * <ul> * <li>{@link JsonValue} {@code requestId} - Request id to send back to the * client-side.</li> * <li>{@link String} {@code term} - The search term.</li> * </ul> */ private final JavaScriptFunction querySuggestions = new JavaScriptFunction() { private static final long serialVersionUID = 1L; @Override public void call(JsonArray arguments) { JsonValue requestId = arguments.get(0); String term = arguments.getString(1); Set<AutocompleteSuggestion> suggestions = querySuggestions(term); JsonValue suggestionsAsJson = suggestionsToJson(suggestions); callFunction("setSuggestions", requestId, suggestionsAsJson); } }; /** * The max amount of suggestions send to the client-side */ private int suggestionLimit = 0; /** * The suggestion provider queried for suggesions */ protected AutocompleteSuggestionProvider suggestionProvider = null; /** * Construct a new {@link AutocompleteTextFieldExtension}. */ public AutocompleteTextFieldExtension() { init(null); } /** * Construct a new {@link AutocompleteTextFieldExtension} and extends the * given {@link AbstractTextField}. * * @param target The textfield to extend. */ public AutocompleteTextFieldExtension(AbstractTextField target) { init(target); } /** * Init stuff * * @param target The textfield to extend. */ private void init(AbstractTextField target) { addFunctions(); if (target != null) { extend(target); } } /** * Extends the given textfield. * * @param target The textfield to extend. */ public void extend(AbstractTextField target) { super.extend(target); // Add the dummy listener target.addTextChangeListener(textChangeListener); } @Override public AbstractTextField getParent() { return (AbstractTextField) super.getParent(); } @Override protected Class<? extends ClientConnector> getSupportedParentType() { return AbstractTextField.class; } @Override protected AutocompleteTextFieldExtensionState getState() { return (AutocompleteTextFieldExtensionState) super.getState(); } @Override protected AutocompleteTextFieldExtensionState getState(boolean markAsDirty) { return (AutocompleteTextFieldExtensionState) super.getState(markAsDirty); } @Override public Class<? extends AutocompleteTextFieldExtensionState> getStateType() { return AutocompleteTextFieldExtensionState.class; } /** * Adds all {@link JavaScriptFunction}s */ private void addFunctions() { addFunction("serverQuerySuggestions", querySuggestions); } /** * Creates an {@link AutocompleteQuery} from the given search term and the * internal {@link #suggestionLimit} and executes it. * * Returns a {@link Set} of {@link AutocompleteSuggestion}s with a * predictable iteration order. * * @param term The search term. * @return Result {@link Set} of {@link AutocompleteSuggestion}s with a * predictable iteration order. */ protected Set<AutocompleteSuggestion> querySuggestions(String term) { AutocompleteQuery autocompleteQuery = new AutocompleteQuery(this, term, suggestionLimit); return querySuggestions(autocompleteQuery); } /** * Executes the given {@link AutocompleteQuery} and makes sure the result is * within the boundries of the {@link AutocompleteQuery}'s limit. * <p> * Returns a {@link Set} of {@link AutocompleteSuggestion}s with a * predictable iteration order. * </p> * * @param query The Query. * @return Result {@link Set} of {@link AutocompleteSuggestion}s with a * predictable iteration order. */ protected Set<AutocompleteSuggestion> querySuggestions(AutocompleteQuery query) { if (suggestionProvider == null) { // no suggestionProvider set return Collections.emptySet(); } Collection<AutocompleteSuggestion> suggestions = suggestionProvider.querySuggestions(query); if (suggestions == null) { // suggestionProvider has returned null return Collections.emptySet(); } int limit = query.getLimit(); if (limit > 0 && limit < suggestions.size()) { // suggestionProvider has returned more results than allowed Set<AutocompleteSuggestion> subSet = new LinkedHashSet<AutocompleteSuggestion>(limit); for (AutocompleteSuggestion suggestion : suggestions) { subSet.add(suggestion); if (subSet.size() >= limit) { // size has reached the limit, ignore the following results // TODO: Should we log a message here? break; } } return subSet; } else { // suggestionProvider has respected the query limit return new LinkedHashSet<AutocompleteSuggestion>(suggestions); } } /** * Converts the given {@link AutocompleteSuggestion} into a * {@link JsonValue} representation because {@link JsonCodec} can't handle * it itself. * * @param suggestions Suggestions. * @return {@link JsonValue} representation. */ protected JsonValue suggestionsToJson(Set<AutocompleteSuggestion> suggestions) { JsonArray array = Json.createArray(); int i = 0; for (AutocompleteSuggestion suggestion : suggestions) { JsonObject object = Json.createObject(); String value = suggestion.getValue(); String description = suggestion.getDescription(); Resource icon = suggestion.getIcon(); List<String> styleNames = suggestion.getStyleNames(); object.put("value", value != null ? Json.create(value) : Json.createNull()); object.put("description", description != null ? Json.create(description) : Json.createNull()); if (icon != null) { String key = "icon-" + i; setResource(key, icon); object.put("icon", key); } else { object.put("icon", Json.createNull()); } if (styleNames != null) { JsonArray styleNamesArray = Json.createArray(); int s = 0; for (String styleName : styleNames) { if (styleName == null) { continue; } styleNamesArray.set(s++, styleName); } object.put("styleNames", styleNamesArray); } else { object.put("styleNames", Json.createNull()); } array.set(i++, object); } return array; } /** * Gets a {@link Logger} instance for this class. * * @return {@link Logger} instance for this class. */ private Logger getLogger() { return Logger.getLogger(AutocompleteTextFieldExtension.class.getName()); } /** * Gets the active {@link AutocompleteSuggestionProvider}. * * @return The active {@link AutocompleteSuggestionProvider}. */ public AutocompleteSuggestionProvider getSuggestionProvider() { return suggestionProvider; } /** * Sets the active {@link AutocompleteSuggestionProvider}. * * @param suggestionProvider The active * {@link AutocompleteSuggestionProvider}. */ public void setSuggestionProvider(AutocompleteSuggestionProvider suggestionProvider) { this.suggestionProvider = suggestionProvider; } /** * Sets the active {@link AutocompleteSuggestionProvider}. * * @param suggestionProvider The active * {@link AutocompleteSuggestionProvider}. * @return this (for method chaining) * @see * #setSuggestionProvider(eu.maxschuster.vaadin.autocompletetextfield.AutocompleteSuggestionProvider) */ public AutocompleteTextFieldExtension withSuggestionProvider( AutocompleteSuggestionProvider suggestionProvider) { setSuggestionProvider(suggestionProvider); return this; } /** * Gets the maximum number of suggestions that are allowed. * <p> * If the active {@link AutocompleteSuggestionProvider} returns more * suggestions than allowed, the excess suggestions will be ignored! * </p> * <p> * If {@code limit <= 0} the suggestions won't be limited. * </p> * * @return Maximum number of suggestions. */ public int getSuggestionLimit() { return suggestionLimit; } /** * Sets the maximum number of suggestions that are allowed. * <p> * If the active {@link AutocompleteSuggestionProvider} returns more * suggestions than allowed, the excess suggestions will be ignored! * </p> * <p> * If limit <= 0 the suggestions won't be limited. * </p> * * @param suggestionLimit Maximum number of suggestions. */ public void setSuggestionLimit(int suggestionLimit) { this.suggestionLimit = suggestionLimit; } /** * Sets the maximum number of suggestions that are allowed. * <p> * If the active {@link AutocompleteSuggestionProvider} returns more * suggestions than allowed, the excess suggestions will be ignored! * </p> * <p> * If limit <= 0 the suggestions won't be limited. * </p> * * @param suggestionLimit Maximum number of suggestions. * @return this (for method chaining) * @see #setSuggestionLimit(int) */ public AutocompleteTextFieldExtension withSuggestionLimit(int suggestionLimit) { setSuggestionLimit(suggestionLimit); return this; } /** * Checks whether items are rendered as HTML. * <p> * The default is false, i.e. to render that caption as plain text. * </p> * * @return true if the captions are rendered as HTML, false if rendered as * plain text. */ public boolean isItemAsHtml() { return getState(false).itemAsHtml; } /** * Sets whether the items are rendered as HTML. * <p> * If set to true, the items are rendered in the browser as HTML and the * developer is responsible for ensuring no harmful HTML is used. If set to * false, the caption is rendered in the browser as plain text. * </p> * <p> * The default is false, i.e. to render that caption as plain text. * </p> * * @param itemAsHtml true if the items are rendered as HTML, false if * rendered as plain text. */ public void setItemAsHtml(boolean itemAsHtml) { getState().itemAsHtml = itemAsHtml; } /** * Sets whether the items are rendered as HTML. * <p> * If set to true, the items are rendered in the browser as HTML and the * developer is responsible for ensuring no harmful HTML is used. If set to * false, the caption is rendered in the browser as plain text. * </p> * <p> * The default is false, i.e. to render that caption as plain text. * </p> * * @param itemAsHtml true if the items are rendered as HTML, false if * rendered as plain text. * @return this (for method chaining) * @see #setItemAsHtml(boolean) */ public AutocompleteTextFieldExtension withItemAsHtml(boolean itemAsHtml) { setItemAsHtml(itemAsHtml); return this; } /** * Gets the minimum number of characters (>=1) a user must type before a * search is performed. * * @return Minimum number of characters. */ public int getMinChars() { return getState(false).minChars; } /** * Sets the minimum number of characters (>=1) a user must type before a * search is performed. * * @param minChars Minimum number of characters. */ public void setMinChars(int minChars) { getState().minChars = minChars; } /** * Sets the minimum number of characters (>=1) a user must type before a * search is performed. * * @param minChars Minimum number of characters. * @return this (for method chaining) * @see #setMinChars(int) */ public AutocompleteTextFieldExtension withMinChars(int minChars) { getState().minChars = minChars; return this; } /** * Gets the delay in milliseconds between when a keystroke occurs and when a * search is performed. A zero-delay is more responsive, but can produce a * lot of load. * * @return Search delay in milliseconds. */ public int getDelay() { return getState(false).delay; } /** * Sets the delay in milliseconds between when a keystroke occurs and when a * search is performed. A zero-delay is more responsive, but can produce a * lot of load. * * @param delay Search delay in milliseconds. */ public void setDelay(int delay) { getState().delay = delay; } /** * Sets the delay in milliseconds between when a keystroke occurs and when a * search is performed. A zero-delay is more responsive, but can produce a * lot of load. * * @param delay Search delay in milliseconds. * @return this (for method chaining) * @see #setDelay(int) */ public AutocompleteTextFieldExtension withDelay(int delay) { setDelay(delay); return this; } /** * Checks if performed searches should be cached. * * @return Cache performed searches. */ public boolean isCache() { return getState(false).cache; } /** * Sets if performed searches should be cached. * * @param cache Cache performed searches. */ public void setCache(boolean cache) { getState().cache = cache; } /** * Sets if performed searches should be cached. * * @param cache Cache performed searches. * @return this (for method chaining) * @see #setCache(boolean) */ public AutocompleteTextFieldExtension withCache(boolean cache) { setCache(cache); return this; } /** * Gets all user-defined CSS style names of the dropdown menu container. If * the component has multiple style names defined, the return string is a * space-separated list of style names. * * @return The style name or a space-separated list of user-defined style * names of the dropdown menu container. */ public String getMenuStyleName() { List<String> styleNames = getState(false).menuStyleNames; String styleName = ""; if (styleNames != null && !styleNames.isEmpty()) { Iterator<String> i = styleNames.iterator(); while (i.hasNext()) { styleName += i.next(); if (i.hasNext()) { styleName += " "; } } } return styleName; } /** * Adds one or more style names to the dropdown menu container. Multiple * styles can be specified as a space-separated list of style names. The * style name will be rendered as a HTML class name, which can be used in a * CSS definition. * * @param styleName The new style to be added to the dropdown menu * container. */ public void addMenuStyleName(String styleName) { List<String> styleNames = getState().menuStyleNames; if (styleName == null || styleName.isEmpty()) { return; } if (styleName.contains(" ")) { StringTokenizer tokenizer = new StringTokenizer(styleName, " "); while (tokenizer.hasMoreTokens()) { addMenuStyleName(tokenizer.nextToken()); } return; } if (styleNames == null) { styleNames = new ArrayList<String>(); getState().menuStyleNames = styleNames; } styleNames.add(styleName); } /** * Adds one or more style names to the dropdown menu container. Multiple * styles can be specified as a space-separated list of style names. The * style name will be rendered as a HTML class name, which can be used in a * CSS definition. * * @param styleNames The new styles to be added to the dropdown menu * container. * @return this (for method chaining) * @see #addMenuStyleName(java.lang.String) */ public AutocompleteTextFieldExtension withMenuStyleName(String... styleNames) { for (String styleName : styleNames) { addMenuStyleName(styleName); } return this; } /** * Removes one or more style names from the dropdown menu container. * Multiple styles can be specified as a space-separated list of style * names. * * @param styleName The style name or style names to be removed. */ public void removeMenuStyleName(String styleName) { List<String> styleNames = getState().menuStyleNames; if (styleName == null || styleName.isEmpty() || styleNames == null) { return; } if (styleName.contains(" ")) { StringTokenizer tokenizer = new StringTokenizer(styleName, " "); while (tokenizer.hasMoreTokens()) { styleNames.remove(tokenizer.nextToken()); } } else { styleNames.remove(styleName); } } /** * Gets the {@link ScrollBehavior} that is used when the user scrolls the * page while the suggestion box is open. * * @return The {@link ScrollBehavior}. */ public ScrollBehavior getScrollBehavior() { return getState(false).scrollBehavior; } /** * Sets the {@link ScrollBehavior} that is used when the user scrolls the * page while the suggestion box is open. * * @param scrollBehavior The {@link ScrollBehavior}. */ public void setScrollBehavior(ScrollBehavior scrollBehavior) { getState().scrollBehavior = scrollBehavior; } /** * Sets the {@link ScrollBehavior} that is used when the user scrolls the * page while the suggestion box is open. * * @param scrollBehavior The {@link ScrollBehavior}. * @return this (for method chaining) * @see * #setScrollBehavior(eu.maxschuster.vaadin.autocompletetextfield.shared.ScrollBehavior) */ public AutocompleteTextFieldExtension withScrollBehavior(ScrollBehavior scrollBehavior) { setScrollBehavior(scrollBehavior); return this; } }