/*
 * 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 org.vaadin.addons.client;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import com.google.gwt.animation.client.AnimationScheduler;
import com.google.gwt.aria.client.CheckedValue;
import com.google.gwt.aria.client.Property;
import com.google.gwt.aria.client.Roles;
import com.google.gwt.aria.client.State;
import com.google.gwt.cell.client.IsCollapsible;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
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.Style;
import com.google.gwt.dom.client.Style.Display;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.dom.client.Style.Visibility;
import com.google.gwt.event.dom.client.BlurEvent;
import com.google.gwt.event.dom.client.BlurHandler;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.FocusEvent;
import com.google.gwt.event.dom.client.FocusHandler;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyDownEvent;
import com.google.gwt.event.dom.client.KeyDownHandler;
import com.google.gwt.event.dom.client.KeyUpEvent;
import com.google.gwt.event.dom.client.KeyUpHandler;
import com.google.gwt.event.dom.client.LoadEvent;
import com.google.gwt.event.dom.client.LoadHandler;
import com.google.gwt.event.dom.client.MouseDownEvent;
import com.google.gwt.event.dom.client.MouseDownHandler;
import com.google.gwt.event.logical.shared.CloseEvent;
import com.google.gwt.event.logical.shared.CloseHandler;
import com.google.gwt.i18n.client.HasDirection.Direction;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.Event.NativePreviewEvent;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.Window;
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.PopupPanel;
import com.google.gwt.user.client.ui.PopupPanel.PositionCallback;
import com.google.gwt.user.client.ui.SuggestOracle.Suggestion;
import com.google.gwt.user.client.ui.TextBox;
import com.google.gwt.user.client.ui.Widget;
import com.vaadin.client.ApplicationConnection;
import com.vaadin.client.BrowserInfo;
import com.vaadin.client.ComputedStyle;
import com.vaadin.client.DeferredWorker;
import com.vaadin.client.Focusable;
import com.vaadin.client.VConsole;
import com.vaadin.client.WidgetUtil;
import com.vaadin.client.ui.Field;
import com.vaadin.client.ui.Icon;
import com.vaadin.client.ui.SubPartAware;
import com.vaadin.client.ui.VCheckBox;
import com.vaadin.client.ui.VComboBox;
import com.vaadin.client.ui.VLazyExecutor;
import com.vaadin.client.ui.VOverlay;
import com.vaadin.client.ui.aria.AriaHelper;
import com.vaadin.client.ui.aria.HandlesAriaCaption;
import com.vaadin.client.ui.aria.HandlesAriaInvalid;
import com.vaadin.client.ui.aria.HandlesAriaRequired;
import com.vaadin.client.ui.menubar.MenuBar;
import com.vaadin.client.ui.menubar.MenuItem;
import com.vaadin.shared.AbstractComponentState;
import com.vaadin.shared.ui.ComponentStateUtil;
import com.vaadin.shared.util.SharedUtil;

/**
 * Client side implementation of the ComboBoxMultiselect component.
 *
 * TODO needs major refactoring (to be extensible etc)
 *
 * @since 8.0
 */
@SuppressWarnings("deprecation")
public class VComboBoxMultiselect extends Composite
		implements Field, KeyDownHandler, KeyUpHandler, ClickHandler, FocusHandler, BlurHandler, Focusable,
		SubPartAware, HandlesAriaCaption, HandlesAriaInvalid, HandlesAriaRequired, DeferredWorker, MouseDownHandler {

	/**
	 * Represents a suggestion in the suggestion popup box.
	 */
	public class ComboBoxMultiselectSuggestion implements Suggestion, Command {

		private final String key;
		private final String caption;
		private String untranslatedIconUri;
		private String style;
		private final VCheckBox checkBox;
		private Date lastExecution;

		/**
		 * Constructor for a single suggestion.
		 *
		 * @param key
		 *            item key, empty string for a special null item not in
		 *            container
		 * @param caption
		 *            item caption
		 * @param style
		 *            item style name, can be empty string
		 * @param untranslatedIconUri
		 *            icon URI or null
		 */
		public ComboBoxMultiselectSuggestion(String key, String caption, String style, String untranslatedIconUri) {
			this.key = key;
			this.caption = caption;
			this.style = style;
			this.untranslatedIconUri = untranslatedIconUri;

			this.checkBox = new VCheckBox();
			this.checkBox.setEnabled(false);
			State.HIDDEN.set(getCheckBoxElement(), true);
		}

		/**
		 * Gets the visible row in the popup as a HTML string. The string
		 * contains an image tag with the rows icon (if an icon has been
		 * specified) and the caption of the item
		 */

		@Override
		public String getDisplayString() {
			final StringBuilder sb = new StringBuilder();
			ApplicationConnection client = VComboBoxMultiselect.this.connector.getConnection();
			final Icon icon = client.getIcon(client.translateVaadinUri(this.untranslatedIconUri));
			if (icon != null) {
				sb.append(icon.getElement()
					.getString());
			}
			String content;
			if ("".equals(this.caption)) {
				// Ensure that empty options use the same height as other
				// options and are not collapsed (#7506)
				content = " ";
			} else {
				content = WidgetUtil.escapeHTML(this.caption);
			}
			sb.append("<span>" + content + "</span>");
			return sb.toString();
		}

		/**
		 * Get a string that represents this item. This is used in the text box.
		 */

		@Override
		public String getReplacementString() {
			return this.caption;
		}

		/**
		 * Get aria label for this item.
		 */
		public String getAriaLabel() {
			return this.caption;
		}

		/**
		 * Get the option key which represents the item on the server side.
		 *
		 * @return The key of the item
		 */
		public String getOptionKey() {
			return this.key;
		}

		/**
		 * Get the URI of the icon. Used when constructing the displayed option.
		 *
		 * @return real (translated) icon URI or null if none
		 */
		public String getIconUri() {
			ApplicationConnection client = VComboBoxMultiselect.this.connector.getConnection();
			return client.translateVaadinUri(this.untranslatedIconUri);
		}

		/**
		 * Gets the style set for this suggestion item. Styles are typically set
		 * by a server-side. The returned style is prefixed by
		 * <code>v-filterselect-item-</code>.
		 *
		 * @since 7.5.6
		 * @return the style name to use, or <code>null</code> to not apply any
		 *         custom style.
		 */
		public String getStyle() {
			return this.style;
		}

		/**
		 * Executes a selection of this item.
		 */

		@Override
		public void execute() {
			if (this.lastExecution == null) {
				this.lastExecution = new Date();
				onSuggestionSelected(this);
				return;
			}

			if (new Date().getTime() - this.lastExecution.getTime() > 300) {
				onSuggestionSelected(this);
			}
		}

		@Override
		public boolean equals(Object obj) {
			if (!(obj instanceof ComboBoxMultiselectSuggestion)) {
				return false;
			}
			ComboBoxMultiselectSuggestion other = (ComboBoxMultiselectSuggestion) obj;
			if (this.key == null && other.key != null || this.key != null && !this.key.equals(other.key)) {
				return false;
			}
			if (this.caption == null && other.caption != null
					|| this.caption != null && !this.caption.equals(other.caption)) {
				return false;
			}
			if (!SharedUtil.equals(this.untranslatedIconUri, other.untranslatedIconUri)) {
				return false;
			}

			return SharedUtil.equals(this.style, other.style);
		}
		
	@Override
	public int hashCode() {
	    final int prime = 31;
	    int result = 1;
	    result = prime * result + VComboBoxMultiselect.this.hashCode();
	    result = prime * result + ((key == null) ? 0 : key.hashCode());
	    result = prime * result + ((caption == null) ? 0 : caption.hashCode());
	    result = prime * result + ((untranslatedIconUri == null) ? 0 : untranslatedIconUri.hashCode());
	    result = prime * result + ((style == null) ? 0 : style.hashCode());
	    return result;
	}

		public VCheckBox getCheckBox() {
			return this.checkBox;
		}

		Element getCheckBoxElement() {
			return this.checkBox.getElement()
				.getFirstChildElement();
		}

		public boolean isChecked() {
			return getCheckBox().getValue();
		}

		public void setChecked(boolean checked) {
			MenuItem menuItem = VComboBoxMultiselect.this.suggestionPopup.getMenuItem(this);
			if (menuItem != null) {
				State.CHECKED.set(menuItem.getElement(), CheckedValue.of(checked));
			}

			getCheckBox().setValue(checked);
		}
	}

	/** An inner class that handles all logic related to mouse wheel. */
	private class MouseWheeler {

		/**
		 * A JavaScript function that handles the mousewheel DOM event, and
		 * passes it on to Java code.
		 *
		 * @see #createMousewheelListenerFunction(Widget)
		 */
		protected final JavaScriptObject mousewheelListenerFunction;

		protected MouseWheeler() {
			this.mousewheelListenerFunction = createMousewheelListenerFunction(VComboBoxMultiselect.this);
		}

		protected native JavaScriptObject createMousewheelListenerFunction(Widget widget)
		/*-{
		    return $entry(function(e) {
		        var deltaX = e.deltaX ? e.deltaX : -0.5*e.wheelDeltaX;
		        var deltaY = e.deltaY ? e.deltaY : -0.5*e.wheelDeltaY;
		
		        // IE8 has only delta y
		        if (isNaN(deltaY)) {
		            deltaY = -0.5*e.wheelDelta;
		        }
		
		        @org.vaadin.addons.client.VComboBoxMultiselect.JsniUtil::moveScrollFromEvent(*)(widget, deltaX, deltaY, e, e.deltaMode);
		    });
		}-*/;

		public void attachMousewheelListener(Element element) {
			attachMousewheelListenerNative(element, this.mousewheelListenerFunction);
		}

		public native void attachMousewheelListenerNative(Element element, JavaScriptObject mousewheelListenerFunction)
		/*-{
		    if (element.addEventListener) {
		        // FireFox likes "wheel", while others use "mousewheel"
		        var eventName = 'onmousewheel' in element ? 'mousewheel' : 'wheel';
		        element.addEventListener(eventName, mousewheelListenerFunction);
		    }
		}-*/;

		public void detachMousewheelListener(Element element) {
			detachMousewheelListenerNative(element, this.mousewheelListenerFunction);
		}

		public native void detachMousewheelListenerNative(Element element, JavaScriptObject mousewheelListenerFunction)
		/*-{
		    if (element.addEventListener) {
		        // FireFox likes "wheel", while others use "mousewheel"
		        var eventName = element.onwheel===undefined?"mousewheel":"wheel";
		        element.removeEventListener(eventName, mousewheelListenerFunction);
		    }
		}-*/;

	}

	/**
	 * A utility class that contains utility methods that are usually called
	 * from JSNI.
	 * <p>
	 * The methods are moved in this class to minimize the amount of JSNI code
	 * as much as feasible.
	 */
	static class JsniUtil {
		private JsniUtil() {
		}

		private static final int DOM_DELTA_PIXEL = 0;
		private static final int DOM_DELTA_LINE = 1;
		private static final int DOM_DELTA_PAGE = 2;

		// Rough estimation of item height
		private static final int SCROLL_UNIT_PX = 25;

		private static double deltaSum = 0;

		public static void moveScrollFromEvent(final Widget widget, final double deltaX, final double deltaY,
				final NativeEvent event, final int deltaMode) {
			if (!Double.isNaN(deltaY)) {
				VComboBoxMultiselect filterSelect = (VComboBoxMultiselect) widget;

				switch (deltaMode) {
				case DOM_DELTA_LINE:
					if (deltaY >= 0) {
						filterSelect.suggestionPopup.selectNextItem();
					} else {
						filterSelect.suggestionPopup.selectPrevItem();
					}
					break;
				case DOM_DELTA_PAGE:
					if (deltaY >= 0) {
						filterSelect.selectNextPage();
					} else {
						filterSelect.selectPrevPage();
					}
					break;
				case DOM_DELTA_PIXEL:
				default:
					// Accumulate dampened deltas
					deltaSum += Math.pow(Math.abs(deltaY), 0.7) * Math.signum(deltaY);

					// "Scroll" if change exceeds item height
					while (Math.abs(deltaSum) >= SCROLL_UNIT_PX) {
						if (!filterSelect.dataReceivedHandler.isWaitingForFilteringResponse()) {
							// Move selection if page flip is not in progress
							if (deltaSum < 0) {
								filterSelect.suggestionPopup.selectPrevItem();
							} else {
								filterSelect.suggestionPopup.selectNextItem();
							}
						}
						deltaSum -= SCROLL_UNIT_PX * Math.signum(deltaSum);
					}
					break;
				}
			}
		}
	}

	/**
	 * Represents the popup box with the selection options. Wraps a suggestion
	 * menu.
	 */
	public class SuggestionPopup extends VOverlay implements PositionCallback, CloseHandler<PopupPanel> {

		private static final int Z_INDEX = 30000;

		/** For internal use only. May be removed or replaced in the future. */
		public final SuggestionMenu menu;

		private final Element up = DOM.createDiv();
		private final Element down = DOM.createDiv();
		private final Element status = DOM.createDiv();

		private boolean isPagingEnabled = true;

		private long lastAutoClosed;

		private int popupOuterPadding = -1;

		private int topPosition;
		private int leftPosition;

		private final MouseWheeler mouseWheeler = new MouseWheeler();

		private boolean scrollPending = false;

		/**
		 * Default constructor
		 */
		SuggestionPopup() {
			super(true, false);
			debug("VComboBoxMultiselect.SP: constructor()");
			setOwner(VComboBoxMultiselect.this);
			this.menu = new SuggestionMenu();
			setWidget(this.menu);

			getElement().getStyle()
				.setZIndex(Z_INDEX);

			final Element root = getContainerElement();

			this.up.setInnerHTML("<span>Prev</span>");
			DOM.sinkEvents(this.up, Event.ONCLICK);

			this.down.setInnerHTML("<span>Next</span>");
			DOM.sinkEvents(this.down, Event.ONCLICK);

			root.insertFirst(this.up);
			root.appendChild(this.down);
			root.appendChild(this.status);

			DOM.sinkEvents(root, Event.ONMOUSEDOWN | Event.ONMOUSEWHEEL);
			addCloseHandler(this);

			Roles.getListRole()
				.set(getElement());

			setPreviewingAllNativeEvents(true);
		}

		public MenuItem getMenuItem(Command command) {
			for (MenuItem menuItem : this.menu.getItems()) {
				if (command.equals(menuItem.getCommand())) {
					return menuItem;
				}
			}
			return null;
		}

		@Override
		protected void onLoad() {
			super.onLoad();

			// Register mousewheel listener on paged select
			if (VComboBoxMultiselect.this.pageLength > 0) {
				this.mouseWheeler.attachMousewheelListener(getElement());
			}
		}

		@Override
		protected void onUnload() {
			this.mouseWheeler.detachMousewheelListener(getElement());
			super.onUnload();
		}

		/**
		 * Shows the popup where the user can see the filtered options that have
		 * been set with a call to
		 * {@link SuggestionMenu#setSuggestions(Collection)}.
		 *
		 * @param currentPage
		 *            The current page number
		 */
		public void showSuggestions(final int currentPage) {
			debug("VComboBoxMultiselect.SP: showSuggestions(" + currentPage + ", " + getTotalSuggestions() + ")");

			final SuggestionPopup popup = this;
			// Add TT anchor point
			getElement().setId("VAADIN_COMBOBOX_OPTIONLIST");

			this.leftPosition = getDesiredLeftPosition();
			this.topPosition = getDesiredTopPosition();

			setPopupPosition(this.leftPosition, this.topPosition);

			final int first = currentPage * VComboBoxMultiselect.this.pageLength + 1;
			final int last = first + VComboBoxMultiselect.this.currentSuggestions.size() - 1;
			final int matches = getTotalSuggestions();
			if (last > 0) {
				// nullsel not counted, as requested by user
				this.status.setInnerText((matches == 0 ? 0 : first) + "-" + last + "/" + matches);
			} else {
				this.status.setInnerText("");
			}
			// We don't need to show arrows or statusbar if there is
			// only one page
			if (matches <= VComboBoxMultiselect.this.pageLength || VComboBoxMultiselect.this.pageLength == 0) {
				setPagingEnabled(false);
			} else {
				setPagingEnabled(true);
			}
			setPrevButtonActive(first > 1);
			setNextButtonActive(last < matches);

			// clear previously fixed width
			this.menu.setWidth("");
			this.menu.getElement()
				.getFirstChildElement()
				.getStyle()
				.clearWidth();

			setPopupPositionAndShow(popup);
		}

		private int getDesiredTopPosition() {
			return toInt32(WidgetUtil.getBoundingClientRect(VComboBoxMultiselect.this.tb.getElement())
				.getBottom()) + Window.getScrollTop();
		}

		private int getDesiredLeftPosition() {
			return toInt32(WidgetUtil.getBoundingClientRect(VComboBoxMultiselect.this.getElement())
				.getLeft());
		}

		private native int toInt32(double val)
		/*-{
		    return val | 0;
		}-*/;

		/**
		 * Should the next page button be visible to the user?
		 *
		 * @param active
		 */
		private void setNextButtonActive(boolean active) {
			debug("VComboBoxMultiselect.SP: setNextButtonActive(" + active + ")");

			if (active) {
				DOM.sinkEvents(this.down, Event.ONCLICK);
				this.down.setClassName(VComboBoxMultiselect.this.getStylePrimaryName() + "-nextpage");
			} else {
				DOM.sinkEvents(this.down, 0);
				this.down.setClassName(VComboBoxMultiselect.this.getStylePrimaryName() + "-nextpage-off");
			}
		}

		/**
		 * Should the previous page button be visible to the user
		 *
		 * @param active
		 */
		private void setPrevButtonActive(boolean active) {
			debug("VComboBoxMultiselect.SP: setPrevButtonActive(" + active + ")");

			if (active) {
				DOM.sinkEvents(this.up, Event.ONCLICK);
				this.up.setClassName(VComboBoxMultiselect.this.getStylePrimaryName() + "-prevpage");
			} else {
				DOM.sinkEvents(this.up, 0);
				this.up.setClassName(VComboBoxMultiselect.this.getStylePrimaryName() + "-prevpage-off");
			}

		}

		/**
		 * Selects the next item in the filtered selections.
		 */
		public void selectNextItem() {
			debug("VComboBoxMultiselect.SP: selectNextItem()");

			final int index = this.menu.getSelectedIndex() + 1;
			if (this.menu.getItems()
				.size() > index) {
				selectItem(this.menu.getItems()
					.get(index));

			} else {
				selectNextPage();
			}
		}

		/**
		 * Selects the previous item in the filtered selections.
		 */
		public void selectPrevItem() {
			debug("VComboBoxMultiselect.SP: selectPrevItem()");

			final int index = this.menu.getSelectedIndex() - 1;
			if (index > -1) {
				selectItem(this.menu.getItems()
					.get(index));

			} else if (index == -1) {
				selectPrevPage();

			} else {
				if (!this.menu.getItems()
					.isEmpty()) {
					selectLastItem();
				}
			}
		}

		/**
		 * Select the first item of the suggestions list popup.
		 *
		 * @since 7.2.6
		 */
		public void selectFirstItem() {
			debug("VFS.SP: selectFirstItem()");
			int index = 0;
			List<MenuItem> items = menu.getItems();
			
			if (items != null) {
                		if (!items.isEmpty() && items.size() > 1) {
                		    if (VComboBoxMultiselect.this.showClearButton && VComboBoxMultiselect.this.showSelectAllButton) {
                			index = 2;
                		    } else if (VComboBoxMultiselect.this.showClearButton
                			    || VComboBoxMultiselect.this.showSelectAllButton) {
                			index = 1;
                		    }
                		}
        			
                		if (!items.isEmpty()) {
                		    selectItem(getFirstNotSelectedItem(index));
                		}
			}
		}

		/**
		 * returns first not checked item, if all are checked first item will be
		 * returned
		 * 
		 * @param mi
		 */
		private MenuItem getFirstNotSelectedItem(int index) {
			MenuItem found = getFirstNotSelectedItemRecursive(index);
			return found == null ? this.menu.getItems()
				.get(index) : found;
		}

		private MenuItem getFirstNotSelectedItemRecursive(int index) {
			if (index >= this.menu.getItems()
				.size()) {
				return null;
			}

			MenuItem mi = this.menu.getItems()
				.get(index);

			if (mi == null) {
				return null;
			}

			ComboBoxMultiselectSuggestion suggestion = (ComboBoxMultiselectSuggestion) mi.getCommand();

			if (suggestion.isChecked()) {
				return getFirstNotSelectedItemRecursive(index + 1);
			}
			return mi;
		}

		/**
		 * Select the last item of the suggestions list popup.
		 *
		 * @since 7.2.6
		 */
		public void selectLastItem() {
			debug("VComboBoxMultiselect.SP: selectLastItem()");
			selectItem(this.menu.getLastItem());
		}

		/*
		 * Sets the selected item in the popup menu.
		 */
		private void selectItem(final MenuItem newSelectedItem) {
			this.menu.selectItem(newSelectedItem);
		}

		/**
		 * Selects the item at the given index
		 * 
		 * @param index
		 *            item at index to select
		 */
		public void selectItemAtIndex(int index) {
			if (index == -1) {
				return;
			}
			if (VComboBoxMultiselect.this.showSelectAllButton) {
				index++;
			}
			if (VComboBoxMultiselect.this.showClearButton) {
				index++;
			}
			selectItem(this.menu.getItems()
				.get(index));
		}

		/*
		 * Using a timer to scroll up or down the pages so when we receive lots
		 * of consecutive mouse wheel events the pages does not flicker.
		 */
		private LazyPageScroller lazyPageScroller = new LazyPageScroller();

		private class LazyPageScroller extends Timer {
			private int pagesToScroll = 0;

			@Override
			public void run() {
				debug("VComboBoxMultiselect.SP.LPS: run()");

				if (this.pagesToScroll != 0) {
					if (!VComboBoxMultiselect.this.dataReceivedHandler.isWaitingForFilteringResponse()) {
						/*
						 * Avoid scrolling while we are waiting for a response
						 * because otherwise the waiting flag will be reset in
						 * the first response and the second response will be
						 * ignored, causing an empty popup...
						 *
						 * As long as the scrolling delay is suitable
						 * double/triple clicks will work by scrolling two or
						 * three pages at a time and this should not be a
						 * problem.
						 */
						// this makes sure that we don't close the popup
						VComboBoxMultiselect.this.dataReceivedHandler.setNavigationCallback(() -> {
						});
						filterOptions(	VComboBoxMultiselect.this.currentPage + this.pagesToScroll,
										VComboBoxMultiselect.this.lastFilter);
					}
					this.pagesToScroll = 0;
				}
			}

			public void scrollUp() {
				debug("VComboBoxMultiselect.SP.LPS: scrollUp()");
				if (VComboBoxMultiselect.this.pageLength > 0
						&& VComboBoxMultiselect.this.currentPage + this.pagesToScroll > 0) {
					this.pagesToScroll--;
					cancel();
					schedule(200);
				}
			}

			public void scrollDown() {
				debug("VComboBoxMultiselect.SP.LPS: scrollDown()");
				if (VComboBoxMultiselect.this.pageLength > 0
						&& getTotalSuggestions() > (VComboBoxMultiselect.this.currentPage + this.pagesToScroll + 1)
								* VComboBoxMultiselect.this.pageLength) {
					this.pagesToScroll++;
					cancel();
					schedule(200);
				}
			}
		}

		private void scroll(double deltaY) {
			boolean scrollActive = this.menu.isScrollActive();

			debug("VComboBoxMultiselect.SP: scroll() scrollActive: " + scrollActive);

			if (!scrollActive) {
				if (deltaY > 0d) {
					this.lazyPageScroller.scrollDown();
				} else {
					this.lazyPageScroller.scrollUp();
				}
			}
		}

		@Override
		public void onBrowserEvent(Event event) {
			debug("VComboBoxMultiselect.SP: onBrowserEvent()");

			if (event.getTypeInt() == Event.ONCLICK) {
				final Element target = DOM.eventGetTarget(event);
				if (target == this.up || target == DOM.getChild(this.up, 0)) {
					this.lazyPageScroller.scrollUp();
				} else if (target == this.down || target == DOM.getChild(this.down, 0)) {
					this.lazyPageScroller.scrollDown();
				}

			}

			/*
			 * Prevent the keyboard focus from leaving the textfield by
			 * preventing the default behaviour of the browser. Fixes #4285.
			 */
			handleMouseDownEvent(event);
		}

		@Override
		protected void onPreviewNativeEvent(NativePreviewEvent event) {
			// Check all events outside the combobox to see if they scroll the
			// page. We cannot use e.g. Window.addScrollListener() because the
			// scrolled element can be at any level on the page.

			// Normally this is only called when the popup is showing, but make
			// sure we don't accidentally process all events when not showing.
			if (!this.scrollPending && isShowing()
					&& !DOM.isOrHasChild(SuggestionPopup.this.getElement(), Element.as(event.getNativeEvent()
						.getEventTarget()))) {
				if (getDesiredLeftPosition() != this.leftPosition || getDesiredTopPosition() != this.topPosition) {
					updatePopupPositionOnScroll();
				}
			}

			super.onPreviewNativeEvent(event);
		}

		/**
		 * Make the popup follow the position of the ComboBoxMultiselect when
		 * the page is scrolled.
		 */
		private void updatePopupPositionOnScroll() {
			if (!this.scrollPending) {
				AnimationScheduler.get()
					.requestAnimationFrame(timestamp -> {
						if (isShowing()) {
							this.leftPosition = getDesiredLeftPosition();
							this.topPosition = getDesiredTopPosition();
							setPopupPosition(this.leftPosition, this.topPosition);
						}
						this.scrollPending = false;
					});
				this.scrollPending = true;
			}
		}

		/**
		 * Should paging be enabled. If paging is enabled then only a certain
		 * amount of items are visible at a time and a scrollbar or buttons are
		 * visible to change page. If paging is turned of then all options are
		 * rendered into the popup menu.
		 *
		 * @param paging
		 *            Should the paging be turned on?
		 */
		public void setPagingEnabled(boolean paging) {
			debug("VComboBoxMultiselect.SP: setPagingEnabled(" + paging + ")");
			if (this.isPagingEnabled == paging) {
				return;
			}
			if (paging) {
				this.down.getStyle()
					.clearDisplay();
				this.up.getStyle()
					.clearDisplay();
				this.status.getStyle()
					.clearDisplay();
			} else {
				this.down.getStyle()
					.setDisplay(Display.NONE);
				this.up.getStyle()
					.setDisplay(Display.NONE);
				this.status.getStyle()
					.setDisplay(Display.NONE);
			}
			this.isPagingEnabled = paging;
		}

		@Override
		public void setPosition(int offsetWidth, int offsetHeight) {
			debug("VComboBoxMultiselect.SP: setPosition(" + offsetWidth + ", " + offsetHeight + ")");

			int top = this.topPosition;
			int left = getPopupLeft();

			// reset menu size and retrieve its "natural" size
			this.menu.setHeight("");
			if (VComboBoxMultiselect.this.currentPage > 0 && !hasNextPage()) {
				// fix height to avoid height change when getting to last page
				this.menu.fixHeightTo(VComboBoxMultiselect.this.pageLength);
			}

			// ignoring the parameter as in V7
			offsetHeight = getOffsetHeight();
			final int desiredHeight = offsetHeight;
			final int desiredWidth = getMainWidth();

			debug("VComboBoxMultiselect.SP:     desired[" + desiredWidth + ", " + desiredHeight + "]");

			Element menuFirstChild = this.menu.getElement()
				.getFirstChildElement();
			int naturalMenuWidth;
			if (BrowserInfo.get()
				.isIE()
					&& BrowserInfo.get()
						.getBrowserMajorVersion() < 10) {
				// On IE 8 & 9 visibility is set to hidden and measuring
				// elements while they are hidden yields incorrect results
				String before = this.menu.getElement()
					.getParentElement()
					.getStyle()
					.getVisibility();
				this.menu.getElement()
					.getParentElement()
					.getStyle()
					.setVisibility(Visibility.VISIBLE);
				naturalMenuWidth = WidgetUtil.getRequiredWidth(menuFirstChild);
				this.menu.getElement()
					.getParentElement()
					.getStyle()
					.setProperty("visibility", before);
			} else {
				naturalMenuWidth = WidgetUtil.getRequiredWidth(menuFirstChild);
			}

			if (this.popupOuterPadding == -1) {
				this.popupOuterPadding = WidgetUtil.measureHorizontalPaddingAndBorder(this.menu.getElement(), 2)
						+ WidgetUtil
							.measureHorizontalPaddingAndBorder(	VComboBoxMultiselect.this.suggestionPopup.getElement(),
																0);
			}

			updateMenuWidth(desiredWidth, naturalMenuWidth);

			if (BrowserInfo.get()
				.isIE()
					&& BrowserInfo.get()
						.getBrowserMajorVersion() < 11) {
				// Must take margin,border,padding manually into account for
				// menu element as we measure the element child and set width to
				// the element parent

				double naturalMenuOuterWidth;
				if (BrowserInfo.get()
					.getBrowserMajorVersion() < 10) {
					// On IE 8 & 9 visibility is set to hidden and measuring
					// elements while they are hidden yields incorrect results
					String before = this.menu.getElement()
						.getParentElement()
						.getStyle()
						.getVisibility();
					this.menu.getElement()
						.getParentElement()
						.getStyle()
						.setVisibility(Visibility.VISIBLE);
					naturalMenuOuterWidth = WidgetUtil.getRequiredWidthDouble(menuFirstChild)
							+ getMarginBorderPaddingWidth(this.menu.getElement());
					this.menu.getElement()
						.getParentElement()
						.getStyle()
						.setProperty("visibility", before);
				} else {
					naturalMenuOuterWidth = WidgetUtil.getRequiredWidthDouble(menuFirstChild)
							+ getMarginBorderPaddingWidth(this.menu.getElement());
				}

				/*
				 * IE requires us to specify the width for the container
				 * element. Otherwise it will be 100% wide
				 */
				double rootWidth = Math.max(desiredWidth - this.popupOuterPadding, naturalMenuOuterWidth);
				getContainerElement().getStyle()
					.setWidth(rootWidth, Unit.PX);
			}

			final int textInputHeight = VComboBoxMultiselect.this.getOffsetHeight();
			final int textInputTopOnPage = VComboBoxMultiselect.this.tb.getAbsoluteTop();
			final int viewportOffset = Document.get()
				.getScrollTop();
			final int textInputTopInViewport = textInputTopOnPage - viewportOffset;
			final int textInputBottomInViewport = textInputTopInViewport + textInputHeight;

			final int spaceAboveInViewport = textInputTopInViewport;
			final int spaceBelowInViewport = Window.getClientHeight() - textInputBottomInViewport;

			if (spaceBelowInViewport < offsetHeight && spaceBelowInViewport < spaceAboveInViewport) {
				// popup on top of input instead
				if (offsetHeight > spaceAboveInViewport) {
					// Shrink popup height to fit above
					offsetHeight = spaceAboveInViewport;
				}
				top = textInputTopOnPage - offsetHeight;
			} else {
				// Show below, position calculated in showSuggestions for some
				// strange reason
				top = this.topPosition;
				offsetHeight = Math.min(offsetHeight, spaceBelowInViewport);
			}

			// fetch real width (mac FF bugs here due GWT popups overflow:auto )
			offsetWidth = menuFirstChild.getOffsetWidth();

			if (offsetHeight < desiredHeight) {
				int menuHeight = offsetHeight;
				if (this.isPagingEnabled) {
					menuHeight -= this.up.getOffsetHeight() + this.down.getOffsetHeight()
							+ this.status.getOffsetHeight();
				} else {
					final ComputedStyle s = new ComputedStyle(this.menu.getElement());
					menuHeight -= s.getIntProperty("marginBottom") + s.getIntProperty("marginTop");
				}

				// If the available page height is really tiny then this will be
				// negative and an exception will be thrown on setHeight.
				int menuElementHeight = this.menu.getItemOffsetHeight();
				if (menuHeight < menuElementHeight) {
					menuHeight = menuElementHeight;
				}

				this.menu.setHeight(menuHeight + "px");

				if (VComboBoxMultiselect.this.suggestionPopupWidth == null) {
					final int naturalMenuWidthPlusScrollBar = naturalMenuWidth + WidgetUtil.getNativeScrollbarSize();
					if (offsetWidth < naturalMenuWidthPlusScrollBar) {
						this.menu.setWidth(naturalMenuWidthPlusScrollBar + "px");
					}
				}
			}

			if (offsetWidth + left > Window.getClientWidth()) {
				left = VComboBoxMultiselect.this.getAbsoluteLeft() + VComboBoxMultiselect.this.getOffsetWidth()
						- offsetWidth;
				if (left < 0) {
					left = 0;
					this.menu.setWidth(Window.getClientWidth() + "px");

				}
			}

			setPopupPosition(left, top);
			this.menu.scrollSelectionIntoView();
		}

		/**
		 * Adds in-line CSS rules to the DOM according to the
		 * suggestionPopupWidth field
		 *
		 * @param desiredWidth
		 * @param naturalMenuWidth
		 */
		private void updateMenuWidth(final int desiredWidth, int naturalMenuWidth) {
			/**
			 * Three different width modes for the suggestion pop-up:
			 *
			 * 1. Legacy "null"-mode: width is determined by the longest item
			 * caption for each page while still maintaining minimum width of
			 * (desiredWidth - popupOuterPadding)
			 *
			 * 2. relative to the component itself
			 *
			 * 3. fixed width
			 */
			String width = "auto";
			if (VComboBoxMultiselect.this.suggestionPopupWidth == null) {
				if (naturalMenuWidth < desiredWidth) {
					naturalMenuWidth = desiredWidth - this.popupOuterPadding;
					width = desiredWidth - this.popupOuterPadding + "px";
				}
			} else if (isrelativeUnits(VComboBoxMultiselect.this.suggestionPopupWidth)) {
				float mainComponentWidth = desiredWidth - this.popupOuterPadding;
				// convert percentage value to fraction
				int widthInPx = Math
					.round(mainComponentWidth * asFraction(VComboBoxMultiselect.this.suggestionPopupWidth));
				width = widthInPx + "px";
			} else {
				// use as fixed width CSS definition
				width = WidgetUtil.escapeAttribute(VComboBoxMultiselect.this.suggestionPopupWidth);
			}
			this.menu.setWidth(width);
		}

		/**
		 * Returns the percentage value as a fraction, e.g. 42% -> 0.42
		 *
		 * @param percentage
		 */
		private float asFraction(String percentage) {
			String trimmed = percentage.trim();
			String withoutPercentSign = trimmed.substring(0, trimmed.length() - 1);
			float asFraction = Float.parseFloat(withoutPercentSign) / 100;
			return asFraction;
		}

		/**
		 * @since 7.7
		 * @param suggestionPopupWidth
		 * @return
		 */
		private boolean isrelativeUnits(String suggestionPopupWidth) {
			return suggestionPopupWidth.trim()
				.endsWith("%");
		}

		/**
		 * Was the popup just closed?
		 *
		 * @return true if popup was just closed
		 */
		public boolean isJustClosed() {
			debug("VComboBoxMultiselect.SP: justClosed()");
			final long now = new Date().getTime();
			return this.lastAutoClosed > 0 && now - this.lastAutoClosed < 200;
		}

		/*
		 * (non-Javadoc)
		 *
		 * @see
		 * com.google.gwt.event.logical.shared.CloseHandler#onClose(com.google
		 * .gwt.event.logical.shared.CloseEvent)
		 */

		@Override
		public void onClose(CloseEvent<PopupPanel> event) {
			debug("VComboBoxMultiselect.SP: onClose(" + event.isAutoClosed() + ")");

			if (event.isAutoClosed()) {
				this.lastAutoClosed = new Date().getTime();
			}
			
			connector.sendBlurEvent();			
		}

		/**
		 * Updates style names in suggestion popup to help theme building.
		 *
		 * @param componentState
		 *            shared state of the combo box
		 */
		public void updateStyleNames(AbstractComponentState componentState) {
			debug("VComboBoxMultiselect.SP: updateStyleNames()");
			setStyleName(VComboBoxMultiselect.this.getStylePrimaryName() + "-suggestpopup");
			this.menu.setStyleName(VComboBoxMultiselect.this.getStylePrimaryName() + "-suggestmenu");
			this.status.setClassName(VComboBoxMultiselect.this.getStylePrimaryName() + "-status");
			if (ComponentStateUtil.hasStyles(componentState)) {
				for (String style : componentState.styles) {
					if (!"".equals(style)) {
						addStyleDependentName(style);
					}
				}
			}
		}

	}

	/**
	 * The menu where the suggestions are rendered
	 */
	public class SuggestionMenu extends MenuBar implements SubPartAware, LoadHandler {

		private VLazyExecutor delayedImageLoadExecutioner = new VLazyExecutor(100, new ScheduledCommand() {

			@Override
			public void execute() {
				debug("VComboBoxMultiselect.SM: delayedImageLoadExecutioner()");
				if (VComboBoxMultiselect.this.suggestionPopup.isVisible()
						&& VComboBoxMultiselect.this.suggestionPopup.isAttached()) {
					setWidth("");
					getElement().getFirstChildElement()
						.getStyle()
						.clearWidth();
					VComboBoxMultiselect.this.suggestionPopup
						.setPopupPositionAndShow(VComboBoxMultiselect.this.suggestionPopup);
				}

			}
		});

		/**
		 * Default constructor
		 */
		SuggestionMenu() {
			super(true);
			debug("VComboBoxMultiselect.SM: constructor()");
			addDomHandler(this, LoadEvent.getType());

			setScrollEnabled(true);
		}

		/**
		 * Fixes menus height to use same space as full page would use. Needed
		 * to avoid height changes when quickly "scrolling" to last page.
		 * 
		 * @param pageItemsCount
		 *            height items count
		 */
		public void fixHeightTo(int pageItemsCount) {
			setHeight(getPreferredHeight(pageItemsCount));
		}

		/*
		 * Gets the preferred height of the menu including pageItemsCount items.
		 */
		String getPreferredHeight(int pageItemsCount) {
			if (VComboBoxMultiselect.this.currentSuggestions.size() > 0) {
				final int pixels = getPreferredHeight() / VComboBoxMultiselect.this.currentSuggestions.size()
						* pageItemsCount;
				return pixels + "px";
			} else {
				return "";
			}
		}

		/**
		 * Sets the suggestions rendered in the menu.
		 *
		 * @param suggestions
		 *            The suggestions to be rendered in the menu
		 */
		public void setSuggestions(Collection<ComboBoxMultiselectSuggestion> suggestions) {
			debug("VComboBoxMultiselect.SM: setSuggestions(" + suggestions + ")");

			clearItems();

			if (VComboBoxMultiselect.this.showClearButton) {
				MenuItem clearMenuItem = new MenuItem(VComboBoxMultiselect.this.clearButtonCaption, false,
						VComboBoxMultiselect.this.clearCmd);
				clearMenuItem.getElement()
					.setId(DOM.createUniqueId());
				clearMenuItem.addStyleName("align-center");
				Property.LABEL.set(clearMenuItem.getElement(), VComboBoxMultiselect.this.clearButtonCaption);
				this.addItem(clearMenuItem);
			}

			if (VComboBoxMultiselect.this.showSelectAllButton) {
				MenuItem selectAllMenuItem = new MenuItem(VComboBoxMultiselect.this.selectAllButtonCaption, false,
						VComboBoxMultiselect.this.selectAllCmd);
				selectAllMenuItem.getElement()
					.setId(DOM.createUniqueId());
				selectAllMenuItem.addStyleName("align-center");
				Property.LABEL.set(selectAllMenuItem.getElement(), VComboBoxMultiselect.this.selectAllButtonCaption);
				this.addItem(selectAllMenuItem);
			}

			final Iterator<ComboBoxMultiselectSuggestion> it = suggestions.iterator();
			int currentSuggestionIndex = VComboBoxMultiselect.this.currentPage * VComboBoxMultiselect.this.pageLength;
		
			while (it.hasNext()) {
				final ComboBoxMultiselectSuggestion suggestion = it.next();
                		final MenuItem mi = new MenuItem(suggestion.getDisplayString(), true, suggestion);

				String style = suggestion.getStyle();
				if (style != null) {
					mi.addStyleName("v-filterselect-item-" + style);
				}
				Roles.getListitemRole()
					.set(mi.getElement());

				WidgetUtil.sinkOnloadForImages(mi.getElement());

				boolean isSelected = VComboBoxMultiselect.this.selectedOptionKeys != null
						&& VComboBoxMultiselect.this.selectedOptionKeys.contains(suggestion.getOptionKey());
				
				suggestion.setChecked(isSelected);
				mi.getElement()
					.insertFirst(suggestion.getCheckBox()
						.getElement());

				Property.LABEL.set(mi.getElement(), suggestion.getAriaLabel());
				Property.SETSIZE.set(mi.getElement(), getTotalSuggestions());
				Property.POSINSET.set(mi.getElement(), ++currentSuggestionIndex);
				State.CHECKED.set(mi.getElement(), CheckedValue.of(isSelected));

				this.addItem(mi);
				
			}
			
			VComboBoxMultiselect.this.suggestionPopup.selectFirstItem();
		}

		/**
		 * Create/select a suggestion based on the used entered string. This
		 * method is called after filtering has completed with the given string.
		 *
		 * @param enteredItemValue
		 *            user entered string
		 */
		public void actOnEnteredValueAfterFiltering(String enteredItemValue) {
			debug("VComboBoxMultiselect.SM: doPostFilterSelectedItemAction()");
			final MenuItem item = getSelectedItem();

			// check for exact match in menu
			int p = getItems().size();
			if (p > 0) {
				for (int i = 0; i < p; i++) {
					final MenuItem potentialExactMatch = getItems().get(i);
					if (potentialExactMatch.getText()
						.equals(enteredItemValue)) {
						selectItem(potentialExactMatch);
						// do not send a value change event if null was and
						// stays selected
						if (!"".equals(enteredItemValue) || VComboBoxMultiselect.this.selectedOptionKeys != null
								&& !VComboBoxMultiselect.this.selectedOptionKeys.isEmpty()) {
							doItemAction(potentialExactMatch, true);
						}
						return;
					}
				}
			}
			if (VComboBoxMultiselect.this.allowNewItems) {
				if (!enteredItemValue.equals(VComboBoxMultiselect.this.lastNewItemString)) {
					// Store last sent new item string to avoid double sends
					VComboBoxMultiselect.this.lastNewItemString = enteredItemValue;
					VComboBoxMultiselect.this.connector.sendNewItem(enteredItemValue);
					// TODO try to select the new value if it matches what was
					// sent for V7 compatibility
				}
			} else if (item != null && !"".equals(VComboBoxMultiselect.this.lastFilter) && item.getText()
				.toLowerCase()
				.contains(VComboBoxMultiselect.this.lastFilter.toLowerCase())) {
				doItemAction(item, true);
			} else {
				// currentSuggestion has key="" for nullselection
				if (VComboBoxMultiselect.this.currentSuggestion != null
						&& !"".equals(VComboBoxMultiselect.this.currentSuggestion.key)) {
					// An item (not null) selected
					String text = VComboBoxMultiselect.this.currentSuggestion.getReplacementString();
					setText(text);
					VComboBoxMultiselect.this.selectedOptionKeys.add(VComboBoxMultiselect.this.currentSuggestion.key);
				}
			}
		}

		private static final String SUBPART_PREFIX = "item";

		@Override
		public com.google.gwt.user.client.Element getSubPartElement(String subPart) {
			int index = Integer.parseInt(subPart.substring(SUBPART_PREFIX.length()));

			MenuItem item = getItems().get(index);

			return item.getElement();
		}

		@Override
		public String getSubPartName(com.google.gwt.user.client.Element subElement) {
			if (!getElement().isOrHasChild(subElement)) {
				return null;
			}

			Element menuItemRoot = subElement;
			while (menuItemRoot != null && !menuItemRoot.getTagName()
				.equalsIgnoreCase("td")) {
				menuItemRoot = menuItemRoot.getParentElement()
					.cast();
			}
			// "menuItemRoot" is now the root of the menu item

			final int itemCount = getItems().size();
			for (int i = 0; i < itemCount; i++) {
				if (getItems().get(i)
					.getElement() == menuItemRoot) {
					String name = SUBPART_PREFIX + i;
					return name;
				}
			}
			return null;
		}

		@Override
		public void onLoad(LoadEvent event) {
			debug("VComboBoxMultiselect.SM: onLoad()");
			// Handle icon onload events to ensure shadow is resized
			// correctly
			this.delayedImageLoadExecutioner.trigger();

		}

		/**
		 * @deprecated use {@link SuggestionPopup#selectFirstItem()} instead.
		 */
		@Deprecated
		public void selectFirstItem() {
			debug("VComboBoxMultiselect.SM: selectFirstItem()");
			MenuItem firstItem = getItems().get(0);
			selectItem(firstItem);
		}

		/**
		 * @deprecated use {@link SuggestionPopup#selectLastItem()} instead.
		 */
		@Deprecated
		public void selectLastItem() {
			debug("VComboBoxMultiselect.SM: selectLastItem()");
			List<MenuItem> items = getItems();
			MenuItem lastItem = items.get(items.size() - 1);
			selectItem(lastItem);
		}

		/*
		 * Gets the height of one menu item.
		 */
		int getItemOffsetHeight() {
			List<MenuItem> items = getItems();
			return items != null && items.size() > 0 ? items.get(0)
				.getOffsetHeight() : 0;
		}

		/*
		 * Gets the width of one menu item.
		 */
		int getItemOffsetWidth() {
			List<MenuItem> items = getItems();
			return items != null && items.size() > 0 ? items.get(0)
				.getOffsetWidth() : 0;
		}

		/**
		 * Returns true if the scroll is active on the menu element or if the
		 * menu currently displays the last page with less items then the
		 * maximum visibility (in which case the scroll is not active, but the
		 * scroll is active for any other page in general).
		 *
		 * @since 7.2.6
		 */
		@Override
		public boolean isScrollActive() {
			String height = getElement().getStyle()
				.getHeight();
			String preferredHeight = getPreferredHeight(VComboBoxMultiselect.this.pageLength);

			return !(height == null || height.length() == 0 || height.equals(preferredHeight));
		}

		/**
		 * Highlight (select) an item matching the current text box content
		 * without triggering its action.
		 */
		public void highlightSelectedItem() {
			int p = getItems().size();
			// first check if there is a key match to handle items with
			// identical captions
			String currentKey = VComboBoxMultiselect.this.currentSuggestion != null
					? VComboBoxMultiselect.this.currentSuggestion.getOptionKey() : "";
			for (int i = 0; i < p; i++) {
				final MenuItem potentialExactMatch = getItems().get(i);
				if (currentKey.equals(getSuggestionKey(potentialExactMatch)) && VComboBoxMultiselect.this.tb.getText()
					.equals(potentialExactMatch.getText())) {
					selectItem(potentialExactMatch);
					VComboBoxMultiselect.this.tb.setSelectionRange(VComboBoxMultiselect.this.tb.getText()
						.length(), 0);
					return;
				}
			}
			// then check for exact string match in menu
			String text = VComboBoxMultiselect.this.tb.getText();
			for (int i = 0; i < p; i++) {
				final MenuItem potentialExactMatch = getItems().get(i);
				if (potentialExactMatch.getText()
					.equals(text)) {
					selectItem(potentialExactMatch);
					VComboBoxMultiselect.this.tb.setSelectionRange(VComboBoxMultiselect.this.tb.getText()
						.length(), 0);
					return;
				}
			}
		}
	}

	private String getSuggestionKey(MenuItem item) {
		if (item != null && item.getCommand() != null && item.getCommand() instanceof ComboBoxMultiselectSuggestion) {
			return ((ComboBoxMultiselectSuggestion) item.getCommand()).getOptionKey();
		}
		return "";
	}

	/**
	 * TextBox variant used as input element for filter selects, which prevents
	 * selecting text when disabled.
	 *
	 * @since 7.1.5
	 */
	public class FilterSelectTextBox extends TextBox {

		/**
		 * Creates a new filter select text box.
		 *
		 * @since 7.6.4
		 */
		public FilterSelectTextBox() {
			/*-
			 * Stop the browser from showing its own suggestion popup.
			 *
			 * Using an invalid value instead of "off" as suggested by
			 * https://developer.mozilla.org/en-US/docs/Web/Security/Securing_your_site/Turning_off_form_autocompletion
			 *
			 * Leaving the non-standard Safari options autocapitalize and
			 * autocorrect untouched since those do not interfere in the same
			 * way, and they might be useful in a combo box where new items are
			 * allowed.
			 */
			getElement().setAttribute("autocomplete", "nope");
		}

		/**
		 * Overridden to avoid selecting text when text input is disabled
		 */
		@Override
		public void setSelectionRange(int pos, int length) {
			if (VComboBoxMultiselect.this.textInputEnabled) {
				/*
				 * set selection range with a backwards direction: anchor at the
				 * back, focus at the front. This means that items that are too
				 * long to display will display from the start and not the end
				 * even on Firefox.
				 *
				 * We need the JSNI function to set selection range so that we
				 * can use the optional direction attribute to set the anchor to
				 * the end and the focus to the start. This makes Firefox work
				 * the same way as other browsers (#13477)
				 */
				WidgetUtil.setSelectionRange(getElement(), pos, length, "backward");

			} else {
				/*
				 * Setting the selectionrange for an uneditable textbox leads to
				 * unwanted behaviour when the width of the textbox is narrower
				 * than the width of the entry: the end of the entry is shown
				 * instead of the beginning. (see #13477)
				 *
				 * To avoid this, we set the caret to the beginning of the line.
				 */

				super.setSelectionRange(0, 0);
			}
		}

	}

	/**
	 * Handler receiving notifications from the connector and updating the
	 * widget state accordingly.
	 *
	 * This class is still subject to change and should not be considered as
	 * public stable API.
	 *
	 * @since 8.0
	 */
	public class DataReceivedHandler {

		private Runnable navigationCallback = null;
		/**
		 * Set true when popupopened has been clicked. Cleared on each
		 * UIDL-update. This handles the special case where are not filtering
		 * yet and the selected value has changed on the server-side. See #2119
		 * <p>
		 * For internal use only. May be removed or replaced in the future.
		 */
		private boolean popupOpenerClicked = false;
		/** For internal use only. May be removed or replaced in the future. */
		private boolean waitingForFilteringResponse = false;
		private boolean initialData = true;
		private String pendingUserInput = null;
		private boolean showPopup = false;
		private boolean blurUpdate = false;

		/**
		 * Called by the connector when new data for the last requested filter
		 * is received from the server.
		 */
		public void dataReceived() {
			if (this.initialData || this.blurUpdate) {
				VComboBoxMultiselect.this.suggestionPopup.menu
					.setSuggestions(VComboBoxMultiselect.this.currentSuggestions);
				performSelection(VComboBoxMultiselect.this.serverSelectedKeys, true, true);
				updateSuggestionPopupMinWidth();
				updateRootWidth();
				this.initialData = false;
				return;
			}

			VComboBoxMultiselect.this.suggestionPopup.menu.setSuggestions(VComboBoxMultiselect.this.currentSuggestions);
			if (!this.waitingForFilteringResponse && VComboBoxMultiselect.this.suggestionPopup.isAttached()) {
				this.showPopup = true;
			}
			if (this.showPopup) {
				VComboBoxMultiselect.this.suggestionPopup.showSuggestions(VComboBoxMultiselect.this.currentPage);
				if (VComboBoxMultiselect.this.currentSuggestion != null
						&& VComboBoxMultiselect.this.currentSuggestions != null) {

					VComboBoxMultiselect.this.suggestionPopup
						.selectItemAtIndex(VComboBoxMultiselect.this.currentSuggestions
							.indexOf(VComboBoxMultiselect.this.currentSuggestion));
				}
			}

			this.waitingForFilteringResponse = false;

			if (this.pendingUserInput != null) {
				VComboBoxMultiselect.this.suggestionPopup.menu.actOnEnteredValueAfterFiltering(this.pendingUserInput);
				this.pendingUserInput = null;
			} else if (this.popupOpenerClicked) {
				// make sure the current item is selected in the popup
				VComboBoxMultiselect.this.suggestionPopup.menu.highlightSelectedItem();
			} else {
				navigateItemAfterPageChange();
			}

			this.popupOpenerClicked = false;
		}

		/**
		 * Perform filtering with the user entered string and when the results
		 * are received, perform any action appropriate for the user input
		 * (select an item or create a new one).
		 *
		 * @param value
		 *            user input
		 */
		public void reactOnInputWhenReady(String value) {
			this.pendingUserInput = value;
			this.showPopup = false;
			filterOptions(0, value);
		}

		/*
		 * This method navigates to the proper item in the combobox page. This
		 * should be executed after setSuggestions() method which is called from
		 * VComboBoxMultiselect.showSuggestions(). ShowSuggestions() method
		 * builds the page content. As far as setSuggestions() method is called
		 * as deferred, navigateItemAfterPageChange method should be also be
		 * called as deferred. #11333
		 */
		private void navigateItemAfterPageChange() {
			if (this.navigationCallback != null) {
				// navigationCallback is not reset here but after any server
				// request in case you are in between two requests both changing
				// the page back and forth

				// we're paging w/ arrows
				this.navigationCallback.run();
				this.navigationCallback = null;
			}
		}

		/**
		 * Called by the connector any pending navigation operations should be
		 * cleared.
		 */
		public void clearPendingNavigation() {
			this.navigationCallback = null;
		}

		/**
		 * Set a callback that is invoked when a page change occurs if there
		 * have not been intervening requests to the server. The callback is
		 * reset when any additional request is made to the server.
		 *
		 * @param callback
		 *            method to call after filtering has completed
		 */
		public void setNavigationCallback(Runnable callback) {
			this.showPopup = true;
			this.navigationCallback = callback;
		}

		/**
		 * Record that the popup opener has been clicked and the popup should be
		 * opened on the next request.
		 *
		 * This handles the special case where are not filtering yet and the
		 * selected value has changed on the server-side. See #2119. The flag is
		 * cleared on each server reply.
		 */
		public void popupOpenerClicked() {
			this.popupOpenerClicked = true;
			this.showPopup = true;
		}

		/**
		 * Cancel a pending request to perform post-filtering actions.
		 */
		private void cancelPendingPostFiltering() {
			this.pendingUserInput = null;
		}

		/**
		 * Called by the connector when it has finished handling any reply from
		 * the server, regardless of what was updated.
		 */
		public void serverReplyHandled() {
			this.popupOpenerClicked = false;
			VComboBoxMultiselect.this.lastNewItemString = null;

			// if (!initDone) {
			// debug("VComboBoxMultiselect: init done, updating widths");
			// // Calculate minimum textarea width
			// updateSuggestionPopupMinWidth();
			// updateRootWidth();
			// initDone = true;
			// }
		}

		/**
		 * For internal use only - this method will be removed in the future.
		 *
		 * @return true if the combo box is waiting for a reply from the server
		 *         with a new page of data, false otherwise
		 */
		public boolean isWaitingForFilteringResponse() {
			return this.waitingForFilteringResponse;
		}

		/**
		 * For internal use only - this method will be removed in the future.
		 *
		 * @return true if the combo box is waiting for initial data from the
		 *         server, false otherwise
		 */
		public boolean isWaitingForInitialData() {
			return this.initialData;
		}

		/**
		 * Set a flag that filtering of options is pending a response from the
		 * server.
		 */
		private void startWaitingForFilteringResponse() {
			this.waitingForFilteringResponse = true;
		}

		/**
		 * Perform selection (if appropriate) based on a reply from the server.
		 * When this method is called, the suggestions have been reset if new
		 * ones (different from the previous list) were received from the
		 * server.
		 *
		 * @param selectedKeys
		 *            new selected keys or null if none given by the server
		 * @param selectedCaption
		 *            new selected item caption if sent by the server or null -
		 *            this is used when the selected item is not on the current
		 *            page
		 */
		public void updateSelectionFromServer(Set<String> selectedKeys, String selectedCaption) {
			boolean oldSuggestionTextMatchTheOldSelection = VComboBoxMultiselect.this.currentSuggestion != null
					&& VComboBoxMultiselect.this.currentSuggestion.getReplacementString()
						.equals(VComboBoxMultiselect.this.tb.getText());

			// VComboBoxMultiselect.this.serverSelectedKeys = selectedKeys;
			VComboBoxMultiselect.this.serverSelectedKeys.clear();
			if (selectedKeys != null) {
				for (String selectedKey : selectedKeys) {
					VComboBoxMultiselect.this.serverSelectedKeys.add(selectedKey);
				}
			}

			performSelection(	selectedKeys, oldSuggestionTextMatchTheOldSelection,
								!isWaitingForFilteringResponse() || this.popupOpenerClicked);

			cancelPendingPostFiltering();

			if (!VComboBoxMultiselect.this.suggestionPopup.isShowing()) {
				setSelectedCaption(selectedCaption);
			}
		}

		public void setBlurUpdate(boolean blurUpdate) {
			this.blurUpdate = blurUpdate;
		}

	}

	// TODO decide whether this should change - affects themes and v7
	public static final String CLASSNAME = "v-filterselect";
	private static final String STYLE_NO_INPUT = "no-input";

	/** For internal use only. May be removed or replaced in the future. */
	public int pageLength;

	/** For internal use only. May be removed or replaced in the future. */
	public String clearButtonCaption = "clear";

	/** For internal use only. May be removed or replaced in the future. */
	public boolean showClearButton;

	/** For internal use only. May be removed or replaced in the future. */
	public String selectAllButtonCaption = "select all";

	/** For internal use only. May be removed or replaced in the future. */
	public boolean showSelectAllButton;

	/** For internal use only. May be removed or replaced in the future. */
	Command clearCmd = new Command() {

		@Override
		public void execute() {
			debug("VFS: clearCmd()");

			String filter = VComboBoxMultiselect.this.tb.getText();
			VComboBoxMultiselect.this.connector.clear(filter);

			setText("");
			filterOptions(0, "");
		}
	};

	/** For internal use only. May be removed or replaced in the future. */
	Command selectAllCmd = new Command() {

		@Override
		public void execute() {
			debug("VFS: selectAllCmd()");
			String filter = VComboBoxMultiselect.this.tb.getText();
			VComboBoxMultiselect.this.connector.selectAll(filter);

			setText("");
			filterOptions(0, "");
		}
	};

	private boolean enableDebug = false;

	private final FlowPanel panel = new FlowPanel();

	/**
	 * The text box where the filter is written
	 * <p>
	 * For internal use only. May be removed or replaced in the future.
	 */
	public final TextBox tb;

	/** For internal use only. May be removed or replaced in the future. */
	public final SuggestionPopup suggestionPopup;

	/**
	 * Used when measuring the width of the popup
	 */
	private final HTML popupOpener = new HTML("");

	private class IconWidget extends Widget {
		IconWidget(Icon icon) {
			setElement(icon.getElement());
		}
	}

	private IconWidget selectedItemIcon;

	/** For internal use only. May be removed or replaced in the future. */
	public ComboBoxMultiselectConnector connector;

	/** For internal use only. May be removed or replaced in the future. */
	public int currentPage;

	/**
	 * A collection of available suggestions (options) as received from the
	 * server.
	 * <p>
	 * For internal use only. May be removed or replaced in the future.
	 */
	public final List<ComboBoxMultiselectSuggestion> currentSuggestions = new ArrayList<>();

	/** For internal use only. May be removed or replaced in the future. */
	public Set<String> serverSelectedKeys = new LinkedHashSet<>();
	/** For internal use only. May be removed or replaced in the future. */
	public Set<String> selectedOptionKeys = new LinkedHashSet<>();

	/** For internal use only. May be removed or replaced in the future. */
	public boolean initDone = false;

	/** For internal use only. May be removed or replaced in the future. */
	public String lastFilter = "";

	/**
	 * The current suggestion selected from the dropdown. This is one of the
	 * values in currentSuggestions except when filtering, in this case
	 * currentSuggestion might not be in currentSuggestions.
	 * <p>
	 * For internal use only. May be removed or replaced in the future.
	 */
	public ComboBoxMultiselectSuggestion currentSuggestion;

	/** For internal use only. May be removed or replaced in the future. */
	public boolean allowNewItems;

	/** Total number of suggestions, excluding null selection item. */
	private int totalSuggestions;

	/** For internal use only. May be removed or replaced in the future. */
	public boolean enabled;

	/** For internal use only. May be removed or replaced in the future. */
	public boolean readonly;

	/** For internal use only. May be removed or replaced in the future. */
	public String inputPrompt = "";

	/** For internal use only. May be removed or replaced in the future. */
	public int suggestionPopupMinWidth = 0;

	public String suggestionPopupWidth = null;

	private int popupWidth = -1;
	/**
	 * Stores the last new item string to avoid double submissions. Cleared on
	 * uidl updates.
	 * <p>
	 * For internal use only. May be removed or replaced in the future.
	 */
	public String lastNewItemString;

	/** For internal use only. May be removed or replaced in the future. */
	public boolean focused = false;

	/**
	 * If set to false, the component should not allow entering text to the
	 * field even for filtering.
	 */
	private boolean textInputEnabled = true;

	private final DataReceivedHandler dataReceivedHandler = new DataReceivedHandler();

	/**
	 * Default constructor.
	 */
	public VComboBoxMultiselect() {
		this.tb = createTextBox();
		this.suggestionPopup = createSuggestionPopup();

		this.popupOpener.addMouseDownHandler(VComboBoxMultiselect.this);
		Roles.getButtonRole()
			.setAriaHiddenState(this.popupOpener.getElement(), true);
		Roles.getButtonRole()
			.set(this.popupOpener.getElement());

		this.panel.add(this.tb);
		this.panel.add(this.popupOpener);
		initWidget(this.panel);
		Roles.getComboboxRole()
			.set(this.panel.getElement());

		this.tb.addKeyDownHandler(this);
		this.tb.addKeyUpHandler(this);

		this.tb.addFocusHandler(this);
		this.tb.addBlurHandler(this);

		this.panel.addDomHandler(this, ClickEvent.getType());

		setStyleName(CLASSNAME);

		sinkEvents(Event.ONPASTE);
	}

	private static double getMarginBorderPaddingWidth(Element element) {
		final ComputedStyle s = new ComputedStyle(element);
		return s.getMarginWidth() + s.getBorderWidth() + s.getPaddingWidth();

	}

	/*
	 * (non-Javadoc)
	 *
	 * @see
	 * com.google.gwt.user.client.ui.Composite#onBrowserEvent(com.google.gwt
	 * .user.client.Event)
	 */
	@Override
	public void onBrowserEvent(Event event) {
		super.onBrowserEvent(event);

		if (event.getTypeInt() == Event.ONPASTE) {
			if (this.textInputEnabled) {
				filterOptions(this.currentPage);
			}
		}
	}

	/**
	 * This method will create the TextBox used by the VComboBoxMultiselect
	 * instance. It is invoked during the Constructor and should only be
	 * overridden if a custom TextBox shall be used. The overriding method
	 * cannot use any instance variables.
	 *
	 * @since 7.1.5
	 * @return TextBox instance used by this VComboBoxMultiselect
	 */
	protected TextBox createTextBox() {
		return new FilterSelectTextBox();
	}

	/**
	 * This method will create the SuggestionPopup used by the
	 * VComboBoxMultiselect instance. It is invoked during the Constructor and
	 * should only be overridden if a custom SuggestionPopup shall be used. The
	 * overriding method cannot use any instance variables.
	 *
	 * @since 7.1.5
	 * @return SuggestionPopup instance used by this VComboBoxMultiselect
	 */
	protected SuggestionPopup createSuggestionPopup() {
		return new SuggestionPopup();
	}

	@Override
	public void setStyleName(String style) {
		super.setStyleName(style);
		updateStyleNames();
	}

	@Override
	public void setStylePrimaryName(String style) {
		super.setStylePrimaryName(style);
		updateStyleNames();
	}

	protected void updateStyleNames() {
		this.tb.setStyleName(getStylePrimaryName() + "-input");
		this.popupOpener.setStyleName(getStylePrimaryName() + "-button");
		this.suggestionPopup.setStyleName(getStylePrimaryName() + "-suggestpopup");
	}

	/**
	 * Does the Select have more pages?
	 *
	 * @return true if a next page exists, else false if the current page is the
	 *         last page
	 */
	public boolean hasNextPage() {
		return this.pageLength > 0 && getTotalSuggestions() > (this.currentPage + 1) * this.pageLength;
	}

	/**
	 * Filters the options at a certain page. Uses the text box input as a
	 * filter and ensures the popup is opened when filtering results are
	 * available.
	 *
	 * @param page
	 *            The page which items are to be filtered
	 */
	public void filterOptions(int page) {
		this.dataReceivedHandler.popupOpenerClicked();
		filterOptions(page, this.tb.getText());
	}

	/**
	 * Filters the options at certain page using the given filter.
	 *
	 * @param page
	 *            The page to filter
	 * @param filter
	 *            The filter to apply to the components
	 */
	public void filterOptions(int page, String filter) {
		debug("VComboBoxMultiselect: filterOptions(" + page + ", " + filter + ")");

		if (filter.equals(this.lastFilter) && this.currentPage == page && this.suggestionPopup.isAttached()) {
			// already have the page
			this.dataReceivedHandler.dataReceived();
			return;
		}

		if (!filter.equals(this.lastFilter)) {
			// when filtering, let the server decide the page unless we've
			// set the filter to empty and explicitly said that we want to see
			// the results starting from page 0.
			if ("".equals(filter) && page != 0) {
				// let server decide
				page = -1;
			} else {
				page = 0;
			}
		}

		this.dataReceivedHandler.startWaitingForFilteringResponse();
		this.connector.requestPage(page, filter);

		this.lastFilter = filter;

		// If the data was updated from cache, the page has been updated too, if
		// not, update
		if (this.dataReceivedHandler.isWaitingForFilteringResponse()) {
			this.currentPage = page;
		}
	}

	/** For internal use only. May be removed or replaced in the future. */
	public void updateReadOnly() {
		debug("VComboBoxMultiselect: updateReadOnly()");
		this.tb.setReadOnly(this.readonly || !this.textInputEnabled);
	}

	public void setTextInputAllowed(boolean textInputAllowed) {
		debug("VComboBoxMultiselect: setTextInputAllowed()");
		// Always update styles as they might have been overwritten
		if (textInputAllowed) {
			removeStyleDependentName(STYLE_NO_INPUT);
			Roles.getTextboxRole()
				.removeAriaReadonlyProperty(this.tb.getElement());
		} else {
			addStyleDependentName(STYLE_NO_INPUT);
			Roles.getTextboxRole()
				.setAriaReadonlyProperty(this.tb.getElement(), true);
		}

		if (this.textInputEnabled == textInputAllowed) {
			return;
		}

		this.textInputEnabled = textInputAllowed;
		updateReadOnly();
	}

	/**
	 * Sets the text in the text box.
	 *
	 * @param text
	 *            the text to set in the text box
	 */
	public void setText(final String text) {
		/**
		 * To leave caret in the beginning of the line. SetSelectionRange
		 * wouldn't work on IE (see #13477)
		 */
		Direction previousDirection = this.tb.getDirection();
		this.tb.setDirection(Direction.RTL);
		this.tb.setText(text);
		this.tb.setDirection(previousDirection);
	}

	/**
	 * Set or reset the placeholder attribute for the text field.
	 *
	 * @param placeholder
	 *            new placeholder string or null for none
	 */
	public void setPlaceholder(String placeholder) {
		this.inputPrompt = placeholder;
		updatePlaceholder();
	}

	/**
	 * Update placeholder visibility (hidden when read-only or disabled).
	 */
	public void updatePlaceholder() {
		if (this.inputPrompt != null && this.enabled && !this.readonly) {
			this.tb.getElement()
				.setAttribute("placeholder", this.inputPrompt);
		} else {
			this.tb.getElement()
				.removeAttribute("placeholder");
		}
	}

	/**
	 * Triggered when a suggestion is selected.
	 *
	 * @param suggestion
	 *            The suggestion that just got selected.
	 */
	public void onSuggestionSelected(ComboBoxMultiselectSuggestion suggestion) {
		debug("VComboBoxMultiselect: onSuggestionSelected(" + suggestion.caption + ": " + suggestion.key + ")");

		this.dataReceivedHandler.cancelPendingPostFiltering();

		this.currentSuggestion = suggestion;
		String newKey = suggestion.getOptionKey();
		
		if (!this.selectedOptionKeys.contains(newKey)) {
			this.selectedOptionKeys.add(newKey);
			this.connector.sendSelections(new HashSet<>(Arrays.asList(newKey)), new HashSet<>());
		} else {
			this.selectedOptionKeys.remove(newKey);
			this.connector.sendSelections(new HashSet<>(), new HashSet<>(Arrays.asList(newKey)));
		}
	}

	/**
	 * Perform selection based on a message from the server.
	 *
	 * The special case where the selected item is not on the current page is
	 * handled separately by the caller.
	 *
	 * @param selectedKeys
	 *            non-empty selected item keys
	 * @param forceUpdateText
	 *            true to force the text box value to match the suggestion text
	 * @param updatePromptAndSelectionIfMatchFound
	 */
	private void performSelection(Set<String> selectedKeys, boolean forceUpdateText,
			boolean updatePromptAndSelectionIfMatchFound) {
		this.selectedOptionKeys = selectedKeys;

		// some item selected
		for (ComboBoxMultiselectSuggestion suggestion : this.currentSuggestions) {
			String suggestionKey = suggestion.getOptionKey();
			if (selectedKeys == null || !selectedKeys.contains(suggestionKey)) {
				continue;
			}
			// at this point, suggestion key matches the new selection key
			if (updatePromptAndSelectionIfMatchFound && !this.selectedOptionKeys.contains(suggestionKey)
					|| suggestion.getReplacementString()
						.equals(this.tb.getText())
					|| forceUpdateText) {
				this.selectedOptionKeys.add(suggestionKey);
			}
		}
	}

	private void forceReflow() {
		WidgetUtil.setStyleTemporarily(this.tb.getElement(), "zoom", "1");
	}

	/**
	 * Positions the icon vertically in the middle. Should be called after the
	 * icon has loaded
	 */
	private void updateSelectedIconPosition() {
		// Position icon vertically to middle
		int availableHeight = 0;
		availableHeight = getOffsetHeight();

		int iconHeight = WidgetUtil.getRequiredHeight(this.selectedItemIcon);
		int marginTop = (availableHeight - iconHeight) / 2;
		this.selectedItemIcon.getElement()
			.getStyle()
			.setMarginTop(marginTop, Unit.PX);
	}

	private static Set<Integer> navigationKeyCodes = new HashSet<>();
	static {
		navigationKeyCodes.add(KeyCodes.KEY_DOWN);
		navigationKeyCodes.add(KeyCodes.KEY_UP);
		navigationKeyCodes.add(KeyCodes.KEY_PAGEDOWN);
		navigationKeyCodes.add(KeyCodes.KEY_PAGEUP);
		navigationKeyCodes.add(KeyCodes.KEY_ENTER);
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see
	 * com.google.gwt.event.dom.client.KeyDownHandler#onKeyDown(com.google.gwt
	 * .event.dom.client.KeyDownEvent)
	 */

	@Override
	public void onKeyDown(KeyDownEvent event) {
		if (this.enabled && !this.readonly) {
			int keyCode = event.getNativeKeyCode();

			debug("VComboBoxMultiselect: key down: " + keyCode);

			if (this.dataReceivedHandler.isWaitingForFilteringResponse() && navigationKeyCodes.contains(keyCode)
					&& (!this.allowNewItems || keyCode != KeyCodes.KEY_ENTER)) {
				/*
				 * Keyboard navigation events should not be handled while we are
				 * waiting for a response. This avoids flickering, disappearing
				 * items, wrongly interpreted responses and more.
				 */
				debug("Ignoring " + keyCode + " because we are waiting for a filtering response");

				DOM.eventPreventDefault(DOM.eventGetCurrentEvent());
				event.stopPropagation();
				return;
			}

			if (this.suggestionPopup.isAttached()) {
				debug("Keycode " + keyCode + " target is popup");
				popupKeyDown(event);
			} else {
				debug("Keycode " + keyCode + " target is text field");
				inputFieldKeyDown(event);
			}
		}
	}

	private void debug(String string) {
		if (this.enableDebug) {
			VConsole.error(string);
		}
	}

	/**
	 * Triggered when a key is pressed in the text box
	 *
	 * @param event
	 *            The KeyDownEvent
	 */
	private void inputFieldKeyDown(KeyDownEvent event) {
		debug("VComboBoxMultiselect: inputFieldKeyDown(" + event.getNativeKeyCode() + ")");

		switch (event.getNativeKeyCode()) {
		case KeyCodes.KEY_DOWN:
		case KeyCodes.KEY_UP:
		case KeyCodes.KEY_PAGEDOWN:
		case KeyCodes.KEY_PAGEUP:
			// open popup as from gadget
			filterOptions(-1, "");
			this.tb.selectAll();
			this.dataReceivedHandler.popupOpenerClicked();
			break;
		case KeyCodes.KEY_ENTER:
			/*
			 * This only handles the case when new items is allowed, a text is
			 * entered, the popup opener button is clicked to close the popup
			 * and enter is then pressed (see #7560).
			 */
			if (!this.allowNewItems) {
				return;
			}

			if (this.currentSuggestion != null && this.tb.getText()
				.equals(this.currentSuggestion.getReplacementString())) {
				// Retain behavior from #6686 by returning without stopping
				// propagation if there's nothing to do
				return;
			}
			this.dataReceivedHandler.reactOnInputWhenReady(this.tb.getText());

			event.stopPropagation();
			break;
		}

	}

	/**
	 * Triggered when a key was pressed in the suggestion popup.
	 *
	 * @param event
	 *            The KeyDownEvent of the key
	 */
	private void popupKeyDown(KeyDownEvent event) {
		debug("VComboBoxMultiselect: popupKeyDown(" + event.getNativeKeyCode() + ")");

		// Propagation of handled events is stopped so other handlers such as
		// shortcut key handlers do not also handle the same events.
		switch (event.getNativeKeyCode()) {
		case KeyCodes.KEY_DOWN:
			this.suggestionPopup.selectNextItem();

			DOM.eventPreventDefault(DOM.eventGetCurrentEvent());
			event.stopPropagation();
			break;
		case KeyCodes.KEY_UP:
			this.suggestionPopup.selectPrevItem();

			DOM.eventPreventDefault(DOM.eventGetCurrentEvent());
			event.stopPropagation();
			break;
		case KeyCodes.KEY_PAGEDOWN:
			selectNextPage();
			event.stopPropagation();
			break;
		case KeyCodes.KEY_PAGEUP:
			selectPrevPage();
			event.stopPropagation();
			break;
		case KeyCodes.KEY_ESCAPE:
			reset();
			DOM.eventPreventDefault(DOM.eventGetCurrentEvent());
			event.stopPropagation();
			break;
		case KeyCodes.KEY_TAB:
		case KeyCodes.KEY_ENTER:

			// queue this, may be cancelled by selection
			int selectedIndex = this.suggestionPopup.menu.getSelectedIndex();
			if (!this.allowNewItems && selectedIndex != -1) {

				debug("index before: " + selectedIndex);
				if (this.showClearButton) {
					selectedIndex = selectedIndex - 1;
				}
				if (this.showSelectAllButton) {
					selectedIndex = selectedIndex - 1;
				}

				debug("index after: " + selectedIndex);
				if (selectedIndex == -2) {
					this.clearCmd.execute();
				} else if (selectedIndex == -1) {
					if (this.showSelectAllButton) {
						this.selectAllCmd.execute();
					} else {
						this.clearCmd.execute();
					}
				}

				debug("entered suggestion: " + this.currentSuggestions.get(selectedIndex).caption);
				onSuggestionSelected(this.currentSuggestions.get(selectedIndex));
			} else {
				this.dataReceivedHandler.reactOnInputWhenReady(this.tb.getText());
			}

			event.stopPropagation();
			break;
		}
	}

	/*
	 * Show the prev page.
	 */
	private void selectPrevPage() {
		if (this.currentPage > 0) {
			this.dataReceivedHandler.setNavigationCallback(() -> this.suggestionPopup.selectLastItem());
			filterOptions(this.currentPage - 1, this.lastFilter);
		}
	}

	/*
	 * Show the next page.
	 */
	private void selectNextPage() {
		if (hasNextPage()) {
			this.dataReceivedHandler.setNavigationCallback(() -> this.suggestionPopup.selectFirstItem());
			filterOptions(this.currentPage + 1, this.lastFilter);
		}
	}

	/**
	 * Triggered when a key was depressed.
	 *
	 * @param event
	 *            The KeyUpEvent of the key depressed
	 */
	@Override
	public void onKeyUp(KeyUpEvent event) {
		debug("VComboBoxMultiselect: onKeyUp(" + event.getNativeKeyCode() + ")");

		if (this.enabled && !this.readonly) {
			switch (event.getNativeKeyCode()) {
			case KeyCodes.KEY_ENTER:
			case KeyCodes.KEY_TAB:
			case KeyCodes.KEY_SHIFT:
			case KeyCodes.KEY_CTRL:
			case KeyCodes.KEY_ALT:
			case KeyCodes.KEY_DOWN:
			case KeyCodes.KEY_UP:
			case KeyCodes.KEY_PAGEDOWN:
			case KeyCodes.KEY_PAGEUP:
			case KeyCodes.KEY_ESCAPE:
				// NOP
				break;
			default:
				if (this.textInputEnabled) {
					// when filtering, we always want to see the results on the
					// first page first.
					filterOptions(0);
				}
				break;
			}
		}
	}

	/**
	 * Resets the ComboBoxMultiselect to its initial state.
	 */
	private void reset() {
		debug("VComboBoxMultiselect: reset()");

		// just fetch selected information from state
		String text = this.connector.getState().selectedItemsCaption;
		setText(text == null ? "" : text);
		this.selectedOptionKeys = this.connector.getState().selectedItemKeys;
		if (this.selectedOptionKeys == null || this.selectedOptionKeys.isEmpty()) {
			this.selectedOptionKeys = null;
			updatePlaceholder();
		}
		this.currentSuggestion = null; // #13217
		// else {
		// this.currentSuggestion = this.currentSuggestions.stream()
		// .filter(suggestion ->
		// this.selectedOptionKeys.contains(suggestion.getOptionKey()))
		// .findAny()
		// .orElse(null);
		// }

		this.suggestionPopup.hide();
	}

	/**
	 * Listener for popupopener.
	 */
	@Override
	public void onClick(ClickEvent event) {
		debug("VComboBoxMultiselect: onClick()");
		if (this.enabled && !this.readonly) {
			getDataReceivedHandler().blurUpdate = false;
			// ask suggestionPopup if it was just closed, we are using GWT
			// Popup's auto close feature
			if (!this.suggestionPopup.isJustClosed()) {
				filterOptions(-1, "");
				this.dataReceivedHandler.popupOpenerClicked();
			}
			DOM.eventPreventDefault(DOM.eventGetCurrentEvent());
			focus();
			setText("");
		}
	}

	/**
	 * Update minimum width for combo box textarea based on input prompt and
	 * suggestions.
	 * <p>
	 * For internal use only. May be removed or replaced in the future.
	 */
	public void updateSuggestionPopupMinWidth() {
		debug("VComboBoxMultiselect: updateSuggestionPopupMinWidth()");

		// used only to calculate minimum width
		String captions = WidgetUtil.escapeHTML(this.inputPrompt);

		for (ComboBoxMultiselectSuggestion suggestion : this.currentSuggestions) {
			// Collect captions so we can calculate minimum width for
			// textarea
			if (captions.length() > 0) {
				captions += "|";
			}
			captions += WidgetUtil.escapeHTML(suggestion.getReplacementString());
		}

		// Calculate minimum textarea width
		this.suggestionPopupMinWidth = minWidth(captions);
	}

	/**
	 * Calculate minimum width for FilterSelect textarea.
	 * <p>
	 * For internal use only. May be removed or replaced in the future.
	 *
	 * @param captions
	 *            pipe separated string listing all the captions to measure
	 * @return minimum width in pixels
	 */
	public native int minWidth(String captions)
	/*-{
	    if(!captions || captions.length <= 0)
	            return 0;
	    captions = captions.split("|");
	    var d = $wnd.document.createElement("div");
	    var html = "";
	    for(var i=0; i < captions.length; i++) {
	            html += "<div>" + captions[i] + "</div>";
	            // TODO apply same CSS classname as in suggestionmenu
	    }
	    d.style.position = "absolute";
	    d.style.top = "0";
	    d.style.left = "0";
	    d.style.visibility = "hidden";
	    d.innerHTML = html;
	    $wnd.document.body.appendChild(d);
	    var w = d.offsetWidth;
	    $wnd.document.body.removeChild(d);
	    return w;
	}-*/;

	/**
	 * A flag which prevents a focus event from taking place.
	 */
	boolean iePreventNextFocus = false;

	/*
	 * (non-Javadoc)
	 *
	 * @see
	 * com.google.gwt.event.dom.client.FocusHandler#onFocus(com.google.gwt.event
	 * .dom.client.FocusEvent)
	 */

	@Override
	public void onFocus(FocusEvent event) {
		debug("VComboBoxMultiselect: onFocus()");

		/*
		 * When we disable a blur event in ie we need to refocus the textfield.
		 * This will cause a focus event we do not want to process, so in that
		 * case we just ignore it.
		 */
		if (BrowserInfo.get()
			.isIE() && this.iePreventNextFocus) {
			this.iePreventNextFocus = false;
			return;
		}

		this.focused = true;
		updatePlaceholder();
		addStyleDependentName("focus");

		this.connector.sendFocusEvent();

		this.connector.getConnection()
			.getVTooltip()
			.showAssistive(this.connector.getTooltipInfo(getElement()));
	}

	/**
	 * A flag which cancels the blur event and sets the focus back to the
	 * textfield if the Browser is IE.
	 */
	boolean preventNextBlurEventInIE = false;

	private String explicitSelectedCaption;

	/*
	 * (non-Javadoc)
	 *
	 * @see
	 * com.google.gwt.event.dom.client.BlurHandler#onBlur(com.google.gwt.event
	 * .dom.client.BlurEvent)
	 */

	@Override
	public void onBlur(BlurEvent event) {
		debug("VComboBoxMultiselect: onBlur()");

		if (BrowserInfo.get()
			.isIE() && this.preventNextBlurEventInIE) {
			/*
			 * Clicking in the suggestion popup or on the popup button in IE
			 * causes a blur event to be sent for the field. In other browsers
			 * this is prevented by canceling/preventing default behavior for
			 * the focus event, in IE we handle it here by refocusing the text
			 * field and ignoring the resulting focus event for the textfield
			 * (in onFocus).
			 */
			this.preventNextBlurEventInIE = false;

			Element focusedElement = WidgetUtil.getFocusedElement();
			if (getElement().isOrHasChild(focusedElement) || this.suggestionPopup.getElement()
				.isOrHasChild(focusedElement)) {

				// IF the suggestion popup or another part of the
				// VComboBoxMultiselect
				// was focused, move the focus back to the textfield and prevent
				// the triggered focus event (in onFocus).
				this.iePreventNextFocus = true;
				this.tb.setFocus(true);
				return;
			}
		}

		this.focused = false;
		updatePlaceholder();
		removeStyleDependentName("focus");

		// Send new items when clicking out with the mouse.
		if (!this.readonly) {
			if (this.textInputEnabled && this.allowNewItems && (this.currentSuggestion == null || this.tb.getText()
				.equals(this.currentSuggestion.getReplacementString()))) {
				this.dataReceivedHandler.reactOnInputWhenReady(this.tb.getText());
			} else {
				reset();
			}
			this.suggestionPopup.hide();
		}

		this.connector.sendBlurEvent();
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see com.vaadin.client.Focusable#focus()
	 */

	@Override
	public void focus() {
		debug("VComboBoxMultiselect: focus()");
		this.focused = true;
		updatePlaceholder();
		this.tb.setFocus(true);
	}

	/**
	 * Calculates the width of the select if the select has undefined width.
	 * Should be called when the width changes or when the icon changes.
	 * <p>
	 * For internal use only. May be removed or replaced in the future.
	 */
	public void updateRootWidth() {
		debug("VComboBoxMultiselect: updateRootWidth()");

		if (this.connector.isUndefinedWidth()) {

			/*
			 * When the select has a undefined with we need to check that we are
			 * only setting the text box width relative to the first page width
			 * of the items. If this is not done the text box width will change
			 * when the popup is used to view longer items than the text box is
			 * wide.
			 */
			int w = WidgetUtil.getRequiredWidth(this);

			if (this.dataReceivedHandler.isWaitingForInitialData() && this.suggestionPopupMinWidth > w) {
				/*
				 * We want to compensate for the paddings just to preserve the
				 * exact size as in Vaadin 6.x, but we get here before
				 * MeasuredSize has been initialized.
				 * Util.measureHorizontalPaddingAndBorder does not work with
				 * border-box, so we must do this the hard way.
				 */
				Style style = getElement().getStyle();
				String originalPadding = style.getPadding();
				String originalBorder = style.getBorderWidth();
				style.setPaddingLeft(0, Unit.PX);
				style.setBorderWidth(0, Unit.PX);
				style.setProperty("padding", originalPadding);
				style.setProperty("borderWidth", originalBorder);

				// Use util.getRequiredWidth instead of getOffsetWidth here

				int iconWidth = this.selectedItemIcon == null ? 0 : WidgetUtil.getRequiredWidth(this.selectedItemIcon);
				int buttonWidth = this.popupOpener == null ? 0 : WidgetUtil.getRequiredWidth(this.popupOpener);

				/*
				 * Instead of setting the width of the wrapper, set the width of
				 * the combobox. Subtract the width of the icon and the
				 * popupopener
				 */

				this.tb.setWidth(this.suggestionPopupMinWidth - iconWidth - buttonWidth + "px");
			}

			/*
			 * Lock the textbox width to its current value if it's not already
			 * locked. This can happen after setWidth("") which resets the
			 * textbox width to "100%".
			 */
			if (!this.tb.getElement()
				.getStyle()
				.getWidth()
				.endsWith("px")) {
				int iconWidth = this.selectedItemIcon == null ? 0 : this.selectedItemIcon.getOffsetWidth();
				this.tb.setWidth(this.tb.getOffsetWidth() - iconWidth + "px");
			}
		}
	}

	/**
	 * Get the width of the select in pixels where the text area and icon has
	 * been included.
	 *
	 * @return The width in pixels
	 */
	private int getMainWidth() {
		return getOffsetWidth();
	}

	@Override
	public void setWidth(String width) {
		super.setWidth(width);
		if (width.length() != 0) {
			this.tb.setWidth("100%");
		}
	}

	/**
	 * Handles special behavior of the mouse down event.
	 *
	 * @param event
	 */
	private void handleMouseDownEvent(Event event) {
		/*
		 * Prevent the keyboard focus from leaving the textfield by preventing
		 * the default behaviour of the browser. Fixes #4285.
		 */
		if (event.getTypeInt() == Event.ONMOUSEDOWN) {
			debug("VComboBoxMultiselect: blocking mouseDown event to avoid blur");

			event.preventDefault();
			event.stopPropagation();

			/*
			 * In IE the above wont work, the blur event will still trigger. So,
			 * we set a flag here to prevent the next blur event from happening.
			 * This is not needed if do not already have focus, in that case
			 * there will not be any blur event and we should not cancel the
			 * next blur.
			 */
			if (BrowserInfo.get()
				.isIE() && this.focused) {
				this.preventNextBlurEventInIE = true;
				debug("VComboBoxMultiselect: Going to prevent next blur event on IE");
			}
		}
	}

	@Override
	public void onMouseDown(MouseDownEvent event) {
		debug("VComboBoxMultiselect.onMouseDown(): blocking mouseDown event to avoid blur");

		event.preventDefault();
		event.stopPropagation();

		/*
		 * In IE the above wont work, the blur event will still trigger. So, we
		 * set a flag here to prevent the next blur event from happening. This
		 * is not needed if do not already have focus, in that case there will
		 * not be any blur event and we should not cancel the next blur.
		 */
		if (BrowserInfo.get()
			.isIE() && this.focused) {
			this.preventNextBlurEventInIE = true;
			debug("VComboBoxMultiselect: Going to prevent next blur event on IE");
		}
	}

	@Override
	protected void onDetach() {
		super.onDetach();
		this.suggestionPopup.hide();
	}

	@Override
	public com.google.gwt.user.client.Element getSubPartElement(String subPart) {
		String[] parts = subPart.split("/");
		if ("textbox".equals(parts[0])) {
			return this.tb.getElement();
		} else if ("button".equals(parts[0])) {
			return this.popupOpener.getElement();
		} else if ("popup".equals(parts[0]) && this.suggestionPopup.isAttached()) {
			if (parts.length == 2) {
				return this.suggestionPopup.menu.getSubPartElement(parts[1]);
			}
			return this.suggestionPopup.getElement();
		}
		return null;
	}

	@Override
	public String getSubPartName(com.google.gwt.user.client.Element subElement) {
		if (this.tb.getElement()
			.isOrHasChild(subElement)) {
			return "textbox";
		} else if (this.popupOpener.getElement()
			.isOrHasChild(subElement)) {
			return "button";
		} else if (this.suggestionPopup.getElement()
			.isOrHasChild(subElement)) {
			return "popup";
		}
		return null;
	}

	@Override
	public void setAriaRequired(boolean required) {
		AriaHelper.handleInputRequired(this.tb, required);
	}

	@Override
	public void setAriaInvalid(boolean invalid) {
		AriaHelper.handleInputInvalid(this.tb, invalid);
	}

	@Override
	public void bindAriaCaption(com.google.gwt.user.client.Element captionElement) {
		AriaHelper.bindCaption(this.tb, captionElement);
	}

	@Override
	public boolean isWorkPending() {
		return this.dataReceivedHandler.isWaitingForFilteringResponse()
				|| this.suggestionPopup.lazyPageScroller.isRunning();
	}

	/**
	 * Sets the caption of selected item, if "scroll to page" is disabled. This
	 * method is meant for internal use and may change in future versions.
	 *
	 * @since 7.7
	 * @param selectedCaption
	 *            the caption of selected item
	 */
	public void setSelectedCaption(String selectedCaption) {
		this.explicitSelectedCaption = selectedCaption;
		if (selectedCaption != null) {
			setText(selectedCaption);
		}
	}

	/**
	 * This method is meant for internal use and may change in future versions.
	 *
	 * @since 7.7
	 * @return the caption of selected item, if "scroll to page" is disabled
	 */
	public String getSelectedCaption() {
		return this.explicitSelectedCaption;
	}

	/**
	 * Returns a handler receiving notifications from the connector about
	 * communications.
	 *
	 * @return the dataReceivedHandler
	 */
	public DataReceivedHandler getDataReceivedHandler() {
		return this.dataReceivedHandler;
	}

	/**
	 * Sets the number of items to show per page, or 0 for showing all items.
	 *
	 * @param pageLength
	 *            new page length or 0 for all items
	 */
	public void setPageLength(int pageLength) {
		this.pageLength = pageLength;
	}

	/**
	 * Sets the caption of the clear button.
	 *
	 * @param clearButtonCaption
	 *            caption of the clear button
	 */
	public void setClearButtonCaption(String clearButtonCaption) {
		this.clearButtonCaption = clearButtonCaption;
	}

	/**
	 * Sets the caption of the selectAll button.
	 *
	 * @param selectAllButtonCaption
	 *            caption of the selectAll button
	 */
	public void setSelectAllButtonCaption(String selectAllButtonCaption) {
		this.selectAllButtonCaption = selectAllButtonCaption;
	}

	/**
	 * Sets the clear button visible.
	 * 
	 * @param showClearButton
	 *            visible
	 */
	public void setShowClearButton(boolean showClearButton) {
		this.showClearButton = showClearButton;
	}

	/**
	 * Sets the select all button visible.
	 * 
	 * @param showSelectAllButton
	 *            visible
	 */
	public void setShowSelectAllButton(boolean showSelectAllButton) {
		this.showSelectAllButton = showSelectAllButton;
	}

	/**
	 * Sets the suggestion pop-up's width as a CSS string. By using relative
	 * units (e.g. "50%") it's possible to set the popup's width relative to the
	 * ComboBoxMultiselect itself.
	 *
	 * @param suggestionPopupWidth
	 *            new popup width as CSS string, null for old default width
	 *            calculation based on items
	 */
	public void setSuggestionPopupWidth(String suggestionPopupWidth) {
		this.suggestionPopupWidth = suggestionPopupWidth;
	}

	/**
	 * Sets whether creation of new items when there is no match is allowed or
	 * not.
	 *
	 * @param allowNewItems
	 *            true to allow creation of new items, false to only allow
	 *            selection of existing items
	 */
	public void setAllowNewItems(boolean allowNewItems) {
		this.allowNewItems = allowNewItems;
	}

	/**
	 * Sets the total number of suggestions.
	 * <p>
	 * NOTE: this excluded the possible null selection item!
	 * <p>
	 * NOTE: this just updates the state, but doesn't update any UI.
	 *
	 * @since 8.0
	 * @param totalSuggestions
	 *            total number of suggestions
	 */
	public void setTotalSuggestions(int totalSuggestions) {
		this.totalSuggestions = totalSuggestions;
	}

	/**
	 * Gets the total number of suggestions, excluding the null selection item.
	 *
	 * @since 8.0
	 * @return total number of suggestions
	 */
	public int getTotalSuggestions() {
		return this.totalSuggestions;
	}

}