diff options
7 files changed, 723 insertions, 802 deletions
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/ui/VFilterSelect.java b/compatibility-client/src/main/java/com/vaadin/v7/client/ui/VFilterSelect.java index 38b1bf785d..0b7724f3d0 100644 --- a/compatibility-client/src/main/java/com/vaadin/v7/client/ui/VFilterSelect.java +++ b/compatibility-client/src/main/java/com/vaadin/v7/client/ui/VFilterSelect.java @@ -31,6 +31,7 @@ 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.Node; import com.google.gwt.dom.client.Style; import com.google.gwt.dom.client.Style.Display; import com.google.gwt.dom.client.Style.Unit; @@ -66,9 +67,12 @@ 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.ComponentConnector; import com.vaadin.client.ComputedStyle; +import com.vaadin.client.ConnectorMap; import com.vaadin.client.DeferredWorker; import com.vaadin.client.Focusable; +import com.vaadin.client.UIDL; import com.vaadin.client.VConsole; import com.vaadin.client.WidgetUtil; import com.vaadin.client.ui.Field; @@ -83,9 +87,9 @@ 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.EventId; import com.vaadin.shared.ui.ComponentStateUtil; import com.vaadin.shared.util.SharedUtil; -import com.vaadin.v7.client.ui.combobox.ComboBoxConnector; import com.vaadin.v7.shared.ui.combobox.FilteringMode; /** @@ -112,22 +116,17 @@ public class VFilterSelect extends Composite /** * Constructor * - * @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 + * @param uidl + * The UIDL recieved from the server */ - public FilterSelectSuggestion(String key, String caption, String style, - String untranslatedIconUri) { - this.key = key; - this.caption = caption; - this.style = style; - this.untranslatedIconUri = untranslatedIconUri; + public FilterSelectSuggestion(UIDL uidl) { + key = uidl.getStringAttribute("key"); + caption = uidl.getStringAttribute("caption"); + style = uidl.getStringAttribute("style"); + + if (uidl.hasAttribute("icon")) { + untranslatedIconUri = uidl.getStringAttribute("icon"); + } } /** @@ -139,7 +138,6 @@ public class VFilterSelect extends Composite @Override public String getDisplayString() { final StringBuffer sb = new StringBuffer(); - ApplicationConnection client = connector.getConnection(); final Icon icon = client .getIcon(client.translateVaadinUri(untranslatedIconUri)); if (icon != null) { @@ -181,15 +179,13 @@ public class VFilterSelect extends Composite * @return */ public String getIconUri() { - ApplicationConnection client = connector.getConnection(); return client.translateVaadinUri(untranslatedIconUri); } /** * Gets the style set for this suggestion item. Styles are typically set - * by a server-side - * {@link com.vaadin.v7.ui.ComboBox.ItemStyleGenerator}. The returned - * style is prefixed by <code>v-filterselect-item-</code>. + * by a server-side {@link com.vaadin.ui.ComboBox.ItemStyleGenerator}. + * 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 @@ -247,12 +243,12 @@ public class VFilterSelect extends Composite 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; } - + @com.vaadin.v7.client.ui.VFilterSelect.JsniUtil::moveScrollFromEvent(*)(widget, deltaX, deltaY, e, e.deltaMode); }); }-*/; @@ -306,8 +302,7 @@ public class VFilterSelect extends Composite // "Scroll" if change exceeds item height while (Math.abs(deltaSum) >= SCROLL_UNIT_PX) { - if (!filterSelect.dataReceivedHandler - .isWaitingForFilteringResponse()) { + if (!filterSelect.waitingForFilteringResponse) { // Move selection if page flip is not in progress if (deltaSum < 0) { filterSelect.suggestionPopup.selectPrevItem(); @@ -474,6 +469,15 @@ public class VFilterSelect extends Composite .clearWidth(); setPopupPositionAndShow(popup); + // Fix for #14173 + // IE9 and IE10 have a bug, when resize an a element with + // box-shadow. + // IE9 and IE10 need explicit update to remove extra + // box-shadows + if (BrowserInfo.get().isIE9() + || BrowserInfo.get().isIE10()) { + forceReflow(); + } } }); } @@ -604,7 +608,7 @@ public class VFilterSelect extends Composite public void run() { debug("VFS.SP.LPS: run()"); if (pagesToScroll != 0) { - if (!dataReceivedHandler.isWaitingForFilteringResponse()) { + if (!waitingForFilteringResponse) { /* * Avoid scrolling while we are waiting for a response * because otherwise the waiting flag will be reset in @@ -853,6 +857,10 @@ public class VFilterSelect extends Composite menu.setWidth(Window.getClientWidth() + "px"); } + if (BrowserInfo.get().isIE() + && BrowserInfo.get().getBrowserMajorVersion() < 10) { + setTdWidth(menu.getElement(), Window.getClientWidth() - 8); + } } setPopupPosition(left, top); @@ -896,6 +904,44 @@ public class VFilterSelect extends Composite width = WidgetUtil.escapeAttribute(suggestionPopupWidth); } menu.setWidth(width); + + // IE8 or 9? + if (BrowserInfo.get().isIE() + && BrowserInfo.get().getBrowserMajorVersion() < 10) { + // using legacy mode? + if (suggestionPopupWidth == null) { + // set the TD widths manually as these browsers do not + // respect display: block; width:100% rules + setTdWidth(menu.getElement(), naturalMenuWidth); + } else { + int compensation = WidgetUtil + .measureHorizontalPaddingAndBorder( + menu.getElement(), 4); + setTdWidth(menu.getElement(), + menu.getOffsetWidth() - compensation); + } + + } + } + + /** + * Descends to child elements until finds TD elements and sets their + * width in pixels. Can be used to workaround IE8 & 9 TD element + * display: block issues + * + * @param parent + * @param width + */ + private void setTdWidth(Node parent, int width) { + for (int i = 0; i < parent.getChildCount(); i++) { + Node child = parent.getChild(i); + if ("td".equals(child.getNodeName().toLowerCase())) { + ((Element) child).getStyle().setWidth(width, Unit.PX); + } else { + setTdWidth(child, width); + } + + } } /** @@ -952,10 +998,13 @@ public class VFilterSelect extends Composite /** * Updates style names in suggestion popup to help theme building. * + * @param uidl + * UIDL for the whole combo box * @param componentState * shared state of the combo box */ - public void updateStyleNames(AbstractComponentState componentState) { + public void updateStyleNames(UIDL uidl, + AbstractComponentState componentState) { debug("VFS.SP: updateStyleNames()"); setStyleName( VFilterSelect.this.getStylePrimaryName() + "-suggestpopup"); @@ -1073,11 +1122,18 @@ public class VFilterSelect extends Composite isFirstIteration = false; } + if (suggestionPopupWidth != null && BrowserInfo.get().isIE() + && BrowserInfo.get().getBrowserMajorVersion() < 10) { + // set TD width to a low value so that they won't mandate the + // suggestion pop-up width + suggestionPopup.setTdWidth(suggestionPopup.menu.getElement(), + 1); + } } /** * Send the current selection to the server. Triggered when a selection - * is made with the ENTER key. + * is made or on a blur event. */ public void doSelectedItemAction() { debug("VFS.SM: doSelectedItemAction()"); @@ -1092,23 +1148,32 @@ public class VFilterSelect extends Composite } // null is not visible on pages != 0, and not visible when // filtering: handle separately - connector.requestFirstPage(); + client.updateVariable(paintableId, "filter", "", false); + client.updateVariable(paintableId, "page", 0, false); + client.updateVariable(paintableId, "selected", new String[] {}, + immediate); + afterUpdateClientVariables(); suggestionPopup.hide(); return; } - dataReceivedHandler.doPostFilterWhenReady(); + updateSelectionWhenReponseIsReceived = waitingForFilteringResponse; + if (!waitingForFilteringResponse) { + doPostFilterSelectedItemAction(); + } } /** - * Triggered after a selection has been made. + * Triggered after a selection has been made */ public void doPostFilterSelectedItemAction() { debug("VFS.SM: doPostFilterSelectedItemAction()"); final MenuItem item = getSelectedItem(); final String enteredItemValue = tb.getText(); + updateSelectionWhenReponseIsReceived = false; + // check for exact match in menu int p = getItems().size(); if (p > 0) { @@ -1136,7 +1201,9 @@ public class VFilterSelect extends Composite * Store last sent new item string to avoid double sends */ lastNewItemString = enteredItemValue; - connector.sendNewItem(enteredItemValue); + client.updateVariable(paintableId, "newitem", + enteredItemValue, immediate); + afterUpdateClientVariables(); } } else if (item != null && !"".equals(lastFilter) && (filteringmode == FilteringMode.CONTAINS @@ -1330,197 +1397,6 @@ public class VFilterSelect extends Composite } - /** - * 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; - private boolean performPostFilteringOnDataReceived = false; - /** For internal use only. May be removed or replaced in the future. */ - private boolean waitingForFilteringResponse = false; - - /** - * Called by the connector when new data for the last requested filter - * is received from the server. - */ - public void dataReceived() { - suggestionPopup.showSuggestions(currentSuggestions, currentPage, - totalMatches); - - waitingForFilteringResponse = false; - - if (!popupOpenerClicked) { - navigateItemAfterPageChange(); - } - - if (performPostFilteringOnDataReceived) { - performPostFilteringOnDataReceived = false; - suggestionPopup.menu.doPostFilterSelectedItemAction(); - } - - popupOpenerClicked = false; - } - - /* - * This method navigates to the proper item in the combobox page. This - * should be executed after setSuggestions() method which is called from - * vFilterSelect.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 (navigationCallback != null) { - // pageChangeCallback 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 - Scheduler.get().scheduleDeferred(new ScheduledCommand() { - @Override - public void execute() { - if (navigationCallback != null) { - navigationCallback.run(); - } - } - }); - } - } - - /** - * Called by the connector when any request has been sent to the server, - * before waiting for a reply. - */ - public void anyRequestSentToServer() { - 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 - */ - public void setNavigationCallback(Runnable callback) { - 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 UIDL reply. - */ - public void popupOpenerClicked() { - popupOpenerClicked = true; - } - - /** - * Cancel a pending request to perform post-filtering actions. - */ - private void cancelPendingPostFiltering() { - performPostFilteringOnDataReceived = false; - } - - /** - * Called by the connector when it has finished handling any reply from - * the server, regardless of what was updated. - */ - public void serverReplyHandled() { - popupOpenerClicked = false; - } - - /** - * 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 waitingForFilteringResponse; - } - - /** - * Set a flag that filtering of options is pending a response from the - * server. - */ - private void startWaitingForFilteringResponse() { - waitingForFilteringResponse = true; - } - - /** - * Perform the post-filter action either now (if not waiting for a - * server response) or when a response is received. - */ - private void doPostFilterWhenReady() { - if (isWaitingForFilteringResponse()) { - performPostFilteringOnDataReceived = true; - } else { - performPostFilteringOnDataReceived = false; - suggestionPopup.menu.doPostFilterSelectedItemAction(); - } - } - - /** - * 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 selectedKey - * new selected key 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 - * @param oldSuggestionTextMatchTheOldSelection - * true if the old filtering text box contents matched - * exactly the old selection - */ - public void updateSelectionFromServer(String selectedKey, - String selectedCaption, - boolean oldSuggestionTextMatchTheOldSelection) { - // when filtering with empty filter, server sets the selected key - // to "", which we don't select here. Otherwise we won't be able to - // reset back to the item that was selected before filtering - // started. - if (selectedKey != null && !selectedKey.equals("")) { - performSelection(selectedKey, - oldSuggestionTextMatchTheOldSelection, - !isWaitingForFilteringResponse() || popupOpenerClicked); - setSelectedCaption(null); - } else if (!isWaitingForFilteringResponse() - && selectedCaption != null) { - // scrolling to correct page is disabled, caption is passed as a - // special parameter - setSelectedCaption(selectedCaption); - } else { - if (!isWaitingForFilteringResponse() || popupOpenerClicked) { - resetSelection(popupOpenerClicked); - } - } - } - - } - @Deprecated public static final FilteringMode FILTERINGMODE_OFF = FilteringMode.OFF; @Deprecated @@ -1583,7 +1459,10 @@ public class VFilterSelect extends Composite private IconWidget selectedItemIcon; /** For internal use only. May be removed or replaced in the future. */ - public ComboBoxConnector connector; + public ApplicationConnection client; + + /** For internal use only. May be removed or replaced in the future. */ + public String paintableId; /** For internal use only. May be removed or replaced in the future. */ public int currentPage; @@ -1594,17 +1473,34 @@ public class VFilterSelect extends Composite * <p> * For internal use only. May be removed or replaced in the future. */ - public final List<FilterSelectSuggestion> currentSuggestions = new ArrayList<FilterSelectSuggestion>(); + public final List<FilterSelectSuggestion> currentSuggestions = new ArrayList<>(); + + /** For internal use only. May be removed or replaced in the future. */ + public boolean immediate; /** For internal use only. May be removed or replaced in the future. */ public String selectedOptionKey; /** For internal use only. May be removed or replaced in the future. */ + public boolean waitingForFilteringResponse = false; + + /** For internal use only. May be removed or replaced in the future. */ + public boolean updateSelectionWhenReponseIsReceived = false; + + /** 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 = ""; + /** For internal use only. May be removed or replaced in the future. */ + public enum Select { + NONE, FIRST, LAST + } + + /** For internal use only. May be removed or replaced in the future. */ + public Select selectPopupItemWhenResponseIsReceived = Select.NONE; + /** * The current suggestion selected from the dropdown. This is one of the * values in currentSuggestions except when filtering, in this case @@ -1644,6 +1540,15 @@ public class VFilterSelect extends Composite /** For internal use only. May be removed or replaced in the future. */ public boolean prompting = false; + /** + * 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. + */ + public boolean popupOpenerClicked; + /** For internal use only. May be removed or replaced in the future. */ public int suggestionPopupMinWidth = 0; @@ -1667,8 +1572,6 @@ public class VFilterSelect extends Composite */ private boolean textInputEnabled = true; - private final DataReceivedHandler dataReceivedHandler = new DataReceivedHandler(); - /** * Default constructor. */ @@ -1808,7 +1711,22 @@ public class VFilterSelect extends Composite * The filter to apply to the components */ public void filterOptions(int page, String filter) { - debug("VFS: filterOptions(" + page + ", " + filter + ")"); + filterOptions(page, filter, true); + } + + /** + * Filters the options at certain page using the given filter + * + * @param page + * The page to filter + * @param filter + * The filter to apply to the options + * @param immediate + * Whether to send the options request immediately + */ + private void filterOptions(int page, String filter, boolean immediate) { + debug("VFS: filterOptions(" + page + ", " + filter + ", " + immediate + + ")"); if (filter.equals(lastFilter) && currentPage == page) { if (!suggestionPopup.isAttached()) { @@ -1829,8 +1747,10 @@ public class VFilterSelect extends Composite } } - dataReceivedHandler.startWaitingForFilteringResponse(); - connector.requestPage(filter, page); + waitingForFilteringResponse = true; + client.updateVariable(paintableId, "filter", filter, false); + client.updateVariable(paintableId, "page", page, immediate); + afterUpdateClientVariables(); lastFilter = filter; currentPage = page; @@ -1928,7 +1848,7 @@ public class VFilterSelect extends Composite debug("VFS: onSuggestionSelected(" + suggestion.caption + ": " + suggestion.key + ")"); } - dataReceivedHandler.cancelPendingPostFiltering(); + updateSelectionWhenReponseIsReceived = false; currentSuggestion = suggestion; String newKey; @@ -1951,7 +1871,9 @@ public class VFilterSelect extends Composite if (!(newKey.equals(selectedOptionKey) || ("".equals(newKey) && selectedOptionKey == null))) { selectedOptionKey = newKey; - connector.sendSelection(selectedOptionKey); + client.updateVariable(paintableId, "selected", + new String[] { selectedOptionKey }, immediate); + afterUpdateClientVariables(); // currentPage = -1; // forget the page } @@ -1962,7 +1884,9 @@ public class VFilterSelect extends Composite // hard to interpret that new clause added here :-( selectedOptionKey = newKey; explicitSelectedCaption = null; - connector.sendSelection(selectedOptionKey); + client.updateVariable(paintableId, "selected", + new String[] { selectedOptionKey }, immediate); + afterUpdateClientVariables(); } suggestionPopup.hide(); @@ -1987,8 +1911,7 @@ public class VFilterSelect extends Composite if (selectedItemIcon != null) { panel.remove(selectedItemIcon); } - selectedItemIcon = new IconWidget( - connector.getConnection().getIcon(iconUri)); + selectedItemIcon = new IconWidget(client.getIcon(iconUri)); // Older IE versions don't scale icon correctly if DOM // contains height and width attributes. selectedItemIcon.getElement().removeAttribute("height"); @@ -2005,7 +1928,7 @@ public class VFilterSelect extends Composite } private void afterSelectedItemIconChange() { - if (BrowserInfo.get().isWebkit()) { + if (BrowserInfo.get().isWebkit() || BrowserInfo.get().isIE8()) { // Some browsers need a nudge to reposition the text field forceReflow(); } @@ -2015,88 +1938,6 @@ public class VFilterSelect extends Composite } } - /** - * Perform selection based on a message from the server. - * - * This method is called when the server gave a non-empty selected item key, - * whereas null selection is handled by {@link #resetSelection()} and the - * special case where the selected item is not on the current page is - * handled separately by the caller. - * - * @param selectedKey - * non-empty selected item key - * @param oldSuggestionTextMatchTheOldSelection - * true if the suggestion box text matched the previous selection - * before the message from the server updating the selection - * @param updatePromptAndSelectionIfMatchFound - */ - private void performSelection(String selectedKey, - boolean oldSuggestionTextMatchTheOldSelection, - boolean updatePromptAndSelectionIfMatchFound) { - // some item selected - for (FilterSelectSuggestion suggestion : currentSuggestions) { - String suggestionKey = suggestion.getOptionKey(); - if (!suggestionKey.equals(selectedKey)) { - continue; - } - // at this point, suggestion key matches the new selection key - if (updatePromptAndSelectionIfMatchFound) { - if (!suggestionKey.equals(selectedOptionKey) - || suggestion.getReplacementString() - .equals(tb.getText()) - || oldSuggestionTextMatchTheOldSelection) { - // Update text field if we've got a new - // selection - // Also update if we've got the same text to - // retain old text selection behavior - // OR if selected item caption is changed. - setPromptingOff(suggestion.getReplacementString()); - selectedOptionKey = suggestionKey; - } - } - currentSuggestion = suggestion; - setSelectedItemIcon(suggestion.getIconUri()); - // only a single item can be selected - break; - } - } - - /** - * Reset the selection of the combo box to an empty string if focused, the - * input prompt if not. - * - * This method also clears the current suggestion and the selected option - * key. - */ - private void resetSelection(boolean useSelectedCaption) { - // select nulled - if (!focused) { - // TODO it is unclear whether this is really needed anymore - - // client.updateComponent used to overwrite all styles so we had to - // set them again - setPromptingOff(""); - if (enabled && !readonly) { - setPromptingOn(); - } - } else { - // we have focus in field, prompting can't be set on, - // instead just clear the input if the value has changed from - // something else to null - if (selectedOptionKey != null - || (allowNewItem && !tb.getValue().isEmpty())) { - if (useSelectedCaption && getSelectedCaption() != null) { - tb.setValue(getSelectedCaption()); - tb.selectAll(); - } else { - tb.setValue(""); - } - } - } - currentSuggestion = null; // #13217 - setSelectedItemIcon(null); - selectedOptionKey = null; - } - private void forceReflow() { WidgetUtil.setStyleTemporarily(tb.getElement(), "zoom", "1"); } @@ -2116,7 +1957,7 @@ public class VFilterSelect extends Composite Unit.PX); } - private static Set<Integer> navigationKeyCodes = new HashSet<Integer>(); + private static Set<Integer> navigationKeyCodes = new HashSet<>(); static { navigationKeyCodes.add(KeyCodes.KEY_DOWN); navigationKeyCodes.add(KeyCodes.KEY_UP); @@ -2141,7 +1982,7 @@ public class VFilterSelect extends Composite if (enableDebug) { debug("VFS: key down: " + keyCode); } - if (dataReceivedHandler.isWaitingForFilteringResponse() + if (waitingForFilteringResponse && navigationKeyCodes.contains(keyCode)) { /* * Keyboard navigation events should not be handled while we are @@ -2290,12 +2131,7 @@ public class VFilterSelect extends Composite private void selectPrevPage() { if (currentPage > 0) { filterOptions(currentPage - 1, lastFilter); - dataReceivedHandler.setNavigationCallback(new Runnable() { - @Override - public void run() { - suggestionPopup.selectLastItem(); - } - }); + selectPopupItemWhenResponseIsReceived = Select.LAST; } } @@ -2305,12 +2141,7 @@ public class VFilterSelect extends Composite private void selectNextPage() { if (hasNextPage()) { filterOptions(currentPage + 1, lastFilter); - dataReceivedHandler.setNavigationCallback(new Runnable() { - @Override - public void run() { - suggestionPopup.selectFirstItem(); - } - }); + selectPopupItemWhenResponseIsReceived = Select.FIRST; } } @@ -2394,8 +2225,13 @@ public class VFilterSelect extends Composite // ask suggestionPopup if it was just closed, we are using GWT // Popup's auto close feature if (!suggestionPopup.isJustClosed()) { - filterOptions(-1, ""); - dataReceivedHandler.popupOpenerClicked(); + // If a focus event is not going to be sent, send the options + // request immediately; otherwise queue in the same burst as the + // focus event. Fixes #8321. + boolean immediate = focused + || !client.hasEventListeners(this, EventId.FOCUS); + filterOptions(-1, "", immediate); + popupOpenerClicked = true; lastFilter = ""; } DOM.eventPreventDefault(DOM.eventGetCurrentEvent()); @@ -2488,9 +2324,14 @@ public class VFilterSelect extends Composite } addStyleDependentName("focus"); - connector.sendFocusEvent(); + if (client.hasEventListeners(this, EventId.FOCUS)) { + client.updateVariable(paintableId, EventId.FOCUS, "", true); + afterUpdateClientVariables(); + } - connector.getConnection().getVTooltip() + ComponentConnector connector = ConnectorMap.get(client) + .getConnector(this); + client.getVTooltip() .showAssistive(connector.getTooltipInfo(getElement())); } @@ -2552,7 +2393,10 @@ public class VFilterSelect extends Composite } removeStyleDependentName("focus"); - connector.sendBlurEvent(); + if (client.hasEventListeners(this, EventId.BLUR)) { + client.updateVariable(paintableId, EventId.BLUR, "", true); + afterUpdateClientVariables(); + } } /* @@ -2578,7 +2422,10 @@ public class VFilterSelect extends Composite * For internal use only. May be removed or replaced in the future. */ public void updateRootWidth() { - if (connector.isUndefinedWidth()) { + ComponentConnector paintable = ConnectorMap.get(client) + .getConnector(this); + + if (paintable.isUndefinedWidth()) { /* * When the select has a undefined with we need to check that we are @@ -2734,9 +2581,19 @@ public class VFilterSelect extends Composite AriaHelper.bindCaption(tb, captionElement); } + /* + * Anything that should be set after the client updates the server. + */ + private void afterUpdateClientVariables() { + // We need this here to be consistent with the all the calls. + // Then set your specific selection type only after + // client.updateVariable() method call. + selectPopupItemWhenResponseIsReceived = Select.NONE; + } + @Override public boolean isWorkPending() { - return dataReceivedHandler.isWaitingForFilteringResponse() + return waitingForFilteringResponse || suggestionPopup.lazyPageScroller.isRunning(); } @@ -2765,14 +2622,4 @@ public class VFilterSelect extends Composite return explicitSelectedCaption; } - /** - * Returns a handler receiving notifications from the connector about - * communications. - * - * @return the dataReceivedHandler - */ - public DataReceivedHandler getDataReceivedHandler() { - return dataReceivedHandler; - } - } diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/ui/combobox/ComboBoxConnector.java b/compatibility-client/src/main/java/com/vaadin/v7/client/ui/combobox/ComboBoxConnector.java index 6f851ec792..212a1aa8d1 100644 --- a/compatibility-client/src/main/java/com/vaadin/v7/client/ui/combobox/ComboBoxConnector.java +++ b/compatibility-client/src/main/java/com/vaadin/v7/client/ui/combobox/ComboBoxConnector.java @@ -19,65 +19,28 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import com.google.gwt.core.client.Scheduler; +import com.google.gwt.core.client.Scheduler.ScheduledCommand; import com.vaadin.client.ApplicationConnection; import com.vaadin.client.Paintable; -import com.vaadin.client.Profiler; import com.vaadin.client.UIDL; -import com.vaadin.client.communication.RpcProxy; -import com.vaadin.client.communication.StateChangeEvent; import com.vaadin.client.ui.SimpleManagedLayout; -import com.vaadin.shared.EventId; -import com.vaadin.shared.communication.FieldRpc.FocusAndBlurServerRpc; import com.vaadin.shared.ui.Connect; import com.vaadin.v7.client.ui.AbstractFieldConnector; import com.vaadin.v7.client.ui.VFilterSelect; -import com.vaadin.v7.client.ui.VFilterSelect.DataReceivedHandler; import com.vaadin.v7.client.ui.VFilterSelect.FilterSelectSuggestion; -import com.vaadin.v7.shared.ui.combobox.ComboBoxServerRpc; +import com.vaadin.v7.shared.ui.combobox.ComboBoxConstants; import com.vaadin.v7.shared.ui.combobox.ComboBoxState; +import com.vaadin.v7.shared.ui.combobox.FilteringMode; import com.vaadin.v7.ui.ComboBox; @Connect(ComboBox.class) public class ComboBoxConnector extends AbstractFieldConnector implements Paintable, SimpleManagedLayout { - protected ComboBoxServerRpc rpc = RpcProxy.create(ComboBoxServerRpc.class, - this); - - protected FocusAndBlurServerRpc focusAndBlurRpc = RpcProxy - .create(FocusAndBlurServerRpc.class, this); - - @Override - protected void init() { - super.init(); - getWidget().connector = this; - } - - @Override - public void onStateChanged(StateChangeEvent stateChangeEvent) { - super.onStateChanged(stateChangeEvent); - - Profiler.enter("ComboBoxConnector.onStateChanged update content"); - - getWidget().readonly = isReadOnly(); - getWidget().updateReadOnly(); - - getWidget().setTextInputEnabled(getState().textInputAllowed); - - if (getState().inputPrompt != null) { - getWidget().inputPrompt = getState().inputPrompt; - } else { - getWidget().inputPrompt = ""; - } - - getWidget().pageLength = getState().pageLength; - - getWidget().filteringmode = getState().filteringMode; - - getWidget().suggestionPopupWidth = getState().suggestionPopupWidth; - - Profiler.leave("ComboBoxConnector.onStateChanged update content"); - } + // oldSuggestionTextMatchTheOldSelection is used to detect when it's safe to + // update textbox text by a changed item caption. + private boolean oldSuggestionTextMatchTheOldSelection; /* * (non-Javadoc) @@ -87,13 +50,35 @@ public class ComboBoxConnector extends AbstractFieldConnector */ @Override public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + // Save details + getWidget().client = client; + getWidget().paintableId = uidl.getId(); + + getWidget().readonly = isReadOnly(); + getWidget().updateReadOnly(); + if (!isRealUpdate(uidl)) { return; } + // Inverse logic here to make the default case (text input enabled) + // work without additional UIDL messages + boolean noTextInput = uidl + .hasAttribute(ComboBoxConstants.ATTR_NO_TEXT_INPUT) + && uidl.getBooleanAttribute( + ComboBoxConstants.ATTR_NO_TEXT_INPUT); + getWidget().setTextInputEnabled(!noTextInput); + // not a FocusWidget -> needs own tabindex handling getWidget().tb.setTabIndex(getState().tabIndex); + if (uidl.hasAttribute("filteringmode")) { + getWidget().filteringmode = FilteringMode + .valueOf(uidl.getStringAttribute("filteringmode")); + } + + getWidget().immediate = getState().immediate; + getWidget().nullSelectionAllowed = uidl.hasAttribute("nullselect"); getWidget().nullSelectItem = uidl.hasAttribute("nullselectitem") @@ -101,7 +86,33 @@ public class ComboBoxConnector extends AbstractFieldConnector getWidget().currentPage = uidl.getIntVariable("page"); - getWidget().suggestionPopup.updateStyleNames(getState()); + if (uidl.hasAttribute("pagelength")) { + getWidget().pageLength = uidl.getIntAttribute("pagelength"); + } + + if (uidl.hasAttribute(ComboBoxConstants.ATTR_INPUTPROMPT)) { + // input prompt changed from server + getWidget().inputPrompt = uidl + .getStringAttribute(ComboBoxConstants.ATTR_INPUTPROMPT); + } else { + getWidget().inputPrompt = ""; + } + + if (uidl.hasAttribute("suggestionPopupWidth")) { + getWidget().suggestionPopupWidth = uidl + .getStringAttribute("suggestionPopupWidth"); + } else { + getWidget().suggestionPopupWidth = null; + } + + if (uidl.hasAttribute("suggestionPopupWidth")) { + getWidget().suggestionPopupWidth = uidl + .getStringAttribute("suggestionPopupWidth"); + } else { + getWidget().suggestionPopupWidth = null; + } + + getWidget().suggestionPopup.updateStyleNames(uidl, getState()); getWidget().allowNewItem = uidl.hasAttribute("allownewitem"); getWidget().lastNewItemString = null; @@ -113,21 +124,12 @@ public class ComboBoxConnector extends AbstractFieldConnector getWidget().totalMatches = 0; } - List<FilterSelectSuggestion> newSuggestions = new ArrayList<FilterSelectSuggestion>(); + List<FilterSelectSuggestion> newSuggestions = new ArrayList<>(); for (final Iterator<?> i = options.getChildIterator(); i.hasNext();) { final UIDL optionUidl = (UIDL) i.next(); - String key = optionUidl.getStringAttribute("key"); - String caption = optionUidl.getStringAttribute("caption"); - String style = optionUidl.getStringAttribute("style"); - - String untranslatedIconUri = null; - if (optionUidl.hasAttribute("icon")) { - untranslatedIconUri = optionUidl.getStringAttribute("icon"); - } - final FilterSelectSuggestion suggestion = getWidget().new FilterSelectSuggestion( - key, caption, style, untranslatedIconUri); + optionUidl); newSuggestions.add(suggestion); } @@ -139,15 +141,13 @@ public class ComboBoxConnector extends AbstractFieldConnector // popup. Popup needs to be repopulated with suggestions from UIDL. boolean popupOpenAndCleared = false; - // oldSuggestionTextMatchTheOldSelection is used to detect when it's - // safe to update textbox text by a changed item caption. - boolean oldSuggestionTextMatchesTheOldSelection = false; + oldSuggestionTextMatchTheOldSelection = false; if (suggestionsChanged) { - oldSuggestionTextMatchesTheOldSelection = isWidgetsCurrentSelectionTextInTextBox(); + oldSuggestionTextMatchTheOldSelection = isWidgetsCurrentSelectionTextInTextBox(); getWidget().currentSuggestions.clear(); - if (!getDataReceivedHandler().isWaitingForFilteringResponse()) { + if (!getWidget().waitingForFilteringResponse) { /* * Clear the current suggestions as the server response always * includes the new ones. Exception is when filtering, then we @@ -180,34 +180,62 @@ public class ComboBoxConnector extends AbstractFieldConnector // ) { - // single selected key (can be empty string) or empty array for null - // selection String[] selectedKeys = uidl.getStringArrayVariable("selected"); - String selectedKey = null; - if (selectedKeys.length == 1) { - selectedKey = selectedKeys[0]; - } - // selected item caption in case it is not on the current page - String selectedCaption = null; - if (uidl.hasAttribute("selectedCaption")) { - selectedCaption = uidl.getStringAttribute("selectedCaption"); - } - getDataReceivedHandler().updateSelectionFromServer(selectedKey, - selectedCaption, oldSuggestionTextMatchesTheOldSelection); + // when filtering with empty filter, server sets the selected key + // to "", which we don't select here. Otherwise we won't be able to + // reset back to the item that was selected before filtering + // started. + if (selectedKeys.length > 0 && !selectedKeys[0].equals("")) { + performSelection(selectedKeys[0]); + // if selected key is available, assume caption is know based on + // it as well and clear selected caption + getWidget().setSelectedCaption(null); + + } else if (!getWidget().waitingForFilteringResponse + && uidl.hasAttribute("selectedCaption")) { + // scrolling to correct page is disabled, caption is passed as a + // special parameter + getWidget().setSelectedCaption( + uidl.getStringAttribute("selectedCaption")); + } else { + resetSelection(); + } } - // TODO even this condition should probably be moved to the handler - if ((getDataReceivedHandler().isWaitingForFilteringResponse() - && getWidget().lastFilter.toLowerCase() - .equals(uidl.getStringVariable("filter"))) + if ((getWidget().waitingForFilteringResponse && getWidget().lastFilter + .toLowerCase().equals(uidl.getStringVariable("filter"))) || popupOpenAndCleared) { - getDataReceivedHandler().dataReceived(); + + getWidget().suggestionPopup.showSuggestions( + getWidget().currentSuggestions, getWidget().currentPage, + getWidget().totalMatches); + + getWidget().waitingForFilteringResponse = false; + + if (!getWidget().popupOpenerClicked + && getWidget().selectPopupItemWhenResponseIsReceived != VFilterSelect.Select.NONE) { + + // we're paging w/ arrows + Scheduler.get().scheduleDeferred(new ScheduledCommand() { + @Override + public void execute() { + navigateItemAfterPageChange(); + } + }); + } + + if (getWidget().updateSelectionWhenReponseIsReceived) { + getWidget().suggestionPopup.menu + .doPostFilterSelectedItemAction(); + } } // Calculate minimum textarea width getWidget().updateSuggestionPopupMinWidth(); + getWidget().popupOpenerClicked = false; + /* * if this is our first time we need to recalculate the root width. */ @@ -223,9 +251,58 @@ public class ComboBoxConnector extends AbstractFieldConnector } getWidget().initDone = true; + } + + /* + * This method navigates to the proper item in the combobox page. This + * should be executed after setSuggestions() method which is called from + * vFilterSelect.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 (getWidget().selectPopupItemWhenResponseIsReceived == VFilterSelect.Select.LAST) { + getWidget().suggestionPopup.selectLastItem(); + } else { + getWidget().suggestionPopup.selectFirstItem(); + } + + // If you're in between 2 requests both changing the page back and + // forth, you don't want this here, instead you need it before any + // other request. + // getWidget().selectPopupItemWhenResponseIsReceived = + // VFilterSelect.Select.NONE; // reset + } - // TODO this should perhaps be moved to be a part of dataReceived() - getDataReceivedHandler().serverReplyHandled(); + private void performSelection(String selectedKey) { + // some item selected + for (FilterSelectSuggestion suggestion : getWidget().currentSuggestions) { + String suggestionKey = suggestion.getOptionKey(); + if (!suggestionKey.equals(selectedKey)) { + continue; + } + if (!getWidget().waitingForFilteringResponse + || getWidget().popupOpenerClicked) { + if (!suggestionKey.equals(getWidget().selectedOptionKey) + || suggestion.getReplacementString() + .equals(getWidget().tb.getText()) + || oldSuggestionTextMatchTheOldSelection) { + // Update text field if we've got a new + // selection + // Also update if we've got the same text to + // retain old text selection behavior + // OR if selected item caption is changed. + getWidget() + .setPromptingOff(suggestion.getReplacementString()); + getWidget().selectedOptionKey = suggestionKey; + } + } + getWidget().currentSuggestion = suggestion; + getWidget().setSelectedItemIcon(suggestion.getIconUri()); + // only a single item can be selected + break; + } } private boolean isWidgetsCurrentSelectionTextInTextBox() { @@ -234,15 +311,50 @@ public class ComboBoxConnector extends AbstractFieldConnector .equals(getWidget().tb.getText()); } + private void resetSelection() { + if (!getWidget().waitingForFilteringResponse + || getWidget().popupOpenerClicked) { + // select nulled + if (!getWidget().focused) { + /* + * client.updateComponent overwrites all styles so we must + * ALWAYS set the prompting style at this point, even though we + * think it has been set already... + */ + getWidget().setPromptingOff(""); + if (getWidget().enabled && !getWidget().readonly) { + getWidget().setPromptingOn(); + } + } else { + // we have focus in field, prompting can't be set on, instead + // just clear the input if the value has changed from something + // else to null + if (getWidget().selectedOptionKey != null + || (getWidget().allowNewItem + && !getWidget().tb.getValue().isEmpty())) { + + boolean openedPopupWithNonScrollingMode = (getWidget().popupOpenerClicked + && getWidget().getSelectedCaption() != null); + if (!openedPopupWithNonScrollingMode) { + getWidget().tb.setValue(""); + } else { + getWidget().tb + .setValue(getWidget().getSelectedCaption()); + getWidget().tb.selectAll(); + } + } + } + getWidget().currentSuggestion = null; // #13217 + getWidget().setSelectedItemIcon(null); + getWidget().selectedOptionKey = null; + } + } + @Override public VFilterSelect getWidget() { return (VFilterSelect) super.getWidget(); } - private DataReceivedHandler getDataReceivedHandler() { - return getWidget().getDataReceivedHandler(); - } - @Override public ComboBoxState getState() { return (ComboBoxState) super.getState(); @@ -263,116 +375,4 @@ public class ComboBoxConnector extends AbstractFieldConnector getWidget().tb.setEnabled(widgetEnabled); } - /* - * These methods exist to move communications out of VFilterSelect, and may - * be refactored/removed in the future - */ - - /** - * Send a message about a newly created item to the server. - * - * This method is for internal use only and may be removed in future - * versions. - * - * @since 8.0 - * @param itemValue - * user entered string value for the new item - */ - public void sendNewItem(String itemValue) { - rpc.createNewItem(itemValue); - afterSendRequestToServer(); - } - - /** - * Send a message to the server to request the first page of items without - * filtering or selection. - * - * This method is for internal use only and may be removed in future - * versions. - * - * @since 8.0 - */ - public void requestFirstPage() { - sendSelection(null); - requestPage("", 0); - } - - /** - * Send a message to the server to request a page of items with a given - * filter. - * - * This method is for internal use only and may be removed in future - * versions. - * - * @since 8.0 - * @param filter - * the current filter string - * @param page - * the page number to get - */ - public void requestPage(String filter, int page) { - rpc.requestPage(filter, page); - afterSendRequestToServer(); - } - - /** - * Send a message to the server updating the current selection. - * - * This method is for internal use only and may be removed in future - * versions. - * - * @since 8.0 - * @param selection - * the current selection - */ - public void sendSelection(String selection) { - rpc.setSelectedItem(selection); - afterSendRequestToServer(); - } - - /** - * Notify the server that the combo box received focus. - * - * For timing reasons, ConnectorFocusAndBlurHandler is not used at the - * moment. - * - * This method is for internal use only and may be removed in future - * versions. - * - * @since 8.0 - */ - public void sendFocusEvent() { - boolean registeredListeners = hasEventListener(EventId.FOCUS); - if (registeredListeners) { - focusAndBlurRpc.focus(); - afterSendRequestToServer(); - } - } - - /** - * Notify the server that the combo box lost focus. - * - * For timing reasons, ConnectorFocusAndBlurHandler is not used at the - * moment. - * - * This method is for internal use only and may be removed in future - * versions. - * - * @since 8.0 - */ - public void sendBlurEvent() { - boolean registeredListeners = hasEventListener(EventId.BLUR); - if (registeredListeners) { - focusAndBlurRpc.blur(); - afterSendRequestToServer(); - } - } - - /* - * Called after any request to server. - */ - private void afterSendRequestToServer() { - getDataReceivedHandler().anyRequestSentToServer(); - } - } diff --git a/compatibility-server/src/main/java/com/vaadin/v7/ui/ComboBox.java b/compatibility-server/src/main/java/com/vaadin/v7/ui/ComboBox.java index a0af67359e..37d427b503 100644 --- a/compatibility-server/src/main/java/com/vaadin/v7/ui/ComboBox.java +++ b/compatibility-server/src/main/java/com/vaadin/v7/ui/ComboBox.java @@ -27,16 +27,14 @@ import java.util.Map; import com.vaadin.event.FieldEvents; import com.vaadin.event.FieldEvents.BlurEvent; import com.vaadin.event.FieldEvents.BlurListener; -import com.vaadin.event.FieldEvents.FocusAndBlurServerRpcImpl; import com.vaadin.event.FieldEvents.FocusEvent; import com.vaadin.event.FieldEvents.FocusListener; import com.vaadin.server.PaintException; import com.vaadin.server.PaintTarget; import com.vaadin.server.Resource; -import com.vaadin.ui.Component; import com.vaadin.v7.data.Container; import com.vaadin.v7.data.util.filter.SimpleStringFilter; -import com.vaadin.v7.shared.ui.combobox.ComboBoxServerRpc; +import com.vaadin.v7.shared.ui.combobox.ComboBoxConstants; import com.vaadin.v7.shared.ui.combobox.ComboBoxState; import com.vaadin.v7.shared.ui.combobox.FilteringMode; @@ -79,59 +77,18 @@ public class ComboBox extends AbstractSelect public String getStyle(ComboBox source, Object itemId); } - private ComboBoxServerRpc rpc = new ComboBoxServerRpc() { - @Override - public void createNewItem(String itemValue) { - if (isNewItemsAllowed()) { - // New option entered (and it is allowed) - if (itemValue != null && itemValue.length() > 0) { - getNewItemHandler().addNewItem(itemValue); - // rebuild list - filterstring = null; - prevfilterstring = null; - } - } - } - - @Override - public void setSelectedItem(String item) { - if (item == null) { - setValue(null, true); - } else { - final Object id = itemIdMapper.get(item); - if (id != null && id.equals(getNullSelectionItemId())) { - setValue(null, true); - } else { - setValue(id, true); - } - } - } + private String inputPrompt = null; - @Override - public void requestPage(String filter, int page) { - filterstring = filter; - if (filterstring != null) { - filterstring = filterstring.toLowerCase(getLocale()); - } - currentPage = page; - - // TODO this should trigger a data-only update instead of a full - // repaint - requestRepaint(); - } - }; - - FocusAndBlurServerRpcImpl focusBlurRpc = new FocusAndBlurServerRpcImpl( - this) { - @Override - protected void fireEvent(Component.Event event) { - ComboBox.this.fireEvent(event); - } - }; + /** + * Holds value of property pageLength. 0 disables paging. + */ + protected int pageLength = 10; // Current page when the user is 'paging' trough options private int currentPage = -1; + private FilteringMode filteringMode = FilteringMode.STARTSWITH; + private String filterstring; private String prevfilterstring; @@ -166,35 +123,40 @@ public class ComboBox extends AbstractSelect */ private boolean scrollToSelectedItem = true; + private String suggestionPopupWidth = null; + + /** + * If text input is not allowed, the ComboBox behaves like a pretty + * NativeSelect - the user can not enter any text and clicking the text + * field opens the drop down with options + */ + private boolean textInputAllowed = true; + private ItemStyleGenerator itemStyleGenerator = null; public ComboBox() { - init(); + initDefaults(); } public ComboBox(String caption, Collection<?> options) { super(caption, options); - init(); + initDefaults(); } public ComboBox(String caption, Container dataSource) { super(caption, dataSource); - init(); + initDefaults(); } public ComboBox(String caption) { super(caption); - init(); + initDefaults(); } /** - * Initialize the ComboBox with default settings and register client to - * server RPC implementation. + * Initialize the ComboBox with default settings */ - private void init() { - registerRpc(rpc); - registerRpc(focusBlurRpc); - + private void initDefaults() { setNewItemsAllowed(false); setImmediate(true); } @@ -206,7 +168,7 @@ public class ComboBox extends AbstractSelect * @return the current input prompt, or null if not enabled */ public String getInputPrompt() { - return getState(false).inputPrompt; + return inputPrompt; } /** @@ -217,46 +179,42 @@ public class ComboBox extends AbstractSelect * the desired input prompt, or null to disable */ public void setInputPrompt(String inputPrompt) { - getState().inputPrompt = inputPrompt; + this.inputPrompt = inputPrompt; + markAsDirty(); } private boolean isFilteringNeeded() { return filterstring != null && filterstring.length() > 0 - && getFilteringMode() != FilteringMode.OFF; - } - - /** - * A class representing an item in a ComboBox for server to client - * communication. This class is for internal use only and subject to change. - * - * @since 8.0 - */ - private static class ComboBoxItem implements Serializable { - String key = ""; - String caption = ""; - String style = null; - Resource icon = null; - - // constructor for a null item - public ComboBoxItem() { - } - - public ComboBoxItem(String key, String caption, String style, - Resource icon) { - this.key = key; - this.caption = caption; - this.style = style; - this.icon = icon; - } + && filteringMode != FilteringMode.OFF; } @Override public void paintContent(PaintTarget target) throws PaintException { isPainting = true; try { + if (inputPrompt != null) { + target.addAttribute(ComboBoxConstants.ATTR_INPUTPROMPT, + inputPrompt); + } + + if (!textInputAllowed) { + target.addAttribute(ComboBoxConstants.ATTR_NO_TEXT_INPUT, true); + } + // clear caption change listeners getCaptionChangeListener().clear(); + // The tab ordering number + if (getTabIndex() != 0) { + target.addAttribute("tabindex", getTabIndex()); + } + + // If the field is modified, but not committed, set modified + // attribute + if (isModified()) { + target.addAttribute("modified", true); + } + if (isNewItemsAllowed()) { target.addAttribute("allownewitem", true); } @@ -274,9 +232,20 @@ public class ComboBox extends AbstractSelect String[] selectedKeys = new String[(getValue() == null && getNullSelectionItemId() == null ? 0 : 1)]; + target.addAttribute("pagelength", pageLength); + + if (suggestionPopupWidth != null) { + target.addAttribute("suggestionPopupWidth", + suggestionPopupWidth); + } + + target.addAttribute("filteringmode", getFilteringMode().toString()); + // Paints the options and create array of selected id keys int keyIndex = 0; + target.startTag("options"); + if (currentPage < 0) { optionRequest = false; currentPage = 0; @@ -285,7 +254,8 @@ public class ComboBox extends AbstractSelect boolean nullFilteredOut = isFilteringNeeded(); // null option is needed and not filtered out, even if not on - // current page + // current + // page boolean nullOptionVisible = needNullSelectOption && !nullFilteredOut; @@ -296,18 +266,20 @@ public class ComboBox extends AbstractSelect // filtering options = getFilteredOptions(); filteredSize = options.size(); - options = sanitetizeList(options, nullOptionVisible); + options = sanitizeList(options, nullOptionVisible); } final boolean paintNullSelection = needNullSelectOption && currentPage == 0 && !nullFilteredOut; - List<ComboBoxItem> items = new ArrayList<>(); - if (paintNullSelection) { - ComboBoxItem item = new ComboBoxItem(); - item.style = getItemStyle(null); - items.add(item); + target.startTag("so"); + target.addAttribute("caption", ""); + target.addAttribute("key", ""); + + paintItemStyle(target, null); + + target.endTag("so"); } final Iterator<?> i = options.iterator(); @@ -327,31 +299,24 @@ public class ComboBox extends AbstractSelect final String key = itemIdMapper.key(id); final String caption = getItemCaption(id); final Resource icon = getItemIcon(id); - getCaptionChangeListener().addNotifierForItem(id); - // Prepare to paint the option - ComboBoxItem item = new ComboBoxItem(key, caption, - getItemStyle(id), icon); - items.add(item); + // Paints the option + target.startTag("so"); + if (icon != null) { + target.addAttribute("icon", icon); + } + target.addAttribute("caption", caption); + if (id != null && id.equals(getNullSelectionItemId())) { + target.addAttribute("nullselection", true); + } + target.addAttribute("key", key); if (keyIndex < selectedKeys.length && isSelected(id)) { // at most one item can be selected at a time selectedKeys[keyIndex++] = key; } - } - // paint the items - target.startTag("options"); - for (ComboBoxItem item : items) { - target.startTag("so"); - if (item.icon != null) { - target.addAttribute("icon", item.icon); - } - target.addAttribute("caption", item.caption); - target.addAttribute("key", item.key); - if (item.style != null) { - target.addAttribute("style", item.style); - } + paintItemStyle(target, id); target.endTag("so"); } @@ -389,11 +354,14 @@ public class ComboBox extends AbstractSelect } - private String getItemStyle(Object itemId) throws PaintException { + private void paintItemStyle(PaintTarget target, Object itemId) + throws PaintException { if (itemStyleGenerator != null) { - return itemStyleGenerator.getStyle(this, itemId); + String style = itemStyleGenerator.getStyle(this, itemId); + if (style != null && !style.isEmpty()) { + target.addAttribute("style", style); + } } - return null; } /** @@ -409,7 +377,8 @@ public class ComboBox extends AbstractSelect * selection */ public void setTextInputAllowed(boolean textInputAllowed) { - getState().textInputAllowed = textInputAllowed; + this.textInputAllowed = textInputAllowed; + markAsDirty(); } /** @@ -421,7 +390,7 @@ public class ComboBox extends AbstractSelect * @return */ public boolean isTextInputAllowed() { - return getState(false).textInputAllowed; + return textInputAllowed; } @Override @@ -429,11 +398,6 @@ public class ComboBox extends AbstractSelect return (ComboBoxState) super.getState(); } - @Override - protected ComboBoxState getState(boolean markAsDirty) { - return (ComboBoxState) super.getState(markAsDirty); - } - /** * Returns the filtered options for the current page using a container * filter. @@ -446,7 +410,7 @@ public class ComboBox extends AbstractSelect * {@link #canUseContainerFilter()}). * * Use {@link #getFilteredOptions()} and - * {@link #sanitetizeList(List, boolean)} if this is not the case. + * {@link #sanitizeList(List, boolean)} if this is not the case. * * @param needNullSelectOption * @return filtered list of options (may be empty) or null if cannot use @@ -455,7 +419,7 @@ public class ComboBox extends AbstractSelect protected List<?> getOptionsWithFilter(boolean needNullSelectOption) { Container container = getContainerDataSource(); - if (getPageLength() == 0 && !isFilteringNeeded()) { + if (pageLength == 0 && !isFilteringNeeded()) { // no paging or filtering: return all items filteredSize = container.size(); assert filteredSize >= 0; @@ -470,7 +434,7 @@ public class ComboBox extends AbstractSelect Filterable filterable = (Filterable) container; - Filter filter = buildFilter(filterstring, getFilteringMode()); + Filter filter = buildFilter(filterstring, filteringMode); // adding and removing filters leads to extraneous item set // change events from the underlying container, but the ComboBox does @@ -563,23 +527,25 @@ public class ComboBox extends AbstractSelect /** * Makes correct sublist of given list of options. - * + * <p> * If paint is not an option request (affected by page or filter change), * page will be the one where possible selection exists. - * + * <p> * Detects proper first and last item in list to return right page of * options. Also, if the current page is beyond the end of the list, it will * be adjusted. + * <p> + * Package private only for testing purposes. * * @param options * @param needNullSelectOption * flag to indicate if nullselect option needs to be taken into * consideration */ - private List<?> sanitetizeList(List<?> options, - boolean needNullSelectOption) { - - if (getPageLength() != 0 && options.size() > getPageLength()) { + List<?> sanitizeList(List<?> options, boolean needNullSelectOption) { + int totalRows = options.size() + (needNullSelectOption ? 1 : 0); + if (pageLength != 0 && totalRows > pageLength) { + // options will not fit on one page int indexToEnsureInView = -1; @@ -625,7 +591,7 @@ public class ComboBox extends AbstractSelect int size) { // Not all options are visible, find out which ones are on the // current "page". - int first = currentPage * getPageLength(); + int first = currentPage * pageLength; if (needNullSelectOption && currentPage > 0) { first--; } @@ -652,7 +618,7 @@ public class ComboBox extends AbstractSelect private int getLastItemIndexOnCurrentPage(boolean needNullSelectOption, int size, int first) { // page length usable for non-null items - int effectivePageLength = getPageLength() + int effectivePageLength = pageLength - (needNullSelectOption && (currentPage == 0) ? 1 : 0); return Math.min(size - 1, first + effectivePageLength - 1); } @@ -680,12 +646,12 @@ public class ComboBox extends AbstractSelect int indexToEnsureInView, int size) { if (indexToEnsureInView != -1) { int newPage = (indexToEnsureInView + (needNullSelectOption ? 1 : 0)) - / getPageLength(); + / pageLength; page = newPage; } // adjust the current page if beyond the end of the list - if (page * getPageLength() > size) { - page = (size + (needNullSelectOption ? 1 : 0)) / getPageLength(); + if (page * pageLength > size) { + page = (size + (needNullSelectOption ? 1 : 0)) / pageLength; } return page; } @@ -728,7 +694,7 @@ public class ComboBox extends AbstractSelect } else { caption = caption.toLowerCase(getLocale()); } - switch (getFilteringMode()) { + switch (filteringMode) { case CONTAINS: if (caption.indexOf(filterstring) > -1) { filteredOptions.add(itemId); @@ -757,17 +723,66 @@ public class ComboBox extends AbstractSelect // Not calling super.changeVariables due the history of select // component hierarchy - // all the client to server requests are now handled by RPC + // Selection change + if (variables.containsKey("selected")) { + final String[] ka = (String[]) variables.get("selected"); + + // Single select mode + if (ka.length == 0) { + + // Allows deselection only if the deselected item is visible + final Object current = getValue(); + final Collection<?> visible = getVisibleItemIds(); + if (visible != null && visible.contains(current)) { + setValue(null, true); + } + } else { + final Object id = itemIdMapper.get(ka[0]); + if (id != null && id.equals(getNullSelectionItemId())) { + setValue(null, true); + } else { + setValue(id, true); + } + } + } + + String newFilter; + if ((newFilter = (String) variables.get("filter")) != null) { + // this is a filter request + currentPage = ((Integer) variables.get("page")).intValue(); + filterstring = newFilter; + if (filterstring != null) { + filterstring = filterstring.toLowerCase(getLocale()); + } + requestRepaint(); + } else if (isNewItemsAllowed()) { + // New option entered (and it is allowed) + final String newitem = (String) variables.get("newitem"); + if (newitem != null && newitem.length() > 0) { + getNewItemHandler().addNewItem(newitem); + // rebuild list + filterstring = null; + prevfilterstring = null; + } + } + + if (variables.containsKey(FocusEvent.EVENT_ID)) { + fireEvent(new FocusEvent(this)); + } + if (variables.containsKey(BlurEvent.EVENT_ID)) { + fireEvent(new BlurEvent(this)); + } + } @Override public void setFilteringMode(FilteringMode filteringMode) { - getState().filteringMode = filteringMode; + this.filteringMode = filteringMode; } @Override public FilteringMode getFilteringMode() { - return getState(false).filteringMode; + return filteringMode; } @Override @@ -797,7 +812,7 @@ public class ComboBox extends AbstractSelect * * @deprecated As of 7.0, use {@link ListSelect}, {@link OptionGroup} or * {@link TwinColSelect} instead - * @see com.vaadin.v7.ui.AbstractSelect#setMultiSelect(boolean) + * @see com.vaadin.ui.AbstractSelect#setMultiSelect(boolean) * @throws UnsupportedOperationException * if trying to activate multiselect mode */ @@ -816,7 +831,7 @@ public class ComboBox extends AbstractSelect * @deprecated As of 7.0, use {@link ListSelect}, {@link OptionGroup} or * {@link TwinColSelect} instead * - * @see com.vaadin.v7.ui.AbstractSelect#isMultiSelect() + * @see com.vaadin.ui.AbstractSelect#isMultiSelect() * * @return false */ @@ -832,7 +847,7 @@ public class ComboBox extends AbstractSelect * @return the pageLength */ public int getPageLength() { - return getState(false).pageLength; + return pageLength; } /** @@ -842,7 +857,7 @@ public class ComboBox extends AbstractSelect * @since 7.7 */ public String getPopupWidth() { - return getState(false).suggestionPopupWidth; + return suggestionPopupWidth; } /** @@ -853,7 +868,8 @@ public class ComboBox extends AbstractSelect * the pageLength to set */ public void setPageLength(int pageLength) { - getState().pageLength = pageLength; + this.pageLength = pageLength; + markAsDirty(); } /** @@ -867,7 +883,8 @@ public class ComboBox extends AbstractSelect * the width */ public void setPopupWidth(String width) { - getState().suggestionPopupWidth = width; + suggestionPopupWidth = width; + markAsDirty(); } /** diff --git a/compatibility-server/src/test/java/com/vaadin/v7/tests/server/components/ComboBoxValueChangeTest.java b/compatibility-server/src/test/java/com/vaadin/v7/tests/server/components/ComboBoxValueChangeTest.java index ffb46c519a..e62358f272 100644 --- a/compatibility-server/src/test/java/com/vaadin/v7/tests/server/components/ComboBoxValueChangeTest.java +++ b/compatibility-server/src/test/java/com/vaadin/v7/tests/server/components/ComboBoxValueChangeTest.java @@ -1,10 +1,10 @@ package com.vaadin.v7.tests.server.components; +import java.util.HashMap; +import java.util.Map; + import org.junit.Before; -import com.vaadin.server.ServerRpcManager; -import com.vaadin.server.ServerRpcMethodInvocation; -import com.vaadin.v7.shared.ui.combobox.ComboBoxServerRpc; import com.vaadin.v7.tests.server.component.abstractfield.AbstractFieldValueChangeTestBase; import com.vaadin.v7.ui.AbstractField; import com.vaadin.v7.ui.ComboBox; @@ -20,28 +20,16 @@ public class ComboBoxValueChangeTest @Before public void setUp() { - ComboBox combo = new ComboBox() { - @Override - public String getConnectorId() { - return "id"; - } - }; + ComboBox combo = new ComboBox(); combo.addItem("myvalue"); super.setUp(combo); } @Override protected void setValue(AbstractField<Object> field) { - ComboBox combo = (ComboBox) field; - ServerRpcMethodInvocation invocation = new ServerRpcMethodInvocation( - combo.getConnectorId(), ComboBoxServerRpc.class, - "setSelectedItem", 1); - invocation.setParameters(new Object[] { "myvalue" }); - try { - ServerRpcManager.applyInvocation(combo, invocation); - } catch (Exception e) { - throw new RuntimeException(e); - } + Map<String, Object> variables = new HashMap<>(); + variables.put("selected", new String[] { "myvalue" }); + ((ComboBox) field).changeVariables(field, variables); } } diff --git a/compatibility-server/src/test/java/com/vaadin/v7/ui/ComboBoxTest.java b/compatibility-server/src/test/java/com/vaadin/v7/ui/ComboBoxTest.java new file mode 100644 index 0000000000..62a0c0bdbd --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/v7/ui/ComboBoxTest.java @@ -0,0 +1,158 @@ +package com.vaadin.v7.ui; + +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import com.vaadin.v7.shared.ui.combobox.FilteringMode; + +public class ComboBoxTest { + + private ComboBox comboBox; + + @Before + public void setup() { + comboBox = new ComboBox(); + comboBox.setLocale(Locale.ENGLISH); + } + + @Test + public void options_noFilter() { + ComboBox comboBox = new ComboBox(); + for (int i = 0; i < 10; i++) { + comboBox.addItem("" + i); + } + + List<?> options = comboBox.getFilteredOptions(); + Assert.assertEquals(10, options.size()); + for (int i = 0; i < 10; i++) { + Assert.assertEquals("" + i, options.get(i)); + } + + } + + @Test + public void options_inMemoryFilteringStartsWith() { + for (int i = 0; i < 21; i++) { + comboBox.addItem("" + i); + } + + setFilterAndCurrentPage(comboBox, "1", 0); + + List<?> options = comboBox.getFilteredOptions(); + Assert.assertEquals(11, options.size()); + + } + + @Test + public void options_inMemoryFilteringContains() { + comboBox.setFilteringMode(FilteringMode.CONTAINS); + for (int i = 0; i < 21; i++) { + comboBox.addItem("" + i); + } + + setFilterAndCurrentPage(comboBox, "2", 0); + List<?> options = comboBox.getFilteredOptions(); + Assert.assertEquals(3, options.size()); + + } + + private static void setFilterAndCurrentPage(ComboBox comboBox, + String filterString, int currentPage) { + Map<String, Object> variables = new HashMap<>(); + variables.put("filter", filterString); + variables.put("page", currentPage); + comboBox.changeVariables(null, variables); + + } + + @Test + public void getOptions_moreThanOnePage_noNullItem() { + int nrOptions = comboBox.getPageLength() * 2; + for (int i = 0; i < nrOptions; i++) { + comboBox.addItem("" + i); + } + setFilterAndCurrentPage(comboBox, "", 0); + + List<?> goingToClient = comboBox + .sanitizeList(comboBox.getFilteredOptions(), false); + Assert.assertEquals(comboBox.getPageLength(), goingToClient.size()); + + } + + @Test + public void getOptions_moreThanOnePage_nullItem() { + int nrOptions = comboBox.getPageLength() * 2; + for (int i = 0; i < nrOptions; i++) { + comboBox.addItem("" + i); + } + + setFilterAndCurrentPage(comboBox, "", 0); + List<?> goingToClient = comboBox + .sanitizeList(comboBox.getFilteredOptions(), true); + // Null item is shown on first page + Assert.assertEquals(comboBox.getPageLength() - 1, goingToClient.size()); + + setFilterAndCurrentPage(comboBox, "", 1); + goingToClient = comboBox.sanitizeList(comboBox.getFilteredOptions(), + true); + // Null item is not shown on the second page + Assert.assertEquals(comboBox.getPageLength(), goingToClient.size()); + + } + + @Test + public void getOptions_lessThanOnePage_noNullItem() { + int nrOptions = comboBox.getPageLength() / 2; + for (int i = 0; i < nrOptions; i++) { + comboBox.addItem("" + i); + } + setFilterAndCurrentPage(comboBox, "", 0); + + List<?> goingToClient = comboBox + .sanitizeList(comboBox.getFilteredOptions(), false); + Assert.assertEquals(nrOptions, goingToClient.size()); + + } + + @Test + public void getOptions_lessThanOnePage_withNullItem() { + int nrOptions = comboBox.getPageLength() / 2; + for (int i = 0; i < nrOptions; i++) { + comboBox.addItem("" + i); + } + setFilterAndCurrentPage(comboBox, "", 0); + + List<?> goingToClient = comboBox + .sanitizeList(comboBox.getFilteredOptions(), true); + // All items + null still fit on one page + Assert.assertEquals(nrOptions, goingToClient.size()); + + } + + @Test + public void getOptions_exactlyOnePage_withNullItem() { + int nrOptions = comboBox.getPageLength(); + for (int i = 0; i < nrOptions; i++) { + comboBox.addItem("" + i); + } + setFilterAndCurrentPage(comboBox, "", 0); + + List<?> goingToClient = comboBox + .sanitizeList(comboBox.getFilteredOptions(), true); + // Null item on first page + Assert.assertEquals(nrOptions - 1, goingToClient.size()); + + setFilterAndCurrentPage(comboBox, "", 1); + goingToClient = comboBox.sanitizeList(comboBox.getFilteredOptions(), + true); + // All but one was on the first page + Assert.assertEquals(1, goingToClient.size()); + + } +} diff --git a/compatibility-shared/src/main/java/com/vaadin/v7/shared/ui/combobox/ComboBoxServerRpc.java b/compatibility-shared/src/main/java/com/vaadin/v7/shared/ui/combobox/ComboBoxServerRpc.java deleted file mode 100644 index 69d360b956..0000000000 --- a/compatibility-shared/src/main/java/com/vaadin/v7/shared/ui/combobox/ComboBoxServerRpc.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * 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 com.vaadin.v7.shared.ui.combobox; - -import com.vaadin.shared.communication.ServerRpc; - -/** - * Client to server RPC interface for ComboBox. - * - * @since 8.0 - */ -public interface ComboBoxServerRpc extends ServerRpc { - /** - * Create a new item in the combo box. This method can only be used when the - * ComboBox is configured to allow the creation of new items by the user. - * - * @param itemValue - * user entered string value for the new item - */ - public void createNewItem(String itemValue); - - /** - * Set the current selection. - * - * @param item - * the id of a single item or null to deselect the current value - */ - public void setSelectedItem(String item); - - /** - * Request the server to send a page of the item list. - * - * @param filter - * filter string interpreted according to the current filtering - * mode - * @param page - * zero based page number - */ - public void requestPage(String filter, int page); -} diff --git a/compatibility-shared/src/main/java/com/vaadin/v7/shared/ui/combobox/ComboBoxState.java b/compatibility-shared/src/main/java/com/vaadin/v7/shared/ui/combobox/ComboBoxState.java index b1b9a7252e..d76effc404 100644 --- a/compatibility-shared/src/main/java/com/vaadin/v7/shared/ui/combobox/ComboBoxState.java +++ b/compatibility-shared/src/main/java/com/vaadin/v7/shared/ui/combobox/ComboBoxState.java @@ -26,40 +26,4 @@ public class ComboBoxState extends AbstractSelectState { { primaryStyleName = "v-filterselect"; } - - /** - * If text input is not allowed, the ComboBox behaves like a pretty - * NativeSelect - the user can not enter any text and clicking the text - * field opens the drop down with options. - * - * @since 8.0 - */ - public boolean textInputAllowed = true; - - /** - * A textual prompt that is displayed when the select would otherwise be - * empty, to prompt the user for input. - * - * @since 8.0 - */ - public String inputPrompt = null; - - /** - * Number of items to show per page or 0 to disable paging. - */ - public int pageLength = 10; - - /** - * Current filtering mode (look for match of the user typed string in the - * beginning of the item caption or anywhere in the item caption). - */ - public FilteringMode filteringMode = FilteringMode.STARTSWITH; - - /** - * 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 ComboBox - * itself. - */ - public String suggestionPopupWidth = null; - } |