├── assets └── screenshot1.png ├── vaadin-combobox-multiselect-demo ├── src │ └── main │ │ ├── resources │ │ └── org │ │ │ └── vaadin │ │ │ └── addons │ │ │ └── README │ │ ├── webapp │ │ ├── META-INF │ │ │ ├── MANIFEST.MF │ │ │ └── context.xml │ │ └── VAADIN │ │ │ └── themes │ │ │ └── demo │ │ │ ├── favicon.ico │ │ │ ├── images │ │ │ └── radial-gradient.png │ │ │ └── styles.scss │ │ └── java │ │ └── org │ │ └── vaadin │ │ └── addons │ │ └── demo │ │ ├── util │ │ ├── StringGenerator.java │ │ ├── DemoItem.java │ │ └── TestIcon.java │ │ └── DemoUI.java └── pom.xml ├── vaadin-combobox-multiselect-addon ├── src │ ├── main │ │ ├── resources │ │ │ └── org │ │ │ │ └── vaadin │ │ │ │ └── addons │ │ │ │ ├── public │ │ │ │ └── vaadin-combobox-multiselect │ │ │ │ │ └── styles.css │ │ │ │ └── WidgetSet.gwt.xml │ │ └── java │ │ │ └── org │ │ │ └── vaadin │ │ │ └── addons │ │ │ ├── client │ │ │ ├── ComboBoxMultiselectConstants.java │ │ │ ├── ComboBoxMultiselectServerRpc.java │ │ │ ├── JsniMousewheelHandler.java │ │ │ ├── ComboBoxMultiselectState.java │ │ │ ├── ComboBoxMultiselectConnector.java │ │ │ └── VComboBoxMultiselect.java │ │ │ └── ComboBoxMultiselect.java │ └── test │ │ └── java │ │ └── org │ │ └── vaadin │ │ └── addons │ │ └── MyComponentTest.java ├── assembly │ ├── MANIFEST.MF │ └── assembly.xml └── pom.xml ├── .gitignore ├── pom.xml ├── README.md └── LICENSE.txt /assets/screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bonprix/vaadin-combobox-multiselect/HEAD/assets/screenshot1.png -------------------------------------------------------------------------------- /vaadin-combobox-multiselect-demo/src/main/resources/org/vaadin/addons/README: -------------------------------------------------------------------------------- 1 | Please add your static resources here 2 | -------------------------------------------------------------------------------- /vaadin-combobox-multiselect-demo/src/main/webapp/META-INF/MANIFEST.MF: -------------------------------------------------------------------------------- 1 | Manifest-Version: 1.0 2 | Class-Path: 3 | 4 | -------------------------------------------------------------------------------- /vaadin-combobox-multiselect-demo/src/main/webapp/META-INF/context.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /vaadin-combobox-multiselect-addon/src/main/resources/org/vaadin/addons/public/vaadin-combobox-multiselect/styles.css: -------------------------------------------------------------------------------- 1 | /* comboboxmultiselect suggestpopup */ 2 | .gwt-MenuItem.align-center { 3 | text-align: center; 4 | } -------------------------------------------------------------------------------- /vaadin-combobox-multiselect-demo/src/main/webapp/VAADIN/themes/demo/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bonprix/vaadin-combobox-multiselect/HEAD/vaadin-combobox-multiselect-demo/src/main/webapp/VAADIN/themes/demo/favicon.ico -------------------------------------------------------------------------------- /vaadin-combobox-multiselect-demo/src/main/webapp/VAADIN/themes/demo/images/radial-gradient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bonprix/vaadin-combobox-multiselect/HEAD/vaadin-combobox-multiselect-demo/src/main/webapp/VAADIN/themes/demo/images/radial-gradient.png -------------------------------------------------------------------------------- /vaadin-combobox-multiselect-addon/assembly/MANIFEST.MF: -------------------------------------------------------------------------------- 1 | Manifest-Version: 1.0 2 | Vaadin-Package-Version: 1 3 | Vaadin-Addon: ${Vaadin-Addon} 4 | Vaadin-License-Title: ${Vaadin-License-Title} 5 | Implementation-Vendor: ${Implementation-Vendor} 6 | Implementation-Title: ${Implementation-Title} 7 | Implementation-Version: ${Implementation-Version} 8 | -------------------------------------------------------------------------------- /vaadin-combobox-multiselect-addon/src/test/java/org/vaadin/addons/MyComponentTest.java: -------------------------------------------------------------------------------- 1 | package org.vaadin.addons; 2 | 3 | import junit.framework.Assert; 4 | import org.junit.Test; 5 | 6 | // JUnit tests here 7 | public class MyComponentTest { 8 | 9 | @Test 10 | public void thisAlwaysPasses() { 11 | Assert.assertEquals(true, true); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /vaadin-combobox-multiselect-addon/src/main/resources/org/vaadin/addons/WidgetSet.gwt.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | 3 | # Mobile Tools for Java (J2ME) 4 | .mtj.tmp/ 5 | 6 | # Package Files # 7 | *.jar 8 | *.war 9 | *.ear 10 | rebel.xml 11 | 12 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 13 | hs_err_pid* 14 | 15 | 16 | build/ 17 | coverage.ec 18 | /target 19 | target 20 | velocity.log 21 | velocity.log* 22 | src/main/webapp/VAADIN/gwt-unitCache* 23 | */src/main/webapp/VAADIN/widgetsets* 24 | src/main/webapp/VAADIN/widgetsets/* 25 | .classpath 26 | .project 27 | */.settings/ 28 | *.settings -------------------------------------------------------------------------------- /vaadin-combobox-multiselect-addon/src/main/java/org/vaadin/addons/client/ComboBoxMultiselectConstants.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2000-2016 Vaadin Ltd. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | package org.vaadin.addons.client; 17 | 18 | import java.io.Serializable; 19 | 20 | /** 21 | * Constants related to the combo box component and its client-server 22 | * communication. 23 | * 24 | * @since 8.0 25 | * @author Vaadin Ltd 26 | */ 27 | public class ComboBoxMultiselectConstants implements Serializable { 28 | public static final String STYLE = "style"; 29 | public static final String ICON = "icon"; 30 | } 31 | -------------------------------------------------------------------------------- /vaadin-combobox-multiselect-demo/src/main/webapp/VAADIN/themes/demo/styles.scss: -------------------------------------------------------------------------------- 1 | @import "../valo/valo.scss"; 2 | 3 | $gray: #d1d1cf; 4 | $green: #40b527; 5 | $darkgreen: darken($green, 30%); 6 | 7 | // Prefix all selectors in your theme with .demo 8 | .demo { 9 | 10 | // Include valo theme styles in your theme 11 | @include valo; 12 | 13 | // You can style your demo app right here 14 | .demoContentLayout { 15 | background-color: $gray; 16 | background-image: url(images/radial-gradient.png); 17 | background-size: 90%; 18 | background-position: center center; 19 | background-repeat: no-repeat; 20 | } 21 | 22 | // You can also customize your component for the demo 23 | // app, but remember that these styles are not part of 24 | // the component. To include built-in CSS for your component, 25 | // edit client/styles.css under java sources 26 | div.vaadin-combobox-multiselect { 27 | color: $green; 28 | font-size: 50pt; 29 | font-weight: bold; 30 | text-shadow: 0px 3px 0px $darkgreen, 31 | 0px 14px 10px rgba(0,0,0,0.15), 32 | 0px 24px 2px rgba(0,0,0,0.1), 33 | 0px 34px 30px rgba(0,0,0,0.1); 34 | text-align: center; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /vaadin-combobox-multiselect-demo/src/main/java/org/vaadin/addons/demo/util/StringGenerator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2000-2016 Vaadin Ltd. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | package org.vaadin.addons.demo.util; 17 | 18 | import com.vaadin.shared.util.SharedUtil; 19 | 20 | public class StringGenerator { 21 | static String[] strings = { "lorem", "ipsum", "dolor", "sit", "amet", "consectetur", "quid", "securi", "etiam", 22 | "tamquam", "eu", "fugiat", "nulla", "pariatur" }; 23 | int stringCount = -1; 24 | 25 | String nextString(boolean capitalize) { 26 | if (++this.stringCount >= strings.length) { 27 | this.stringCount = 0; 28 | } 29 | return capitalize ? SharedUtil.capitalize(strings[this.stringCount]) : strings[this.stringCount]; 30 | } 31 | 32 | } -------------------------------------------------------------------------------- /vaadin-combobox-multiselect-addon/assembly/assembly.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | addon 7 | 8 | 10 | 11 | 12 | zip 13 | 14 | 15 | 16 | false 17 | 18 | 19 | 20 | .. 21 | 22 | LICENSE.txt 23 | README.md 24 | 25 | 26 | 27 | target 28 | 29 | 30 | *.jar 31 | *.pdf 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | assembly/MANIFEST.MF 40 | META-INF 41 | true 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /vaadin-combobox-multiselect-demo/src/main/java/org/vaadin/addons/demo/util/DemoItem.java: -------------------------------------------------------------------------------- 1 | package org.vaadin.addons.demo.util; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collection; 5 | 6 | import com.vaadin.server.Resource; 7 | 8 | public class DemoItem { 9 | 10 | private String caption; 11 | private int index; 12 | private String description; 13 | private Resource icon; 14 | 15 | public String getCaption() { 16 | return this.caption; 17 | } 18 | 19 | public void setCaption(String caption) { 20 | this.caption = caption; 21 | } 22 | 23 | public int getIndex() { 24 | return this.index; 25 | } 26 | 27 | public void setIndex(int index) { 28 | this.index = index; 29 | } 30 | 31 | public String getDescription() { 32 | return this.description; 33 | } 34 | 35 | public void setDescription(String description) { 36 | this.description = description; 37 | } 38 | 39 | public Resource getIcon() { 40 | return this.icon; 41 | } 42 | 43 | public void setIcon(Resource icon) { 44 | this.icon = icon; 45 | } 46 | 47 | public static Collection generate(final int size) { 48 | Collection items = new ArrayList<>(); 49 | 50 | TestIcon testIcon = new TestIcon(90); 51 | StringGenerator sg = new StringGenerator(); 52 | for (int i = 1; i < size + 1; i++) { 53 | DemoItem demoItem = new DemoItem(); 54 | demoItem.setCaption(sg.nextString(true) + " " + sg.nextString(false)); 55 | demoItem.setIndex(i); 56 | demoItem.setDescription(sg.nextString(true) + " " + sg.nextString(false) + " " + sg.nextString(false)); 57 | demoItem.setIcon(testIcon.get()); 58 | items.add(demoItem); 59 | } 60 | 61 | return items; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /vaadin-combobox-multiselect-demo/src/main/java/org/vaadin/addons/demo/util/TestIcon.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2000-2013 Vaadin Ltd. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | package org.vaadin.addons.demo.util; 17 | 18 | import java.util.ArrayList; 19 | import java.util.List; 20 | 21 | import com.vaadin.icons.VaadinIcons; 22 | import com.vaadin.server.Resource; 23 | import com.vaadin.server.ThemeResource; 24 | 25 | /** 26 | * 27 | * @since 28 | * @author Vaadin Ltd 29 | */ 30 | public class TestIcon { 31 | 32 | int iconCount = 0; 33 | 34 | public TestIcon(int startIndex) { 35 | this.iconCount = startIndex; 36 | } 37 | 38 | public Resource get() { 39 | return get(false, 32); 40 | } 41 | 42 | public Resource get(boolean isImage) { 43 | return get(isImage, 32); 44 | } 45 | 46 | public Resource get(boolean isImage, int imageSize) { 47 | if (!isImage) { 48 | if (++this.iconCount >= ICONS.size()) { 49 | this.iconCount = 0; 50 | } 51 | return ICONS.get(this.iconCount); 52 | } 53 | return new ThemeResource("../runo/icons/" + imageSize + "/document.png"); 54 | } 55 | 56 | static List ICONS = new ArrayList<>(); 57 | static { 58 | for (VaadinIcons vaadinIcon : VaadinIcons.values()) { 59 | ICONS.add(vaadinIcon); 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | org.vaadin.addons 6 | vaadin-combobox-multiselect-root 7 | pom 8 | 2.9-SNAPSHOT 9 | ComboBoxMultiselect Add-on Project 10 | 11 | 12 | 3 13 | 14 | 15 | 16 | vaadin-combobox-multiselect-addon 17 | vaadin-combobox-multiselect-demo 18 | 19 | 20 | 21 | 22 | 23 | vaadin-prerelease 24 | 25 | false 26 | 27 | 28 | 29 | 30 | vaadin-prereleases 31 | http://maven.vaadin.com/vaadin-prereleases 32 | 33 | 34 | vaadin-snapshots 35 | https://oss.sonatype.org/content/repositories/vaadin-snapshots/ 36 | 37 | false 38 | 39 | 40 | true 41 | 42 | 43 | 44 | 45 | 46 | vaadin-prereleases 47 | http://maven.vaadin.com/vaadin-prereleases 48 | 49 | 50 | vaadin-snapshots 51 | https://oss.sonatype.org/content/repositories/vaadin-snapshots/ 52 | 53 | false 54 | 55 | 56 | true 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /vaadin-combobox-multiselect-addon/src/main/java/org/vaadin/addons/client/ComboBoxMultiselectServerRpc.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2000-2016 Vaadin Ltd. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | package org.vaadin.addons.client; 17 | 18 | import java.util.Set; 19 | 20 | import com.vaadin.shared.communication.ServerRpc; 21 | 22 | /** 23 | * Client to server RPC interface for ComboBoxMultiselect. 24 | * 25 | * @since 8.0 26 | */ 27 | public interface ComboBoxMultiselectServerRpc extends ServerRpc { 28 | /** 29 | * Create a new item in the combo box. This method can only be used when the 30 | * ComboBoxMultiselect is configured to allow the creation of new items by 31 | * the user. 32 | * 33 | * @param itemValue 34 | * user entered string value for the new item 35 | */ 36 | public void createNewItem(String itemValue); 37 | 38 | /** 39 | * Sets the filter to use. 40 | * 41 | * @param filter 42 | * filter string interpreted according to the current filtering 43 | * mode 44 | */ 45 | public void setFilter(String filter); 46 | 47 | /** 48 | * Updates the selected items based on their keys. 49 | * 50 | * @param addedItemKeys 51 | * the item keys added to selection 52 | * @param removedItemKeys 53 | * the item keys removed from selection 54 | * @param sortingNeeded 55 | * is sorting needed before sending data back to client 56 | */ 57 | void updateSelection(Set addedItemKeys, Set removedItemKeys, boolean sortingNeeded); 58 | 59 | /** 60 | * Send the blur event. 61 | */ 62 | void blur(); 63 | 64 | /** 65 | * Select all. 66 | */ 67 | void selectAll(String filter); 68 | 69 | /** 70 | * Clear. 71 | */ 72 | public void clear(String filter); 73 | } 74 | -------------------------------------------------------------------------------- /vaadin-combobox-multiselect-addon/src/main/java/org/vaadin/addons/client/JsniMousewheelHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2000-2016 Vaadin Ltd. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | package org.vaadin.addons.client; 17 | 18 | import com.google.gwt.core.client.JavaScriptObject; 19 | import com.google.gwt.dom.client.Element; 20 | import com.google.gwt.user.client.Event; 21 | import com.google.gwt.user.client.ui.Widget; 22 | import com.vaadin.client.widgets.Escalator; 23 | 24 | /** 25 | * A mousewheel handling class to get around the limits of 26 | * {@link Event#ONMOUSEWHEEL}. 27 | * 28 | * For internal use only. May be removed or replaced in the future. 29 | * 30 | * @see Escalator.JsniWorkaround 31 | */ 32 | // FIXME remove the copy in v7 package 33 | abstract class JsniMousewheelHandler { 34 | 35 | /** 36 | * A JavaScript function that handles the mousewheel DOM event, and passes 37 | * it on to Java code. 38 | * 39 | * @see #createMousewheelListenerFunction(Widget) 40 | */ 41 | protected final JavaScriptObject mousewheelListenerFunction; 42 | 43 | protected JsniMousewheelHandler(final Widget widget) { 44 | this.mousewheelListenerFunction = createMousewheelListenerFunction(widget); 45 | } 46 | 47 | /** 48 | * A method that constructs the JavaScript function that will be stored into 49 | * {@link #mousewheelListenerFunction}. 50 | * 51 | * @param widget 52 | * a reference to the current instance of {@link Widget} 53 | */ 54 | protected abstract JavaScriptObject createMousewheelListenerFunction(Widget widget); 55 | 56 | public native void attachMousewheelListener(Element element) 57 | /*-{ 58 | if (element.addEventListener) { 59 | // FireFox likes "wheel", while others use "mousewheel" 60 | var eventName = 'onmousewheel' in element ? 'mousewheel' : 'wheel'; 61 | element.addEventListener(eventName, this.@com.vaadin.client.ui.JsniMousewheelHandler::mousewheelListenerFunction); 62 | } 63 | }-*/; 64 | 65 | public native void detachMousewheelListener(Element element) 66 | /*-{ 67 | if (element.addEventListener) { 68 | // FireFox likes "wheel", while others use "mousewheel" 69 | var eventName = element.onwheel===undefined?"mousewheel":"wheel"; 70 | element.removeEventListener(eventName, this.@com.vaadin.client.ui.JsniMousewheelHandler::mousewheelListenerFunction); 71 | } 72 | }-*/; 73 | 74 | } 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ComboBoxMultiselect Add-on for Vaadin 8 2 | 3 | The ComboBoxMultiselect component is a client-side Widget. As example was used the ComboBox from Vaadin and the VFilterSelect from Vaadin. 4 | 5 | ![screenshot](assets/screenshot1.png) 6 | 7 | ### Features: 8 | - multiselect with checkbox 9 | - clear selection button 10 | - ordering moves selected always to top 11 | 12 | ## Online demo 13 | http://bonprix.jelastic.servint.net/vaadin-combobox-multiselect-demo/ 14 | 15 | ## Usage 16 | 17 | ### Maven 18 | 19 | ```xml 20 | 21 | org.vaadin.addons 22 | vaadin-combobox-multiselect 23 | 2.0 24 | 25 | 26 | 27 | vaadin-addons 28 | http://maven.vaadin.com/vaadin-addons 29 | 30 | ``` 31 | 32 | No widgetset required. 33 | 34 | ## Download release 35 | 36 | Official releases of this add-on are available at Vaadin Directory. For Maven instructions, download and reviews, go to http://vaadin.com/addon/vaadin-combobox-multiselect 37 | 38 | ## Building and running demo 39 | 40 | git clone https://github.com/bonprix/vaadin-combobox-multiselect 41 | mvn clean install 42 | cd demo 43 | mvn jetty:run 44 | 45 | To see the demo, navigate to http://localhost:8080/ 46 | 47 | ## Release notes 48 | 49 | ### Version 2.0 50 | - Vaadin 8 51 | 52 | ## Known issues 53 | 54 | - please report issues and help us to make this even better ;) 55 | 56 | ## Roadmap 57 | 58 | This component is developed as a part of a bonprix project with no public roadmap or any guarantees of upcoming releases. That said, the following features are planned for upcoming releases: 59 | - use scss 60 | 61 | ## Issue tracking 62 | 63 | The issues for this add-on are tracked on its github.com page. All bug reports and feature requests are appreciated. 64 | 65 | ## Contributions 66 | 67 | Contributions are welcome, but there are no guarantees that they are accepted as such. Process for contributing is the following: 68 | - Fork this project 69 | - Create an issue to this project about the contribution (bug or feature) if there is no such issue about it already. Try to keep the scope minimal. 70 | - Develop and test the fix or functionality carefully. Only include minimum amount of code needed to fix the issue. 71 | - Refer to the fixed issue in commit 72 | - Send a pull request for the original project 73 | - Comment on the original issue that you have implemented a fix for it 74 | 75 | ## License & Author 76 | 77 | Add-on is distributed under MIT License. For license terms, see LICENSE.txt. 78 | 79 | vaadin-combobox-multiselect is written by members of Bonprix Handelsgesellschaft mbh: 80 | - Thorben von Hacht (https://github.com/thorbenvh8) 81 | 82 | # Developer Guide 83 | 84 | ## Getting started 85 | 86 | Here is a simple example on how to try out the add-on component: 87 | 88 | ```java 89 | 90 | // Initialize a list with items 91 | List list = new ArrayList(); 92 | NamedObject vaadin = new NamedObject(2L, "Vaadin"); 93 | list.add(new NamedObject(1L, "Java")); 94 | list.add(vaadin); 95 | list.add(new NamedObject(3L, "Bonprix")); 96 | list.add(new NamedObject(4L, "Addon")); 97 | 98 | // Initialize the ComboBoxMultiselect 99 | final ComboBoxMultiselect comboBoxMultiselect = new ComboBoxMultiselect<>(); 100 | comboBoxMultiselect.setPlaceholder("Type here"); 101 | comboBoxMultiselect.setCaption("ComboBoxMultiselect"); 102 | comboBoxMultiselect.setItems(list); 103 | comboBoxMultiselect.setValue(new HashSet<>(Arrays.asList(vaadin))); 104 | 105 | ``` 106 | 107 | For a more comprehensive example, see src/test/java/org/vaadin/template/demo/DemoUI.java -------------------------------------------------------------------------------- /vaadin-combobox-multiselect-addon/src/main/java/org/vaadin/addons/client/ComboBoxMultiselectState.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2000-2016 Vaadin Ltd. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | package org.vaadin.addons.client; 17 | 18 | import java.util.LinkedHashSet; 19 | import java.util.Set; 20 | 21 | import com.vaadin.shared.annotations.DelegateToWidget; 22 | import com.vaadin.shared.annotations.NoLayout; 23 | import com.vaadin.shared.ui.abstractmultiselect.AbstractMultiSelectState; 24 | 25 | /** 26 | * Shared state for the ComboBoxMultiselect component. 27 | * 28 | * @since 7.0 29 | */ 30 | public class ComboBoxMultiselectState extends AbstractMultiSelectState { 31 | { 32 | // TODO ideally this would be v-combobox, but that would affect a lot of 33 | // themes 34 | this.primaryStyleName = "v-filterselect"; 35 | } 36 | 37 | /** 38 | * The keys of the currently selected items or {@code null} if no item is 39 | * selected. 40 | */ 41 | public Set selectedItemKeys = new LinkedHashSet<>(); 42 | 43 | /** 44 | * If text input is not allowed, the ComboBoxMultiselect behaves like a 45 | * pretty NativeSelect - the user can not enter any text and clicking the 46 | * text field opens the drop down with options. 47 | * 48 | * @since 8.0 49 | */ 50 | @DelegateToWidget 51 | public boolean textInputAllowed = true; 52 | 53 | /** 54 | * The prompt to display in an empty field. Null when disabled. 55 | */ 56 | @DelegateToWidget 57 | @NoLayout 58 | public String placeholder = null; 59 | 60 | /** 61 | * Number of items to show per page or 0 to disable paging. 62 | */ 63 | @DelegateToWidget 64 | public int pageLength; 65 | 66 | /** 67 | * Suggestion pop-up's width as a CSS string. By using relative units (e.g. 68 | * "50%") it's possible to set the popup's width relative to the 69 | * ComboBoxMultiselect itself. 70 | */ 71 | @DelegateToWidget 72 | public String suggestionPopupWidth = "100%"; 73 | 74 | /** 75 | * True to allow the user to send new items to the server, false to only 76 | * select among existing items. 77 | */ 78 | @DelegateToWidget 79 | public boolean allowNewItems = false; 80 | 81 | /** 82 | * True to automatically scroll the ComboBoxMultiselect to show the selected 83 | * item, false not to search for it in the results. 84 | */ 85 | public boolean scrollToSelectedItem = false; 86 | 87 | /** 88 | * The caption of the currently selected items or {@code null} if no item is 89 | * selected. 90 | */ 91 | public String selectedItemsCaption; 92 | 93 | /** 94 | * The caption of the clear button. 95 | */ 96 | @DelegateToWidget 97 | public String clearButtonCaption = "clear"; 98 | 99 | /** 100 | * The caption of the select all button. 101 | */ 102 | @DelegateToWidget 103 | public String selectAllButtonCaption = "select all"; 104 | 105 | /** 106 | * If the clear button should be visible. 107 | */ 108 | @DelegateToWidget 109 | public boolean showClearButton; 110 | 111 | /** 112 | * If the select all button should be visible. 113 | */ 114 | @DelegateToWidget 115 | public boolean showSelectAllButton; 116 | 117 | } 118 | -------------------------------------------------------------------------------- /vaadin-combobox-multiselect-demo/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | org.vaadin.addons 6 | vaadin-combobox-multiselect-demo 7 | war 8 | 2.9-SNAPSHOT 9 | ComboBoxMultiselect Add-on Demo 10 | 11 | 12 | 3 13 | 14 | 15 | 16 | UTF-8 17 | 1.8 18 | 1.8 19 | 8.7.0 20 | 8.7.0 21 | 9.3.9.v20160517 22 | 23 | 24 | 42 | 43 | 44 | 45 | Apache 2 46 | http://www.apache.org/licenses/LICENSE-2.0.txt 47 | repo 48 | 49 | 50 | 51 | 52 | 53 | vaadin-addons 54 | http://maven.vaadin.com/vaadin-addons 55 | 56 | 57 | 58 | 59 | 60 | 61 | com.vaadin 62 | vaadin-bom 63 | ${vaadin.version} 64 | pom 65 | import 66 | 67 | 68 | 69 | 70 | 71 | 72 | org.vaadin.addons 73 | vaadin-combobox-multiselect 74 | ${project.version} 75 | 76 | 77 | com.vaadin 78 | vaadin-push 79 | 80 | 81 | com.vaadin 82 | vaadin-client-compiler 83 | provided 84 | 85 | 86 | com.vaadin 87 | vaadin-themes 88 | 89 | 90 | javax.servlet 91 | javax.servlet-api 92 | 3.0.1 93 | provided 94 | 95 | 96 | 97 | 98 | 99 | 100 | maven-war-plugin 101 | 3.0.0 102 | 103 | false 104 | 105 | 106 | 107 | 108 | com.vaadin 109 | vaadin-maven-plugin 110 | ${vaadin.plugin.version} 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | resources 121 | update-widgetset 122 | compile 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | org.eclipse.jetty 132 | jetty-maven-plugin 133 | ${jetty.plugin.version} 134 | 135 | 2 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | vaadin-prerelease 147 | 148 | false 149 | 150 | 151 | 152 | 153 | vaadin-prereleases 154 | http://maven.vaadin.com/vaadin-prereleases 155 | 156 | 157 | vaadin-snapshots 158 | https://oss.sonatype.org/content/repositories/vaadin-snapshots/ 159 | 160 | false 161 | 162 | 163 | true 164 | 165 | 166 | 167 | 168 | 169 | vaadin-prereleases 170 | http://maven.vaadin.com/vaadin-prereleases 171 | 172 | 173 | vaadin-snapshots 174 | https://oss.sonatype.org/content/repositories/vaadin-snapshots/ 175 | 176 | false 177 | 178 | 179 | true 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | -------------------------------------------------------------------------------- /vaadin-combobox-multiselect-addon/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | org.vaadin.addons 6 | vaadin-combobox-multiselect 7 | jar 8 | 2.9-SNAPSHOT 9 | ComboBoxMultiselect Add-on 10 | 11 | 12 | 3 13 | 14 | 15 | 16 | UTF-8 17 | 1.8 18 | 1.8 19 | 8.7.0 20 | 8.7.0 21 | 22 | 23 | ${project.version} 24 | 25 | ${project.name} 26 | ${project.organization.name} 27 | Apache License 2.0 28 | ${project.artifactId}-${project.version}.jar 29 | 30 | 31 | 32 | Thorben von Hacht (Bonprix Handelsgesellschaft mbH) 33 | https://github.com/bonprix/vaadin-combobox-multiselect 34 | 35 | 36 | 37 | git://github.com/bonprix/vaadin-combobox-multiselect.git 38 | scm:git:git://github.com/bonprix/vaadin-combobox-multiselect.git 39 | scm:git:ssh://git@github.com:/bonprix/vaadin-combobox-multiselect.git 40 | HEAD 41 | 42 | 43 | 44 | GitHub 45 | https://github.com/bonprix/vaadin-combobox-multiselect/issues 46 | 47 | 48 | 49 | 50 | The MIT License (MIT) 51 | http://opensource.org/licenses/MIT 52 | 53 | 54 | 55 | 56 | 57 | vaadin-addons 58 | http://maven.vaadin.com/vaadin-addons 59 | 60 | 61 | 62 | 63 | 64 | com.vaadin 65 | vaadin-server 66 | ${vaadin.version} 67 | 68 | 69 | com.vaadin 70 | vaadin-client 71 | ${vaadin.version} 72 | provided 73 | 74 | 75 | 76 | org.apache.commons 77 | commons-lang3 78 | 3.6 79 | 80 | 81 | 82 | 83 | junit 84 | junit 85 | 4.8.1 86 | test 87 | 88 | 89 | 90 | 91 | 92 | 93 | org.apache.maven.plugins 94 | maven-jar-plugin 95 | 2.6 96 | 97 | 98 | true 99 | 100 | true 101 | true 102 | 103 | 104 | 105 | 1 106 | ${Vaadin-License-Title} 107 | org.vaadin.addons.WidgetSet 108 | 109 | 110 | 111 | 112 | 113 | 114 | org.apache.maven.plugins 115 | maven-javadoc-plugin 116 | 2.10.3 117 | 118 | 119 | attach-javadoc 120 | 121 | jar 122 | 123 | 124 | 125 | 126 | 127 | 128 | org.apache.maven.plugins 129 | maven-source-plugin 130 | 3.0.0 131 | 132 | 133 | attach-sources 134 | 135 | jar 136 | 137 | 138 | 139 | 140 | 141 | 142 | org.apache.maven.plugins 143 | maven-assembly-plugin 144 | 145 | false 146 | 147 | assembly/assembly.xml 148 | 149 | 150 | 151 | 152 | 153 | single 154 | 155 | install 156 | 157 | 158 | 159 | 160 | 161 | 162 | org.apache.maven.plugins 163 | maven-surefire-plugin 164 | 2.19.1 165 | 166 | 167 | 168 | 170 | 171 | 172 | src/main/java 173 | 174 | rebel.xml 175 | 176 | 177 | 178 | src/main/resources 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | vaadin-prerelease 188 | 189 | false 190 | 191 | 192 | 193 | 194 | vaadin-prereleases 195 | http://maven.vaadin.com/vaadin-prereleases 196 | 197 | 198 | vaadin-snapshots 199 | https://oss.sonatype.org/content/repositories/vaadin-snapshots/ 200 | 201 | false 202 | 203 | 204 | true 205 | 206 | 207 | 208 | 209 | 210 | vaadin-prereleases 211 | http://maven.vaadin.com/vaadin-prereleases 212 | 213 | 214 | vaadin-snapshots 215 | https://oss.sonatype.org/content/repositories/vaadin-snapshots/ 216 | 217 | false 218 | 219 | 220 | true 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | -------------------------------------------------------------------------------- /vaadin-combobox-multiselect-demo/src/main/java/org/vaadin/addons/demo/DemoUI.java: -------------------------------------------------------------------------------- 1 | package org.vaadin.addons.demo; 2 | 3 | import java.util.Arrays; 4 | import java.util.HashSet; 5 | import java.util.Iterator; 6 | import java.util.List; 7 | 8 | import javax.servlet.annotation.WebServlet; 9 | 10 | import org.vaadin.addons.ComboBoxMultiselect; 11 | import org.vaadin.addons.demo.util.DemoItem; 12 | 13 | import com.vaadin.annotations.Theme; 14 | import com.vaadin.annotations.Title; 15 | import com.vaadin.annotations.VaadinServletConfiguration; 16 | import com.vaadin.data.provider.ListDataProvider; 17 | import com.vaadin.server.UserError; 18 | import com.vaadin.server.VaadinRequest; 19 | import com.vaadin.server.VaadinServlet; 20 | import com.vaadin.ui.Button; 21 | import com.vaadin.ui.CssLayout; 22 | import com.vaadin.ui.HorizontalLayout; 23 | import com.vaadin.ui.Label; 24 | import com.vaadin.ui.UI; 25 | import com.vaadin.ui.VerticalLayout; 26 | import com.vaadin.ui.themes.ValoTheme; 27 | 28 | @Theme("demo") 29 | @Title("MyComponent Add-on Demo") 30 | @SuppressWarnings("serial") 31 | public class DemoUI extends UI { 32 | 33 | @WebServlet(value = "/*", asyncSupported = true) 34 | @VaadinServletConfiguration(productionMode = false, ui = DemoUI.class) 35 | public static class Servlet extends VaadinServlet { 36 | } 37 | 38 | @SuppressWarnings("unchecked") 39 | @Override 40 | protected void init(final VaadinRequest request) { 41 | // Show it in the middle of the screen 42 | final VerticalLayout layout = new VerticalLayout(); 43 | 44 | layout.setSpacing(false); 45 | 46 | final Label h1 = new Label("org.vaadin.addons.ComboBoxMultiselect"); 47 | h1.addStyleName(ValoTheme.LABEL_H1); 48 | layout.addComponent(h1); 49 | 50 | final HorizontalLayout row = new HorizontalLayout(); 51 | row.addStyleName(ValoTheme.LAYOUT_HORIZONTAL_WRAPPING); 52 | layout.addComponent(row); 53 | 54 | 55 | final List coboboxMultiselectList = Arrays.asList("Werner", "Paul", "Klaus", "Fred", "Jens", "Helge", "Arne", "Achim", "Peter", "Norbert", "Erni", "Bert"); 56 | final ComboBoxMultiselect comboTest = new ComboBoxMultiselect<>("Plain with 12 items"); 57 | comboTest.setPlaceholder("You can type here"); 58 | comboTest.showSelectAllButton(true); 59 | comboTest.showClearButton(true); 60 | comboTest.setItems(coboboxMultiselectList); 61 | row.addComponent(comboTest); 62 | 63 | 64 | ComboBoxMultiselect combo = new ComboBoxMultiselect<>("Normal"); 65 | combo.setPlaceholder("You can type here"); 66 | combo.setItemCaptionGenerator(DemoItem::getCaption); 67 | combo.setItemIconGenerator(DemoItem::getIcon); 68 | combo.showSelectAllButton(true); 69 | combo.showClearButton(true); 70 | combo.setItems(DemoItem.generate(46)); 71 | final Iterator iterator = ((ListDataProvider) combo.getDataProvider()).getItems() 72 | .iterator(); 73 | iterator.next(); 74 | combo.setValue(new HashSet<>(Arrays.asList(iterator.next(), iterator.next(), iterator.next()))); 75 | row.addComponent(combo); 76 | 77 | final CssLayout group = new CssLayout(); 78 | group.setCaption("Grouped with a Button"); 79 | group.addStyleName(ValoTheme.LAYOUT_COMPONENT_GROUP); 80 | row.addComponent(group); 81 | 82 | combo = new ComboBoxMultiselect<>(); 83 | combo.setPlaceholder("You can type here"); 84 | combo.setItems(DemoItem.generate(69)); 85 | combo.setValue(new HashSet<>(Arrays.asList(((ListDataProvider) combo.getDataProvider()).getItems() 86 | .iterator() 87 | .next()))); 88 | combo.setItemCaptionGenerator(DemoItem::getCaption); 89 | combo.setItemIconGenerator(DemoItem::getIcon); 90 | combo.setWidth("240px"); 91 | group.addComponent(combo); 92 | final Button today = new Button("Do It"); 93 | group.addComponent(today); 94 | 95 | combo = new ComboBoxMultiselect<>("Explicit size"); 96 | combo.setPlaceholder("You can type here"); 97 | combo.setItems(DemoItem.generate(200)); 98 | combo.setValue(new HashSet<>(Arrays.asList(((ListDataProvider) combo.getDataProvider()).getItems() 99 | .iterator() 100 | .next()))); 101 | combo.setItemCaptionGenerator(DemoItem::getCaption); 102 | combo.setWidth("260px"); 103 | combo.setHeight("60px"); 104 | row.addComponent(combo); 105 | 106 | combo = new ComboBoxMultiselect<>("No text input allowed"); 107 | combo.setPlaceholder("You can click here"); 108 | combo.setItems(DemoItem.generate(200)); 109 | combo.setValue(new HashSet<>(Arrays.asList(((ListDataProvider) combo.getDataProvider()).getItems() 110 | .iterator() 111 | .next()))); 112 | combo.setItemCaptionGenerator(DemoItem::getCaption); 113 | combo.setTextInputAllowed(false); 114 | row.addComponent(combo); 115 | 116 | combo = new ComboBoxMultiselect<>("Error"); 117 | combo.setPlaceholder("You can type here"); 118 | combo.setItems(DemoItem.generate(200)); 119 | combo.setValue(new HashSet<>(Arrays.asList(((ListDataProvider) combo.getDataProvider()).getItems() 120 | .iterator() 121 | .next()))); 122 | combo.setItemCaptionGenerator(DemoItem::getCaption); 123 | combo.setComponentError(new UserError("Fix it, now!")); 124 | row.addComponent(combo); 125 | 126 | combo = new ComboBoxMultiselect<>("Error, borderless"); 127 | combo.setPlaceholder("You can type here"); 128 | combo.setItems(DemoItem.generate(200)); 129 | combo.setValue(new HashSet<>(Arrays.asList(((ListDataProvider) combo.getDataProvider()).getItems() 130 | .iterator() 131 | .next()))); 132 | combo.setItemCaptionGenerator(DemoItem::getCaption); 133 | combo.setComponentError(new UserError("Fix it, now!")); 134 | combo.addStyleName(ValoTheme.COMBOBOX_BORDERLESS); 135 | row.addComponent(combo); 136 | 137 | combo = new ComboBoxMultiselect<>("Disabled"); 138 | combo.setPlaceholder("You can't type here"); 139 | combo.setItems(DemoItem.generate(200)); 140 | combo.setValue(new HashSet<>(Arrays.asList(((ListDataProvider) combo.getDataProvider()).getItems() 141 | .iterator() 142 | .next()))); 143 | combo.setItemCaptionGenerator(DemoItem::getCaption); 144 | combo.setEnabled(false); 145 | row.addComponent(combo); 146 | 147 | combo = new ComboBoxMultiselect<>("Custom color"); 148 | combo.setPlaceholder("You can type here"); 149 | combo.setItems(DemoItem.generate(200)); 150 | combo.setItemCaptionGenerator(DemoItem::getCaption); 151 | combo.setItemIconGenerator(DemoItem::getIcon); 152 | combo.addStyleName("color1"); 153 | row.addComponent(combo); 154 | 155 | combo = new ComboBoxMultiselect<>("Custom color"); 156 | combo.setPlaceholder("You can type here"); 157 | combo.setItems(DemoItem.generate(200)); 158 | combo.setItemCaptionGenerator(DemoItem::getCaption); 159 | combo.setItemIconGenerator(DemoItem::getIcon); 160 | combo.addStyleName("color2"); 161 | row.addComponent(combo); 162 | 163 | combo = new ComboBoxMultiselect<>("Custom color"); 164 | combo.setPlaceholder("You can type here"); 165 | combo.setItems(DemoItem.generate(200)); 166 | combo.setItemCaptionGenerator(DemoItem::getCaption); 167 | combo.setItemIconGenerator(DemoItem::getIcon); 168 | combo.addStyleName("color3"); 169 | row.addComponent(combo); 170 | 171 | combo = new ComboBoxMultiselect<>("Small"); 172 | combo.setPlaceholder("You can type here"); 173 | combo.setItems(DemoItem.generate(200)); 174 | combo.setItemCaptionGenerator(DemoItem::getCaption); 175 | combo.setItemIconGenerator(DemoItem::getIcon); 176 | combo.addStyleName(ValoTheme.COMBOBOX_SMALL); 177 | row.addComponent(combo); 178 | 179 | combo = new ComboBoxMultiselect<>("Large"); 180 | combo.setPlaceholder("You can type here"); 181 | combo.setItems(DemoItem.generate(200)); 182 | combo.setItemCaptionGenerator(DemoItem::getCaption); 183 | combo.setItemIconGenerator(DemoItem::getIcon); 184 | combo.addStyleName(ValoTheme.COMBOBOX_LARGE); 185 | row.addComponent(combo); 186 | 187 | combo = new ComboBoxMultiselect<>("Borderless"); 188 | combo.setPlaceholder("You can type here"); 189 | combo.setItems(DemoItem.generate(200)); 190 | combo.setValue(new HashSet<>(Arrays.asList(((ListDataProvider) combo.getDataProvider()).getItems() 191 | .iterator() 192 | .next()))); 193 | combo.setItemCaptionGenerator(DemoItem::getCaption); 194 | combo.addStyleName(ValoTheme.COMBOBOX_BORDERLESS); 195 | row.addComponent(combo); 196 | 197 | combo = new ComboBoxMultiselect<>("Tiny"); 198 | combo.setPlaceholder("You can type here"); 199 | combo.setItems(DemoItem.generate(200)); 200 | combo.setItemCaptionGenerator(DemoItem::getCaption); 201 | combo.setItemIconGenerator(DemoItem::getIcon); 202 | combo.addStyleName(ValoTheme.COMBOBOX_TINY); 203 | row.addComponent(combo); 204 | 205 | combo = new ComboBoxMultiselect<>("Huge"); 206 | combo.setPlaceholder("You can type here"); 207 | combo.setItems(DemoItem.generate(200)); 208 | combo.setItemCaptionGenerator(DemoItem::getCaption); 209 | combo.setItemIconGenerator(DemoItem::getIcon); 210 | combo.addStyleName(ValoTheme.COMBOBOX_HUGE); 211 | row.addComponent(combo); 212 | 213 | setContent(layout); 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | -------------------------------------------------------------------------------- /vaadin-combobox-multiselect-addon/src/main/java/org/vaadin/addons/client/ComboBoxMultiselectConnector.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2000-2016 Vaadin Ltd. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | package org.vaadin.addons.client; 17 | 18 | import java.util.Objects; 19 | import java.util.Set; 20 | import java.util.logging.Logger; 21 | 22 | import org.vaadin.addons.ComboBoxMultiselect; 23 | import org.vaadin.addons.client.VComboBoxMultiselect.ComboBoxMultiselectSuggestion; 24 | import org.vaadin.addons.client.VComboBoxMultiselect.DataReceivedHandler; 25 | 26 | import com.vaadin.client.Profiler; 27 | import com.vaadin.client.VConsole; 28 | import com.vaadin.client.annotations.OnStateChange; 29 | import com.vaadin.client.communication.StateChangeEvent; 30 | import com.vaadin.client.connectors.AbstractListingConnector; 31 | import com.vaadin.client.connectors.data.HasDataSource; 32 | import com.vaadin.client.data.AbstractRemoteDataSource; 33 | import com.vaadin.client.data.CacheStrategy; 34 | import com.vaadin.client.data.DataChangeHandler; 35 | import com.vaadin.client.data.DataSource; 36 | import com.vaadin.client.ui.HasErrorIndicator; 37 | import com.vaadin.client.ui.HasRequiredIndicator; 38 | import com.vaadin.client.ui.SimpleManagedLayout; 39 | import com.vaadin.client.widget.grid.datasources.ListDataSource; 40 | import com.vaadin.shared.EventId; 41 | import com.vaadin.shared.Registration; 42 | import com.vaadin.shared.communication.FieldRpc.FocusAndBlurServerRpc; 43 | import com.vaadin.shared.data.DataCommunicatorConstants; 44 | import com.vaadin.shared.ui.Connect; 45 | 46 | import elemental.json.JsonObject; 47 | 48 | @Connect(ComboBoxMultiselect.class) 49 | public class ComboBoxMultiselectConnector extends AbstractListingConnector 50 | implements HasRequiredIndicator, HasDataSource, SimpleManagedLayout, HasErrorIndicator { 51 | 52 | private static final long serialVersionUID = 1L; 53 | 54 | private static final String CHECKED = "s"; 55 | 56 | private final ComboBoxMultiselectServerRpc rpc = getRpcProxy(ComboBoxMultiselectServerRpc.class); 57 | 58 | private final FocusAndBlurServerRpc focusAndBlurRpc = getRpcProxy(FocusAndBlurServerRpc.class); 59 | 60 | private Registration dataChangeHandlerRegistration; 61 | 62 | @Override 63 | protected void init() { 64 | super.init(); 65 | getWidget().connector = this; 66 | } 67 | 68 | @Override 69 | public void onStateChanged(final StateChangeEvent stateChangeEvent) { 70 | super.onStateChanged(stateChangeEvent); 71 | 72 | Profiler.enter("ComboBoxMultiselectConnector.onStateChanged update content"); 73 | 74 | getWidget().readonly = isReadOnly(); 75 | getWidget().updateReadOnly(); 76 | 77 | // not a FocusWidget -> needs own tabindex handling 78 | getWidget().tb.setTabIndex(getState().tabIndex); 79 | 80 | getWidget().suggestionPopup.updateStyleNames(getState()); 81 | 82 | // make sure the input prompt is updated 83 | getWidget().updatePlaceholder(); 84 | 85 | getDataReceivedHandler().serverReplyHandled(); 86 | 87 | // all updates except options have been done 88 | getWidget().initDone = true; 89 | 90 | Profiler.leave("ComboBoxMultiselectConnector.onStateChanged update content"); 91 | } 92 | 93 | @OnStateChange({ "selectedItemKeys", "selectedItemsCaption" }) 94 | private void onSelectionChange() { 95 | getDataReceivedHandler().updateSelectionFromServer( getState().selectedItemKeys, 96 | getState().selectedItemsCaption); 97 | } 98 | 99 | @Override 100 | public VComboBoxMultiselect getWidget() { 101 | return (VComboBoxMultiselect) super.getWidget(); 102 | } 103 | 104 | private DataReceivedHandler getDataReceivedHandler() { 105 | return getWidget().getDataReceivedHandler(); 106 | } 107 | 108 | @Override 109 | public ComboBoxMultiselectState getState() { 110 | return (ComboBoxMultiselectState) super.getState(); 111 | } 112 | 113 | @Override 114 | public void layout() { 115 | final VComboBoxMultiselect widget = getWidget(); 116 | if (widget.initDone) { 117 | widget.updateRootWidth(); 118 | } 119 | } 120 | 121 | @Override 122 | public void setWidgetEnabled(final boolean widgetEnabled) { 123 | super.setWidgetEnabled(widgetEnabled); 124 | getWidget().enabled = widgetEnabled; 125 | getWidget().tb.setEnabled(widgetEnabled); 126 | } 127 | 128 | /* 129 | * These methods exist to move communications out of VComboBoxMultiselect, 130 | * and may be refactored/removed in the future 131 | */ 132 | 133 | /** 134 | * Send a message about a newly created item to the server. 135 | * 136 | * This method is for internal use only and may be removed in future 137 | * versions. 138 | * 139 | * @since 8.0 140 | * @param itemValue 141 | * user entered string value for the new item 142 | */ 143 | public void sendNewItem(final String itemValue) { 144 | this.rpc.createNewItem(itemValue); 145 | getDataReceivedHandler().clearPendingNavigation(); 146 | } 147 | 148 | /** 149 | * Send a message to the server set the current filter. 150 | * 151 | * This method is for internal use only and may be removed in future 152 | * versions. 153 | * 154 | * @since 8.0 155 | * @param filter 156 | * the current filter string 157 | */ 158 | protected void setFilter(final String filter) { 159 | if (!Objects.equals(filter, getWidget().lastFilter)) { 160 | getDataReceivedHandler().clearPendingNavigation(); 161 | 162 | this.rpc.setFilter(filter); 163 | } 164 | } 165 | 166 | /** 167 | * Send a message to the server to request a page of items with the current 168 | * filter. 169 | * 170 | * This method is for internal use only and may be removed in future 171 | * versions. 172 | * 173 | * @since 8.0 174 | * @param page 175 | * the page number to get or -1 to let the server/connector 176 | * decide based on current selection (possibly loading more data 177 | * from the server) 178 | * @param filter 179 | * the filter to apply, never {@code null} 180 | */ 181 | public void requestPage(int page, final String filter) { 182 | setFilter(filter); 183 | 184 | if (page < 0) { 185 | if (getState().scrollToSelectedItem) { 186 | // TODO this should be optimized not to try to fetch everything 187 | getDataSource().ensureAvailability(0, getDataSource().size()); 188 | return; 189 | } else { 190 | page = 0; 191 | } 192 | } 193 | 194 | final int startIndex = Math.max(0, page * getWidget().pageLength); 195 | 196 | getDataSource().ensureAvailability(startIndex, getWidget().pageLength); 197 | } 198 | 199 | /** 200 | * Send a message to the server updating the current selection. 201 | * 202 | * This method is for internal use only and may be removed in future 203 | * versions. 204 | * 205 | * @since 8.0 206 | * @param addedItemKeys 207 | * the item keys added to selection 208 | * @param removedItemKeys 209 | * the item keys removed from selection 210 | */ 211 | public void sendSelections(final Set addedItemKeys, final Set removedItemKeys) { 212 | this.rpc.updateSelection(addedItemKeys, removedItemKeys, false); 213 | getDataReceivedHandler().clearPendingNavigation(); 214 | } 215 | 216 | /** 217 | * Notify the server that the combo box received focus. 218 | * 219 | * For timing reasons, ConnectorFocusAndBlurHandler is not used at the 220 | * moment. 221 | * 222 | * This method is for internal use only and may be removed in future 223 | * versions. 224 | * 225 | * @since 8.0 226 | */ 227 | public void sendFocusEvent() { 228 | final boolean registeredListeners = hasEventListener(EventId.FOCUS); 229 | if (registeredListeners) { 230 | this.focusAndBlurRpc.focus(); 231 | getDataReceivedHandler().clearPendingNavigation(); 232 | } 233 | } 234 | 235 | /** 236 | * Notify the server that the combo box lost focus. 237 | * 238 | * For timing reasons, ConnectorFocusAndBlurHandler is not used at the 239 | * moment. 240 | * 241 | * This method is for internal use only and may be removed in future 242 | * versions. 243 | * 244 | * @since 8.0 245 | */ 246 | public void sendBlurEvent() { 247 | final boolean registeredListeners = hasEventListener(EventId.BLUR); 248 | if (registeredListeners) { 249 | this.focusAndBlurRpc.blur(); 250 | getDataReceivedHandler().clearPendingNavigation(); 251 | } 252 | 253 | getDataReceivedHandler().setBlurUpdate(true); 254 | this.rpc.blur(); 255 | } 256 | 257 | @Override 258 | public void setDataSource(final DataSource dataSource) { 259 | super.setDataSource(dataSource); 260 | this.dataChangeHandlerRegistration = dataSource.addDataChangeHandler(new PagedDataChangeHandler(dataSource)); 261 | } 262 | 263 | @Override 264 | public void onUnregister() { 265 | super.onUnregister(); 266 | this.dataChangeHandlerRegistration.remove(); 267 | } 268 | 269 | @Override 270 | public boolean isRequiredIndicatorVisible() { 271 | return getState().required && !isReadOnly(); 272 | } 273 | 274 | private void refreshData() { 275 | updateCurrentPage(); 276 | 277 | final int start = getWidget().currentPage * getWidget().pageLength; 278 | int end = (getDataSource().size() - start) >= getWidget().pageLength ? getWidget().pageLength : getDataSource().size() - start; 279 | getWidget().currentSuggestions.clear(); 280 | 281 | if (start > 0) { 282 | end = end + start; 283 | } 284 | 285 | updateSuggestions(start, end); 286 | getWidget().setTotalSuggestions(getDataSource().size()); 287 | 288 | getDataReceivedHandler().dataReceived(); 289 | } 290 | 291 | private void updateSuggestions(final int start, final int end) { 292 | for (int i = start; i < end; ++i) { 293 | JsonObject row = getDataSource().getRow(i); 294 | if (row == null) { 295 | getDataSource().ensureAvailability(start, end); 296 | row = getDataSource().getRow(i); 297 | } 298 | 299 | if (row != null) { 300 | final String key = getRowKey(row); 301 | 302 | final String caption = row.getString(DataCommunicatorConstants.NAME); 303 | final String style = row.getString(ComboBoxMultiselectConstants.STYLE); 304 | final String untranslatedIconUri = row.getString(ComboBoxMultiselectConstants.ICON); 305 | final boolean checked = row.getString(CHECKED) != null ? Boolean.TRUE : Boolean.FALSE; 306 | 307 | final ComboBoxMultiselectSuggestion suggestion = getWidget().new ComboBoxMultiselectSuggestion(key, caption, 308 | style, untranslatedIconUri); 309 | suggestion.setChecked(checked); 310 | if (checked) { 311 | getWidget().selectedOptionKeys.add(key); 312 | } 313 | 314 | getWidget().currentSuggestions.add(suggestion); 315 | } else { 316 | // there is not enough options to fill the page 317 | return; 318 | } 319 | } 320 | } 321 | 322 | private boolean isFirstPage() { 323 | return getWidget().currentPage == 0; 324 | } 325 | 326 | private void updateCurrentPage() { 327 | // try to find selected item if requested 328 | if (getState().scrollToSelectedItem && getState().pageLength > 0 && getWidget().currentPage < 0 329 | && getWidget().selectedOptionKeys != null) { 330 | // search for the item with the selected key 331 | getWidget().currentPage = 0; 332 | for (int i = 0; i < getDataSource().size(); ++i) { 333 | final JsonObject row = getDataSource().getRow(i); 334 | if (row != null) { 335 | final String key = AbstractListingConnector.getRowKey(row); 336 | if (getWidget().selectedOptionKeys.contains(key)) { 337 | getWidget().currentPage = i / getState().pageLength; 338 | break; 339 | } 340 | } 341 | } 342 | } else if (getWidget().currentPage < 0) { 343 | getWidget().currentPage = 0; 344 | } 345 | } 346 | 347 | private static final Logger LOGGER = Logger.getLogger(ComboBoxMultiselectConnector.class.getName()); 348 | 349 | private class PagedDataChangeHandler implements DataChangeHandler { 350 | 351 | private final DataSource dataSource; 352 | 353 | public PagedDataChangeHandler(final DataSource dataSource) { 354 | this.dataSource = dataSource; 355 | } 356 | 357 | @Override 358 | public void dataUpdated(final int firstRowIndex, final int numberOfRows) { 359 | // NOOP since dataAvailable is always triggered afterwards 360 | } 361 | 362 | @Override 363 | public void dataRemoved(final int firstRowIndex, final int numberOfRows) { 364 | // NOOP since dataAvailable is always triggered afterwards 365 | } 366 | 367 | @Override 368 | public void dataAdded(final int firstRowIndex, final int numberOfRows) { 369 | // NOOP since dataAvailable is always triggered afterwards 370 | } 371 | 372 | @Override 373 | public void dataAvailable(final int firstRowIndex, final int numberOfRows) { 374 | refreshData(); 375 | } 376 | 377 | @Override 378 | public void resetDataAndSize(final int estimatedNewDataSize) { 379 | if (getState().pageLength == 0) { 380 | if (getWidget().suggestionPopup.isShowing()) { 381 | this.dataSource.ensureAvailability(0, estimatedNewDataSize); 382 | } 383 | // else lets just wait till the popup is opened before 384 | // everything is fetched to it. this could be optimized later on 385 | // to fetch everything if in-memory data is used. 386 | } else { 387 | this.dataSource.ensureAvailability(getState().pageLength * getWidget().currentPage, getState().pageLength); 388 | } 389 | } 390 | 391 | } 392 | 393 | public void selectAll(final String filter) { 394 | this.rpc.selectAll(filter); 395 | } 396 | 397 | public void clear(final String filter) { 398 | this.rpc.clear(filter); 399 | } 400 | } 401 | -------------------------------------------------------------------------------- /vaadin-combobox-multiselect-addon/src/main/java/org/vaadin/addons/ComboBoxMultiselect.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2000-2016 Vaadin Ltd. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package org.vaadin.addons; 18 | 19 | import java.io.Serializable; 20 | import java.lang.reflect.Field; 21 | import java.util.ArrayList; 22 | import java.util.Arrays; 23 | import java.util.Collection; 24 | import java.util.Collections; 25 | import java.util.HashMap; 26 | import java.util.HashSet; 27 | import java.util.LinkedHashSet; 28 | import java.util.List; 29 | import java.util.Map; 30 | import java.util.Objects; 31 | import java.util.Optional; 32 | import java.util.Set; 33 | import java.util.stream.Collectors; 34 | import java.util.stream.Stream; 35 | 36 | import org.apache.commons.lang3.StringUtils; 37 | import org.jsoup.nodes.Element; 38 | import org.vaadin.addons.client.ComboBoxMultiselectConstants; 39 | import org.vaadin.addons.client.ComboBoxMultiselectServerRpc; 40 | import org.vaadin.addons.client.ComboBoxMultiselectState; 41 | 42 | import com.vaadin.data.HasFilterableDataProvider; 43 | import com.vaadin.data.HasValue; 44 | import com.vaadin.data.provider.CallbackDataProvider; 45 | import com.vaadin.data.provider.DataProvider; 46 | import com.vaadin.data.provider.ListDataProvider; 47 | import com.vaadin.data.provider.Query; 48 | import com.vaadin.event.FieldEvents; 49 | import com.vaadin.event.FieldEvents.BlurEvent; 50 | import com.vaadin.event.FieldEvents.BlurListener; 51 | import com.vaadin.event.FieldEvents.FocusAndBlurServerRpcDecorator; 52 | import com.vaadin.event.FieldEvents.FocusEvent; 53 | import com.vaadin.event.FieldEvents.FocusListener; 54 | import com.vaadin.event.selection.MultiSelectionEvent; 55 | import com.vaadin.server.Resource; 56 | import com.vaadin.server.ResourceReference; 57 | import com.vaadin.server.SerializableBiPredicate; 58 | import com.vaadin.server.SerializableConsumer; 59 | import com.vaadin.server.SerializableFunction; 60 | import com.vaadin.server.SerializableToIntFunction; 61 | import com.vaadin.shared.Registration; 62 | import com.vaadin.shared.data.DataCommunicatorConstants; 63 | import com.vaadin.ui.AbstractMultiSelect; 64 | import com.vaadin.ui.IconGenerator; 65 | import com.vaadin.ui.NativeSelect; 66 | import com.vaadin.ui.StyleGenerator; 67 | import com.vaadin.ui.declarative.DesignAttributeHandler; 68 | import com.vaadin.ui.declarative.DesignContext; 69 | import com.vaadin.ui.declarative.DesignFormatter; 70 | 71 | import elemental.json.JsonObject; 72 | 73 | /** 74 | * A filtering dropdown single-select. Items are filtered based on user input. Supports the creation of new items when a handler is set by the user. 75 | * 76 | * @param item (bean) type in ComboBoxMultiselect 77 | * @author Vaadin Ltd 78 | */ 79 | @SuppressWarnings("serial") 80 | public class ComboBoxMultiselect extends AbstractMultiSelect 81 | implements FieldEvents.BlurNotifier, FieldEvents.FocusNotifier, HasFilterableDataProvider { 82 | 83 | public static final Integer DEFAULT_PAGE_LENGTH = 10; 84 | 85 | /** 86 | * A callback method for fetching items. The callback is provided with a non-null string filter, offset index and limit. 87 | * 88 | * @param item (bean) type in ComboBoxMultiselect 89 | * @since 8.0 90 | */ 91 | @FunctionalInterface 92 | public interface FetchItemsCallback extends Serializable { 93 | 94 | /** 95 | * Returns a stream of items that match the given filter, limiting the results with given offset and limit. 96 | *

97 | * This method is called after the size of the data set is asked from a related size callback. The offset and limit are promised to be within the size 98 | * of the data set. 99 | * 100 | * @param filter a non-null filter string 101 | * @param offset the first index to fetch 102 | * @param limit the fetched item count 103 | * @return stream of items 104 | */ 105 | public Stream fetchItems(String filter, int offset, int limit); 106 | } 107 | 108 | /** 109 | * Handler that adds a new item based on user input when the new items allowed mode is active. 110 | * 111 | * @since 8.0 112 | */ 113 | @FunctionalInterface 114 | public interface NewItemHandler extends SerializableConsumer { 115 | } 116 | 117 | /** 118 | * Generator that handles the value of the textfield when not selected. 119 | * 120 | * @since 8.0 121 | */ 122 | @FunctionalInterface 123 | public interface InputTextFieldCaptionGenerator extends SerializableFunction, String> { 124 | } 125 | 126 | /** 127 | * Item style generator class for declarative support. 128 | *

129 | * Provides a straightforward mapping between an item and its style. 130 | * 131 | * @param item type 132 | * @since 8.0 133 | */ 134 | protected static class DeclarativeStyleGenerator implements StyleGenerator { 135 | 136 | private final StyleGenerator fallback; 137 | private final Map styles = new HashMap<>(); 138 | 139 | public DeclarativeStyleGenerator(final StyleGenerator fallback) { 140 | this.fallback = fallback; 141 | } 142 | 143 | @Override 144 | public String apply(final T item) { 145 | return this.styles.containsKey(item) ? this.styles.get(item) : this.fallback.apply(item); 146 | } 147 | 148 | /** 149 | * Sets a {@code style} for the {@code item}. 150 | * 151 | * @param item a data item 152 | * @param style a style for the {@code item} 153 | */ 154 | protected void setStyle(final T item, final String style) { 155 | this.styles.put(item, style); 156 | } 157 | } 158 | 159 | private final ComboBoxMultiselectServerRpc rpc = new ComboBoxMultiselectServerRpc() { 160 | @Override 161 | public void createNewItem(final String itemValue) { 162 | // New option entered 163 | if (getNewItemHandler() != null && itemValue != null && itemValue.length() > 0) { 164 | getNewItemHandler().accept(itemValue); 165 | } 166 | } 167 | 168 | @Override 169 | public void setFilter(final String filterText) { 170 | ComboBoxMultiselect.this.currentFilterText = filterText; 171 | ComboBoxMultiselect.this.filterSlot.accept(filterText); 172 | } 173 | 174 | @Override 175 | public void updateSelection(final Set selectedItemKeys, final Set deselectedItemKeys, final boolean sortingNeeded) { 176 | ComboBoxMultiselect.this.updateSelection(getItemsForSelectionChange(selectedItemKeys), getItemsForSelectionChange(deselectedItemKeys), true, 177 | sortingNeeded); 178 | } 179 | 180 | private Set getItemsForSelectionChange(final Set keys) { 181 | return keys.stream() 182 | .map(key -> getItemForSelectionChange(key)) 183 | .filter(Optional::isPresent) 184 | .map(Optional::get) 185 | .collect(Collectors.toSet()); 186 | } 187 | 188 | private Optional getItemForSelectionChange(final String key) { 189 | final T item = getDataCommunicator().getKeyMapper() 190 | .get(key); 191 | if (item == null || !getItemEnabledProvider().test(item)) { 192 | return Optional.empty(); 193 | } 194 | 195 | return Optional.of(item); 196 | } 197 | 198 | @Override 199 | public void blur() { 200 | ComboBoxMultiselect.this.sortingSelection = Collections.unmodifiableCollection(getSelectedItems()); 201 | setFilter(""); 202 | getDataProvider().refreshAll(); 203 | } 204 | 205 | @Override 206 | public void selectAll(final String filter) { 207 | final ListDataProvider listDataProvider = ((ListDataProvider) getDataProvider()); 208 | final Set addedItems = listDataProvider.getItems() 209 | .stream() 210 | .filter(t -> { 211 | final String caption = getItemCaptionGenerator().apply(t); 212 | if (t == null) { 213 | return false; 214 | } 215 | return caption.toLowerCase() 216 | .contains(filter.toLowerCase()); 217 | }) 218 | .map(t -> itemToKey(t)) 219 | .collect(Collectors.toSet()); 220 | updateSelection(addedItems, new HashSet<>(), true); 221 | } 222 | 223 | @Override 224 | public void clear(final String filter) { 225 | final ListDataProvider listDataProvider = ((ListDataProvider) getDataProvider()); 226 | final Set removedItems = listDataProvider.getItems() 227 | .stream() 228 | .filter(t -> { 229 | final String caption = getItemCaptionGenerator().apply(t); 230 | if (t == null) { 231 | return false; 232 | } 233 | return caption.toLowerCase() 234 | .contains(filter.toLowerCase()); 235 | }) 236 | .map(t -> itemToKey(t)) 237 | .collect(Collectors.toSet()); 238 | updateSelection(new HashSet<>(), removedItems, true); 239 | }; 240 | }; 241 | 242 | /** 243 | * Handler for new items entered by the user. 244 | */ 245 | private NewItemHandler newItemHandler; 246 | 247 | private StyleGenerator itemStyleGenerator = item -> null; 248 | 249 | private String currentFilterText; 250 | 251 | private SerializableConsumer filterSlot = filter -> { 252 | // Just ignore when neither setDataProvider nor setItems has been called 253 | }; 254 | 255 | private final InputTextFieldCaptionGenerator inputTextFieldCaptionGenerator = items -> { 256 | if (items.isEmpty()) { 257 | return ""; 258 | } 259 | 260 | final List captions = new ArrayList<>(); 261 | 262 | if (getState().selectedItemKeys != null) { 263 | for (final T item : items) { 264 | if (item != null) { 265 | captions.add(getItemCaptionGenerator().apply(item)); 266 | } 267 | } 268 | } 269 | 270 | return "(" + captions.size() + ") " + StringUtils.join(captions, "; "); 271 | }; 272 | 273 | private Collection sortingSelection = Collections.unmodifiableCollection(new ArrayList<>()); 274 | 275 | /** 276 | * Constructs an empty combo box without a caption. The content of the combo box can be set with {@link #setDataProvider(DataProvider)} or 277 | * {@link #setItems(Collection)} 278 | */ 279 | public ComboBoxMultiselect() { 280 | super(); 281 | 282 | init(); 283 | } 284 | 285 | /** 286 | * Constructs an empty combo box, whose content can be set with {@link #setDataProvider(DataProvider)} or {@link #setItems(Collection)}. 287 | * 288 | * @param caption the caption to show in the containing layout, null for no caption 289 | */ 290 | public ComboBoxMultiselect(final String caption) { 291 | this(); 292 | setCaption(caption); 293 | } 294 | 295 | /** 296 | * Constructs a combo box with a static in-memory data provider with the given options. 297 | * 298 | * @param caption the caption to show in the containing layout, null for no caption 299 | * @param options collection of options, not null 300 | */ 301 | public ComboBoxMultiselect(final String caption, final Collection options) { 302 | this(caption); 303 | 304 | setItems(options); 305 | } 306 | 307 | /** 308 | * Initialize the ComboBoxMultiselect with default settings and register client to server RPC implementation. 309 | */ 310 | private void init() { 311 | registerRpc(this.rpc); 312 | registerRpc(new FocusAndBlurServerRpcDecorator(this, this::fireEvent)); 313 | 314 | addDataGenerator((final T data, final JsonObject jsonObject) -> { 315 | String caption = getItemCaptionGenerator().apply(data); 316 | if (caption == null) { 317 | caption = ""; 318 | } 319 | jsonObject.put(DataCommunicatorConstants.NAME, caption); 320 | final String style = this.itemStyleGenerator.apply(data); 321 | if (style != null) { 322 | jsonObject.put(ComboBoxMultiselectConstants.STYLE, style); 323 | } 324 | final Resource icon = getItemIconGenerator().apply(data); 325 | if (icon != null) { 326 | final String iconUrl = ResourceReference.create(icon, ComboBoxMultiselect.this, null) 327 | .getURL(); 328 | jsonObject.put(ComboBoxMultiselectConstants.ICON, iconUrl); 329 | } 330 | }); 331 | } 332 | 333 | /** 334 | * {@inheritDoc} 335 | *

336 | * Filtering will use a case insensitive match to show all items where the filter text is a substring of the caption displayed for that item. 337 | */ 338 | @Override 339 | public void setItems(final Collection items) { 340 | final ListDataProvider listDataProvider = DataProvider.ofCollection(items); 341 | 342 | setDataProvider(listDataProvider); 343 | 344 | // sets the PageLength to 10. 345 | // if there are less then 10 items in the combobox, PageLength will get the amount of items. 346 | setPageLength(getDataProvider().size(new Query<>()) >= ComboBoxMultiselect.DEFAULT_PAGE_LENGTH ? ComboBoxMultiselect.DEFAULT_PAGE_LENGTH : getDataProvider().size(new Query<>())); 347 | } 348 | 349 | /** 350 | * {@inheritDoc} 351 | *

352 | * Filtering will use a case insensitive match to show all items where the filter text is a substring of the caption displayed for that item. 353 | */ 354 | @Override 355 | public void setItems(final Stream streamOfItems) { 356 | // Overridden only to add clarification to javadocs 357 | super.setItems(streamOfItems); 358 | } 359 | 360 | /** 361 | * {@inheritDoc} 362 | *

363 | * Filtering will use a case insensitive match to show all items where the filter text is a substring of the caption displayed for that item. 364 | */ 365 | @Override 366 | public void setItems(@SuppressWarnings("unchecked") final T... items) { 367 | // Overridden only to add clarification to javadocs 368 | super.setItems(items); 369 | } 370 | 371 | /** 372 | * Sets a list data provider as the data provider of this combo box. Filtering will use a case insensitive match to show all items where the filter text is 373 | * a substring of the caption displayed for that item. 374 | *

375 | * Note that this is a shorthand that calls {@link #setDataProvider(DataProvider)} with a wrapper of the provided list data provider. This means that 376 | * {@link #getDataProvider()} will return the wrapper instead of the original list data provider. 377 | * 378 | * @param listDataProvider the list data provider to use, not null 379 | * @since 8.0 380 | */ 381 | public void setDataProvider(final ListDataProvider listDataProvider) { 382 | // Cannot use the case insensitive contains shorthand from 383 | // ListDataProvider since it wouldn't react to locale changes 384 | final CaptionFilter defaultCaptionFilter = (itemText, filterText) -> itemText.toLowerCase(getLocale()) 385 | .contains(filterText.toLowerCase(getLocale())); 386 | 387 | setDataProvider(defaultCaptionFilter, listDataProvider); 388 | } 389 | 390 | /** 391 | * Sets the data items of this listing and a simple string filter with which the item string and the text the user has input are compared. 392 | *

393 | * Note that unlike {@link #setItems(Collection)}, no automatic case conversion is performed before the comparison. 394 | * 395 | * @param captionFilter filter to check if an item is shown when user typed some text into the ComboBoxMultiselect 396 | * @param items the data items to display 397 | * @since 8.0 398 | */ 399 | public void setItems(final CaptionFilter captionFilter, final Collection items) { 400 | final ListDataProvider listDataProvider = DataProvider.ofCollection(items); 401 | 402 | setDataProvider(captionFilter, listDataProvider); 403 | } 404 | 405 | /** 406 | * Sets a list data provider with an item caption filter as the data provider of this combo box. The caption filter is used to compare the displayed caption 407 | * of each item to the filter text entered by the user. 408 | * 409 | * @param captionFilter filter to check if an item is shown when user typed some text into the ComboBoxMultiselect 410 | * @param listDataProvider the list data provider to use, not null 411 | * @since 8.0 412 | */ 413 | public void setDataProvider(final CaptionFilter captionFilter, final ListDataProvider listDataProvider) { 414 | Objects.requireNonNull(listDataProvider, "List data provider cannot be null"); 415 | 416 | // Must do getItemCaptionGenerator() for each operation since it might 417 | // not be the same as when this method was invoked 418 | setDataProvider(listDataProvider, filterText -> item -> captionFilter.test(getItemCaptionGenerator().apply(item), filterText)); 419 | } 420 | 421 | /** 422 | * Sets the data items of this listing and a simple string filter with which the item string and the text the user has input are compared. 423 | *

424 | * Note that unlike {@link #setItems(Collection)}, no automatic case conversion is performed before the comparison. 425 | * 426 | * @param captionFilter filter to check if an item is shown when user typed some text into the ComboBoxMultiselect 427 | * @param items the data items to display 428 | * @since 8.0 429 | */ 430 | public void setItems(final CaptionFilter captionFilter, @SuppressWarnings("unchecked") final T... items) { 431 | setItems(captionFilter, Arrays.asList(items)); 432 | } 433 | 434 | /** 435 | * Gets the current placeholder text shown when the combo box would be empty. 436 | * 437 | * @see #setPlaceholder(String) 438 | * @return the current placeholder string, or null if not enabled 439 | * @since 8.0 440 | */ 441 | public String getPlaceholder() { 442 | return getState(false).placeholder; 443 | } 444 | 445 | /** 446 | * Sets the placeholder string - a textual prompt that is displayed when the select would otherwise be empty, to prompt the user for input. 447 | * 448 | * @param placeholder the desired placeholder, or null to disable 449 | * @since 8.0 450 | */ 451 | public void setPlaceholder(final String placeholder) { 452 | getState().placeholder = placeholder; 453 | } 454 | 455 | /** 456 | * Sets whether it is possible to input text into the field or whether the field area of the component is just used to show what is selected. By disabling 457 | * text input, the comboBox will work in the same way as a {@link NativeSelect} 458 | * 459 | * @see #isTextInputAllowed() 460 | * 461 | * @param textInputAllowed true to allow entering text, false to just show the current selection 462 | */ 463 | public void setTextInputAllowed(final boolean textInputAllowed) { 464 | getState().textInputAllowed = textInputAllowed; 465 | } 466 | 467 | /** 468 | * Returns true if the user can enter text into the field to either filter the selections or enter a new value if new item handler is set (see 469 | * {@link #setNewItemHandler(NewItemHandler)}. If text input is disabled, the comboBox will work in the same way as a {@link NativeSelect} 470 | * 471 | * @return true if text input is allowed 472 | */ 473 | public boolean isTextInputAllowed() { 474 | return getState(false).textInputAllowed; 475 | } 476 | 477 | @Override 478 | public Registration addBlurListener(final BlurListener listener) { 479 | return addListener(BlurEvent.EVENT_ID, BlurEvent.class, listener, BlurListener.blurMethod); 480 | } 481 | 482 | @Override 483 | public Registration addFocusListener(final FocusListener listener) { 484 | return addListener(FocusEvent.EVENT_ID, FocusEvent.class, listener, FocusListener.focusMethod); 485 | } 486 | 487 | /** 488 | * Returns the page length of the suggestion popup. 489 | * 490 | * @return the pageLength 491 | */ 492 | public int getPageLength() { 493 | return getState(false).pageLength; 494 | } 495 | 496 | /** 497 | * Returns the suggestion pop-up's width as a CSS string. By default this width is set to "100%". 498 | * 499 | * @see #setPopupWidth 500 | * @since 7.7 501 | * @return explicitly set popup width as CSS size string or null if not set 502 | */ 503 | public String getPopupWidth() { 504 | return getState(false).suggestionPopupWidth; 505 | } 506 | 507 | /** 508 | * Sets the page length for the suggestion popup. Setting the page length to 0 will disable suggestion popup paging (all items visible). 509 | * 510 | * @param pageLength the pageLength to set 511 | */ 512 | public void setPageLength(final int pageLength) { 513 | getState().pageLength = pageLength; 514 | } 515 | 516 | /** 517 | * 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 518 | * ComboBoxMultiselect itself. 519 | *

520 | * By default this width is set to "100%" so that the pop-up's width is equal to the width of the combobox. By setting width to null the pop-up's width will 521 | * automatically expand beyond 100% relative width to fit the content of all displayed items. 522 | * 523 | * @see #getPopupWidth() 524 | * @since 7.7 525 | * @param width the width 526 | */ 527 | public void setPopupWidth(final String width) { 528 | getState().suggestionPopupWidth = width; 529 | } 530 | 531 | /** 532 | * Sets whether to scroll the selected item visible (directly open the page on which it is) when opening the combo box popup or not. 533 | *

534 | * This requires finding the index of the item, which can be expensive in many large lazy loading containers. 535 | * 536 | * @param scrollToSelectedItem true to find the page with the selected item when opening the selection popup 537 | */ 538 | public void setScrollToSelectedItem(final boolean scrollToSelectedItem) { 539 | getState().scrollToSelectedItem = scrollToSelectedItem; 540 | } 541 | 542 | /** 543 | * Returns true if the select should find the page with the selected item when opening the popup. 544 | * 545 | * @see #setScrollToSelectedItem(boolean) 546 | * 547 | * @return true if the page with the selected item will be shown when opening the popup 548 | */ 549 | public boolean isScrollToSelectedItem() { 550 | return getState(false).scrollToSelectedItem; 551 | } 552 | 553 | /** 554 | * Sets the style generator that is used to produce custom class names for items visible in the popup. The CSS class name that will be added to the item is 555 | * v-filterselect-item-[style name]. Returning null from the generator results in no custom style name being set. 556 | * 557 | * @see StyleGenerator 558 | * 559 | * @param itemStyleGenerator the item style generator to set, not null 560 | * @throws NullPointerException if {@code itemStyleGenerator} is {@code null} 561 | * @since 8.0 562 | */ 563 | public void setStyleGenerator(final StyleGenerator itemStyleGenerator) { 564 | Objects.requireNonNull(itemStyleGenerator, "Item style generator must not be null"); 565 | this.itemStyleGenerator = itemStyleGenerator; 566 | getDataCommunicator().reset(); 567 | } 568 | 569 | /** 570 | * Gets the currently used style generator that is used to generate CSS class names for items. The default item style provider returns null for all items, 571 | * resulting in no custom item class names being set. 572 | * 573 | * @see StyleGenerator 574 | * @see #setStyleGenerator(StyleGenerator) 575 | * 576 | * @return the currently used item style generator, not null 577 | * @since 8.0 578 | */ 579 | public StyleGenerator getStyleGenerator() { 580 | return this.itemStyleGenerator; 581 | } 582 | 583 | @Override 584 | public void setItemIconGenerator(final IconGenerator itemIconGenerator) { 585 | super.setItemIconGenerator(itemIconGenerator); 586 | } 587 | 588 | /** 589 | * Sets the handler that is called when user types a new item. The creation of new items is allowed when a new item handler has been set. 590 | * 591 | * @param newItemHandler handler called for new items, null to only permit the selection of existing items 592 | * @since 8.0 593 | */ 594 | public void setNewItemHandler(final NewItemHandler newItemHandler) { 595 | this.newItemHandler = newItemHandler; 596 | getState().allowNewItems = newItemHandler != null; 597 | markAsDirty(); 598 | } 599 | 600 | /** 601 | * Returns the handler called when the user enters a new item (not present in the data provider). 602 | * 603 | * @return new item handler or null if none specified 604 | */ 605 | public NewItemHandler getNewItemHandler() { 606 | return this.newItemHandler; 607 | } 608 | 609 | // HasValue methods delegated to the selection model 610 | 611 | @Override 612 | public Registration addValueChangeListener(final HasValue.ValueChangeListener> listener) { 613 | return addSelectionListener(event -> listener 614 | .valueChange(new ValueChangeEvent<>(event.getComponent(), this, event.getOldValue(), event.isUserOriginated()))); 615 | } 616 | 617 | @Override 618 | protected ComboBoxMultiselectState getState() { 619 | return (ComboBoxMultiselectState) super.getState(); 620 | } 621 | 622 | @Override 623 | protected ComboBoxMultiselectState getState(final boolean markAsDirty) { 624 | return (ComboBoxMultiselectState) super.getState(markAsDirty); 625 | } 626 | 627 | @Override 628 | protected Element writeItem(final Element design, final T item, final DesignContext context) { 629 | final Element element = design.appendElement("option"); 630 | 631 | final String caption = getItemCaptionGenerator().apply(item); 632 | if (caption != null) { 633 | element.html(DesignFormatter.encodeForTextNode(caption)); 634 | } 635 | else { 636 | element.html(DesignFormatter.encodeForTextNode(item.toString())); 637 | } 638 | element.attr("item", item.toString()); 639 | 640 | final Resource icon = getItemIconGenerator().apply(item); 641 | if (icon != null) { 642 | DesignAttributeHandler.writeAttribute("icon", element.attributes(), icon, null, Resource.class, context); 643 | } 644 | 645 | final String style = getStyleGenerator().apply(item); 646 | if (style != null) { 647 | element.attr("style", style); 648 | } 649 | 650 | if (isSelected(item)) { 651 | element.attr("selected", ""); 652 | } 653 | 654 | return element; 655 | } 656 | 657 | @Override 658 | protected void readItems(final Element design, final DesignContext context) { 659 | setStyleGenerator(new DeclarativeStyleGenerator<>(getStyleGenerator())); 660 | super.readItems(design, context); 661 | } 662 | 663 | @SuppressWarnings({ "unchecked", "rawtypes" }) 664 | @Override 665 | protected T readItem(final Element child, final Set selected, final DesignContext context) { 666 | final T item = super.readItem(child, selected, context); 667 | 668 | if (child.hasAttr("style")) { 669 | final StyleGenerator styleGenerator = getStyleGenerator(); 670 | if (styleGenerator instanceof DeclarativeStyleGenerator) { 671 | ((DeclarativeStyleGenerator) styleGenerator).setStyle(item, child.attr("style")); 672 | } 673 | else { 674 | throw new IllegalStateException(String.format("Don't know how " + "to set style using current style generator '%s'", styleGenerator.getClass() 675 | .getName())); 676 | } 677 | } 678 | return item; 679 | } 680 | 681 | @Override 682 | public DataProvider getDataProvider() { 683 | return internalGetDataProvider(); 684 | } 685 | 686 | @Override 687 | public void setDataProvider(final DataProvider dataProvider, final SerializableFunction filterConverter) { 688 | Objects.requireNonNull(dataProvider, "dataProvider cannot be null"); 689 | Objects.requireNonNull(filterConverter, "filterConverter cannot be null"); 690 | 691 | final SerializableFunction convertOrNull = filterText -> { 692 | if (filterText == null || filterText.isEmpty()) { 693 | return null; 694 | } 695 | 696 | return filterConverter.apply(filterText); 697 | }; 698 | 699 | final SerializableConsumer providerFilterSlot = internalSetDataProvider(dataProvider, convertOrNull.apply(this.currentFilterText)); 700 | 701 | this.filterSlot = filter -> providerFilterSlot.accept(convertOrNull.apply(filter)); 702 | } 703 | 704 | @Override 705 | protected SerializableConsumer internalSetDataProvider(final DataProvider dataProvider, final F initialFilter) { 706 | final SerializableConsumer consumer = super.internalSetDataProvider(dataProvider, initialFilter); 707 | 708 | if (getDataProvider() instanceof ListDataProvider) { 709 | final ListDataProvider listDataProvider = ((ListDataProvider) getDataProvider()); 710 | listDataProvider.setSortComparator((o1, o2) -> { 711 | final boolean selected1 = this.sortingSelection.contains(o1); 712 | final boolean selected2 = this.sortingSelection.contains(o2); 713 | 714 | if (selected1 && !selected2) { 715 | return -1; 716 | } 717 | if (!selected1 && selected2) { 718 | return 1; 719 | } 720 | 721 | return getItemCaptionGenerator().apply(o1) 722 | .compareToIgnoreCase(getItemCaptionGenerator().apply(o2)); 723 | }); 724 | } 725 | 726 | return consumer; 727 | } 728 | 729 | /** 730 | * Sets a CallbackDataProvider using the given fetch items callback and a size callback. 731 | *

732 | * This method is a shorthand for making a {@link CallbackDataProvider} that handles a partial {@link Query} object. 733 | * 734 | * @param fetchItems a callback for fetching items 735 | * @param sizeCallback a callback for getting the count of items 736 | * 737 | * @see CallbackDataProvider 738 | * @see #setDataProvider(DataProvider) 739 | */ 740 | public void setDataProvider(final FetchItemsCallback fetchItems, final SerializableToIntFunction sizeCallback) { 741 | setDataProvider(new CallbackDataProvider<>(q -> fetchItems.fetchItems(q.getFilter() 742 | .orElse(""), q.getOffset(), q.getLimit()), 743 | q -> sizeCallback.applyAsInt(q.getFilter() 744 | .orElse("")))); 745 | } 746 | 747 | /** 748 | * Predicate to check {@link ComboBoxMultiselect} item captions against user typed strings. 749 | * 750 | * @see #setItems(CaptionFilter, Collection) 751 | * @see #setItems(CaptionFilter, Object[]) 752 | * @since 8.0 753 | */ 754 | @FunctionalInterface 755 | public interface CaptionFilter extends SerializableBiPredicate { 756 | 757 | /** 758 | * Check item caption against entered text. 759 | * 760 | * @param itemCaption the caption of the item to filter, not {@code null} 761 | * @param filterText user entered filter, not {@code null} 762 | * @return {@code true} if item passes the filter and should be listed, {@code false} otherwise 763 | */ 764 | @Override 765 | public boolean test(String itemCaption, String filterText); 766 | } 767 | 768 | /** 769 | * Removes the given items. Any item that is not currently selected, is ignored. If none of the items are selected, does nothing. 770 | * 771 | * @param items the items to deselect, not {@code null} 772 | * @param userOriginated {@code true} if this was used originated, {@code false} if not 773 | */ 774 | @Override 775 | protected void deselect(final Set items, final boolean userOriginated) { 776 | Objects.requireNonNull(items); 777 | if (items.stream() 778 | .noneMatch(i -> isSelected(i))) { 779 | return; 780 | } 781 | 782 | updateSelection(set -> set.removeAll(items), userOriginated, true); 783 | } 784 | 785 | /** 786 | * Deselects the given item. If the item is not currently selected, does nothing. 787 | * 788 | * @param item the item to deselect, not null 789 | * @param userOriginated {@code true} if this was used originated, {@code false} if not 790 | */ 791 | @Override 792 | protected void deselect(final T item, final boolean userOriginated) { 793 | if (!getSelectedItems().contains(item)) { 794 | return; 795 | } 796 | 797 | updateSelection(set -> set.remove(item), userOriginated, false); 798 | } 799 | 800 | @Override 801 | public void deselectAll() { 802 | if (getSelectedItems().isEmpty()) { 803 | return; 804 | } 805 | 806 | updateSelection(Collection::clear, false, true); 807 | } 808 | 809 | /** 810 | * Selects the given item. Depending on the implementation, may cause other items to be deselected. If the item is already selected, does nothing. 811 | * 812 | * @param item the item to select, not null 813 | * @param userOriginated {@code true} if this was used originated, {@code false} if not 814 | */ 815 | @Override 816 | protected void select(final T item, final boolean userOriginated) { 817 | if (getSelectedItems().contains(item)) { 818 | return; 819 | } 820 | 821 | updateSelection(set -> set.add(item), userOriginated, true); 822 | } 823 | 824 | @Override 825 | protected void updateSelection(final Set addedItems, final Set removedItems, final boolean userOriginated) { 826 | updateSelection(addedItems, removedItems, userOriginated, true); 827 | } 828 | 829 | /** 830 | * Updates the selection by adding and removing the given items. 831 | * 832 | * @param addedItems the items added to selection, not {@code} null 833 | * @param removedItems the items removed from selection, not {@code} null 834 | * @param userOriginated {@code true} if this was used originated, {@code false} if not 835 | * @param sortingNeeded is sorting needed before sending data back to client 836 | */ 837 | protected void updateSelection(final Set addedItems, final Set removedItems, final boolean userOriginated, final boolean sortingNeeded) { 838 | Objects.requireNonNull(addedItems); 839 | Objects.requireNonNull(removedItems); 840 | 841 | // if there are duplicates, some item is both added & removed, just 842 | // discard that and leave things as was before 843 | addedItems.removeIf(item -> removedItems.remove(item)); 844 | 845 | if (getSelectedItems().containsAll(addedItems) && Collections.disjoint(getSelectedItems(), removedItems)) { 846 | return; 847 | } 848 | 849 | updateSelection(set -> { 850 | // order of add / remove does not matter since no duplicates 851 | set.removeAll(removedItems); 852 | set.addAll(addedItems); 853 | }, userOriginated, sortingNeeded); 854 | } 855 | 856 | private void updateSelection(final SerializableConsumer> handler, final boolean userOriginated, final boolean sortingNeeded) { 857 | final LinkedHashSet oldSelection = new LinkedHashSet<>(getSelectedItems()); 858 | final List selection = new ArrayList<>(getSelectedItems()); 859 | handler.accept(selection); 860 | 861 | if (sortingNeeded) { 862 | this.sortingSelection = Collections.unmodifiableCollection(selection); 863 | } 864 | 865 | // TODO selection is private, have to use reflection (remove later) 866 | try { 867 | final Field f1 = getSelectionBaseClass().getDeclaredField("selection"); 868 | f1.setAccessible(true); 869 | f1.set(this, selection); 870 | } 871 | catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) { 872 | e.printStackTrace(); 873 | } 874 | 875 | doSetSelectedKeys(selection); 876 | 877 | fireEvent(new MultiSelectionEvent<>(this, oldSelection, userOriginated)); 878 | 879 | getDataProvider().refreshAll(); 880 | } 881 | 882 | protected Class getSelectionBaseClass() { 883 | return this.getClass() 884 | .getSuperclass(); 885 | } 886 | 887 | /** 888 | * Sets the selected item based on the given communication key. If the key is {@code null}, clears the current selection if any. 889 | * 890 | * @param items the selected items or {@code null} to clear selection 891 | */ 892 | protected void doSetSelectedKeys(final List items) { 893 | final Set keys = itemsToKeys(items); 894 | 895 | getState().selectedItemKeys = keys; 896 | 897 | updateSelectedItemsCaption(); 898 | } 899 | 900 | private void updateSelectedItemsCaption() { 901 | final List items = new ArrayList<>(); 902 | 903 | if (getState().selectedItemKeys != null && !getState().selectedItemKeys.isEmpty()) { 904 | for (final String selectedItemKey : getState().selectedItemKeys) { 905 | final T value = getDataCommunicator().getKeyMapper() 906 | .get(selectedItemKey); 907 | if (value != null) { 908 | items.add(value); 909 | } 910 | } 911 | } 912 | 913 | getState().selectedItemsCaption = this.inputTextFieldCaptionGenerator.apply(items); 914 | } 915 | 916 | /** 917 | * Returns the communication key assigned to the given item. 918 | * 919 | * @param item the item whose key to return 920 | * @return the assigned key 921 | */ 922 | protected String itemToKey(final T item) { 923 | if (item == null) { 924 | return null; 925 | } 926 | // TODO creates a key if item not in data provider 927 | return getDataCommunicator().getKeyMapper() 928 | .key(item); 929 | } 930 | 931 | /** 932 | * Returns the communication keys assigned to the given items. 933 | * 934 | * @param items the items whose key to return 935 | * @return the assigned keys 936 | */ 937 | protected Set itemsToKeys(final List items) { 938 | if (items == null) { 939 | return null; 940 | } 941 | 942 | final Set keys = new LinkedHashSet<>(); 943 | for (final T item : items) { 944 | keys.add(itemToKey(item)); 945 | } 946 | return keys; 947 | } 948 | 949 | public void showClearButton(final boolean showClearButton) { 950 | getState().showClearButton = showClearButton; 951 | } 952 | 953 | public void showSelectAllButton(final boolean showSelectAllButton) { 954 | getState().showSelectAllButton = showSelectAllButton; 955 | } 956 | 957 | public void setClearButtonCaption(final String clearButtonCaption) { 958 | getState().clearButtonCaption = clearButtonCaption; 959 | } 960 | 961 | public void setSelectAllButtonCaption(final String selectAllButtonCaption) { 962 | getState().selectAllButtonCaption = selectAllButtonCaption; 963 | } 964 | } 965 | -------------------------------------------------------------------------------- /vaadin-combobox-multiselect-addon/src/main/java/org/vaadin/addons/client/VComboBoxMultiselect.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2000-2016 Vaadin Ltd. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package org.vaadin.addons.client; 18 | 19 | import java.util.ArrayList; 20 | import java.util.Arrays; 21 | import java.util.Collection; 22 | import java.util.Date; 23 | import java.util.HashSet; 24 | import java.util.Iterator; 25 | import java.util.LinkedHashSet; 26 | import java.util.List; 27 | import java.util.Set; 28 | 29 | import com.google.gwt.animation.client.AnimationScheduler; 30 | import com.google.gwt.aria.client.CheckedValue; 31 | import com.google.gwt.aria.client.Property; 32 | import com.google.gwt.aria.client.Roles; 33 | import com.google.gwt.aria.client.State; 34 | import com.google.gwt.cell.client.IsCollapsible; 35 | import com.google.gwt.core.client.JavaScriptObject; 36 | import com.google.gwt.core.client.Scheduler.ScheduledCommand; 37 | import com.google.gwt.dom.client.Document; 38 | import com.google.gwt.dom.client.Element; 39 | import com.google.gwt.dom.client.NativeEvent; 40 | import com.google.gwt.dom.client.Style; 41 | import com.google.gwt.dom.client.Style.Display; 42 | import com.google.gwt.dom.client.Style.Unit; 43 | import com.google.gwt.dom.client.Style.Visibility; 44 | import com.google.gwt.event.dom.client.BlurEvent; 45 | import com.google.gwt.event.dom.client.BlurHandler; 46 | import com.google.gwt.event.dom.client.ClickEvent; 47 | import com.google.gwt.event.dom.client.ClickHandler; 48 | import com.google.gwt.event.dom.client.FocusEvent; 49 | import com.google.gwt.event.dom.client.FocusHandler; 50 | import com.google.gwt.event.dom.client.KeyCodes; 51 | import com.google.gwt.event.dom.client.KeyDownEvent; 52 | import com.google.gwt.event.dom.client.KeyDownHandler; 53 | import com.google.gwt.event.dom.client.KeyUpEvent; 54 | import com.google.gwt.event.dom.client.KeyUpHandler; 55 | import com.google.gwt.event.dom.client.LoadEvent; 56 | import com.google.gwt.event.dom.client.LoadHandler; 57 | import com.google.gwt.event.dom.client.MouseDownEvent; 58 | import com.google.gwt.event.dom.client.MouseDownHandler; 59 | import com.google.gwt.event.logical.shared.CloseEvent; 60 | import com.google.gwt.event.logical.shared.CloseHandler; 61 | import com.google.gwt.i18n.client.HasDirection.Direction; 62 | import com.google.gwt.user.client.Command; 63 | import com.google.gwt.user.client.DOM; 64 | import com.google.gwt.user.client.Event; 65 | import com.google.gwt.user.client.Event.NativePreviewEvent; 66 | import com.google.gwt.user.client.Timer; 67 | import com.google.gwt.user.client.Window; 68 | import com.google.gwt.user.client.ui.Composite; 69 | import com.google.gwt.user.client.ui.FlowPanel; 70 | import com.google.gwt.user.client.ui.HTML; 71 | import com.google.gwt.user.client.ui.PopupPanel; 72 | import com.google.gwt.user.client.ui.PopupPanel.PositionCallback; 73 | import com.google.gwt.user.client.ui.SuggestOracle.Suggestion; 74 | import com.google.gwt.user.client.ui.TextBox; 75 | import com.google.gwt.user.client.ui.Widget; 76 | import com.vaadin.client.ApplicationConnection; 77 | import com.vaadin.client.BrowserInfo; 78 | import com.vaadin.client.ComputedStyle; 79 | import com.vaadin.client.DeferredWorker; 80 | import com.vaadin.client.Focusable; 81 | import com.vaadin.client.VConsole; 82 | import com.vaadin.client.WidgetUtil; 83 | import com.vaadin.client.ui.Field; 84 | import com.vaadin.client.ui.Icon; 85 | import com.vaadin.client.ui.SubPartAware; 86 | import com.vaadin.client.ui.VCheckBox; 87 | import com.vaadin.client.ui.VComboBox; 88 | import com.vaadin.client.ui.VLazyExecutor; 89 | import com.vaadin.client.ui.VOverlay; 90 | import com.vaadin.client.ui.aria.AriaHelper; 91 | import com.vaadin.client.ui.aria.HandlesAriaCaption; 92 | import com.vaadin.client.ui.aria.HandlesAriaInvalid; 93 | import com.vaadin.client.ui.aria.HandlesAriaRequired; 94 | import com.vaadin.client.ui.menubar.MenuBar; 95 | import com.vaadin.client.ui.menubar.MenuItem; 96 | import com.vaadin.shared.AbstractComponentState; 97 | import com.vaadin.shared.ui.ComponentStateUtil; 98 | import com.vaadin.shared.util.SharedUtil; 99 | 100 | /** 101 | * Client side implementation of the ComboBoxMultiselect component. 102 | * 103 | * TODO needs major refactoring (to be extensible etc) 104 | * 105 | * @since 8.0 106 | */ 107 | @SuppressWarnings("deprecation") 108 | public class VComboBoxMultiselect extends Composite 109 | implements Field, KeyDownHandler, KeyUpHandler, ClickHandler, FocusHandler, BlurHandler, Focusable, 110 | SubPartAware, HandlesAriaCaption, HandlesAriaInvalid, HandlesAriaRequired, DeferredWorker, MouseDownHandler { 111 | 112 | /** 113 | * Represents a suggestion in the suggestion popup box. 114 | */ 115 | public class ComboBoxMultiselectSuggestion implements Suggestion, Command { 116 | 117 | private final String key; 118 | private final String caption; 119 | private String untranslatedIconUri; 120 | private String style; 121 | private final VCheckBox checkBox; 122 | private Date lastExecution; 123 | 124 | /** 125 | * Constructor for a single suggestion. 126 | * 127 | * @param key 128 | * item key, empty string for a special null item not in 129 | * container 130 | * @param caption 131 | * item caption 132 | * @param style 133 | * item style name, can be empty string 134 | * @param untranslatedIconUri 135 | * icon URI or null 136 | */ 137 | public ComboBoxMultiselectSuggestion(String key, String caption, String style, String untranslatedIconUri) { 138 | this.key = key; 139 | this.caption = caption; 140 | this.style = style; 141 | this.untranslatedIconUri = untranslatedIconUri; 142 | 143 | this.checkBox = new VCheckBox(); 144 | this.checkBox.setEnabled(false); 145 | State.HIDDEN.set(getCheckBoxElement(), true); 146 | } 147 | 148 | /** 149 | * Gets the visible row in the popup as a HTML string. The string 150 | * contains an image tag with the rows icon (if an icon has been 151 | * specified) and the caption of the item 152 | */ 153 | 154 | @Override 155 | public String getDisplayString() { 156 | final StringBuilder sb = new StringBuilder(); 157 | ApplicationConnection client = VComboBoxMultiselect.this.connector.getConnection(); 158 | final Icon icon = client.getIcon(client.translateVaadinUri(this.untranslatedIconUri)); 159 | if (icon != null) { 160 | sb.append(icon.getElement() 161 | .getString()); 162 | } 163 | String content; 164 | if ("".equals(this.caption)) { 165 | // Ensure that empty options use the same height as other 166 | // options and are not collapsed (#7506) 167 | content = " "; 168 | } else { 169 | content = WidgetUtil.escapeHTML(this.caption); 170 | } 171 | sb.append("" + content + ""); 172 | return sb.toString(); 173 | } 174 | 175 | /** 176 | * Get a string that represents this item. This is used in the text box. 177 | */ 178 | 179 | @Override 180 | public String getReplacementString() { 181 | return this.caption; 182 | } 183 | 184 | /** 185 | * Get aria label for this item. 186 | */ 187 | public String getAriaLabel() { 188 | return this.caption; 189 | } 190 | 191 | /** 192 | * Get the option key which represents the item on the server side. 193 | * 194 | * @return The key of the item 195 | */ 196 | public String getOptionKey() { 197 | return this.key; 198 | } 199 | 200 | /** 201 | * Get the URI of the icon. Used when constructing the displayed option. 202 | * 203 | * @return real (translated) icon URI or null if none 204 | */ 205 | public String getIconUri() { 206 | ApplicationConnection client = VComboBoxMultiselect.this.connector.getConnection(); 207 | return client.translateVaadinUri(this.untranslatedIconUri); 208 | } 209 | 210 | /** 211 | * Gets the style set for this suggestion item. Styles are typically set 212 | * by a server-side. The returned style is prefixed by 213 | * v-filterselect-item-. 214 | * 215 | * @since 7.5.6 216 | * @return the style name to use, or null to not apply any 217 | * custom style. 218 | */ 219 | public String getStyle() { 220 | return this.style; 221 | } 222 | 223 | /** 224 | * Executes a selection of this item. 225 | */ 226 | 227 | @Override 228 | public void execute() { 229 | if (this.lastExecution == null) { 230 | this.lastExecution = new Date(); 231 | onSuggestionSelected(this); 232 | return; 233 | } 234 | 235 | if (new Date().getTime() - this.lastExecution.getTime() > 300) { 236 | onSuggestionSelected(this); 237 | } 238 | } 239 | 240 | @Override 241 | public boolean equals(Object obj) { 242 | if (!(obj instanceof ComboBoxMultiselectSuggestion)) { 243 | return false; 244 | } 245 | ComboBoxMultiselectSuggestion other = (ComboBoxMultiselectSuggestion) obj; 246 | if (this.key == null && other.key != null || this.key != null && !this.key.equals(other.key)) { 247 | return false; 248 | } 249 | if (this.caption == null && other.caption != null 250 | || this.caption != null && !this.caption.equals(other.caption)) { 251 | return false; 252 | } 253 | if (!SharedUtil.equals(this.untranslatedIconUri, other.untranslatedIconUri)) { 254 | return false; 255 | } 256 | 257 | return SharedUtil.equals(this.style, other.style); 258 | } 259 | 260 | @Override 261 | public int hashCode() { 262 | final int prime = 31; 263 | int result = 1; 264 | result = prime * result + VComboBoxMultiselect.this.hashCode(); 265 | result = prime * result + ((key == null) ? 0 : key.hashCode()); 266 | result = prime * result + ((caption == null) ? 0 : caption.hashCode()); 267 | result = prime * result + ((untranslatedIconUri == null) ? 0 : untranslatedIconUri.hashCode()); 268 | result = prime * result + ((style == null) ? 0 : style.hashCode()); 269 | return result; 270 | } 271 | 272 | public VCheckBox getCheckBox() { 273 | return this.checkBox; 274 | } 275 | 276 | Element getCheckBoxElement() { 277 | return this.checkBox.getElement() 278 | .getFirstChildElement(); 279 | } 280 | 281 | public boolean isChecked() { 282 | return getCheckBox().getValue(); 283 | } 284 | 285 | public void setChecked(boolean checked) { 286 | MenuItem menuItem = VComboBoxMultiselect.this.suggestionPopup.getMenuItem(this); 287 | if (menuItem != null) { 288 | State.CHECKED.set(menuItem.getElement(), CheckedValue.of(checked)); 289 | } 290 | 291 | getCheckBox().setValue(checked); 292 | } 293 | } 294 | 295 | /** An inner class that handles all logic related to mouse wheel. */ 296 | private class MouseWheeler { 297 | 298 | /** 299 | * A JavaScript function that handles the mousewheel DOM event, and 300 | * passes it on to Java code. 301 | * 302 | * @see #createMousewheelListenerFunction(Widget) 303 | */ 304 | protected final JavaScriptObject mousewheelListenerFunction; 305 | 306 | protected MouseWheeler() { 307 | this.mousewheelListenerFunction = createMousewheelListenerFunction(VComboBoxMultiselect.this); 308 | } 309 | 310 | protected native JavaScriptObject createMousewheelListenerFunction(Widget widget) 311 | /*-{ 312 | return $entry(function(e) { 313 | var deltaX = e.deltaX ? e.deltaX : -0.5*e.wheelDeltaX; 314 | var deltaY = e.deltaY ? e.deltaY : -0.5*e.wheelDeltaY; 315 | 316 | // IE8 has only delta y 317 | if (isNaN(deltaY)) { 318 | deltaY = -0.5*e.wheelDelta; 319 | } 320 | 321 | @org.vaadin.addons.client.VComboBoxMultiselect.JsniUtil::moveScrollFromEvent(*)(widget, deltaX, deltaY, e, e.deltaMode); 322 | }); 323 | }-*/; 324 | 325 | public void attachMousewheelListener(Element element) { 326 | attachMousewheelListenerNative(element, this.mousewheelListenerFunction); 327 | } 328 | 329 | public native void attachMousewheelListenerNative(Element element, JavaScriptObject mousewheelListenerFunction) 330 | /*-{ 331 | if (element.addEventListener) { 332 | // FireFox likes "wheel", while others use "mousewheel" 333 | var eventName = 'onmousewheel' in element ? 'mousewheel' : 'wheel'; 334 | element.addEventListener(eventName, mousewheelListenerFunction); 335 | } 336 | }-*/; 337 | 338 | public void detachMousewheelListener(Element element) { 339 | detachMousewheelListenerNative(element, this.mousewheelListenerFunction); 340 | } 341 | 342 | public native void detachMousewheelListenerNative(Element element, JavaScriptObject mousewheelListenerFunction) 343 | /*-{ 344 | if (element.addEventListener) { 345 | // FireFox likes "wheel", while others use "mousewheel" 346 | var eventName = element.onwheel===undefined?"mousewheel":"wheel"; 347 | element.removeEventListener(eventName, mousewheelListenerFunction); 348 | } 349 | }-*/; 350 | 351 | } 352 | 353 | /** 354 | * A utility class that contains utility methods that are usually called 355 | * from JSNI. 356 | *

357 | * The methods are moved in this class to minimize the amount of JSNI code 358 | * as much as feasible. 359 | */ 360 | static class JsniUtil { 361 | private JsniUtil() { 362 | } 363 | 364 | private static final int DOM_DELTA_PIXEL = 0; 365 | private static final int DOM_DELTA_LINE = 1; 366 | private static final int DOM_DELTA_PAGE = 2; 367 | 368 | // Rough estimation of item height 369 | private static final int SCROLL_UNIT_PX = 25; 370 | 371 | private static double deltaSum = 0; 372 | 373 | public static void moveScrollFromEvent(final Widget widget, final double deltaX, final double deltaY, 374 | final NativeEvent event, final int deltaMode) { 375 | if (!Double.isNaN(deltaY)) { 376 | VComboBoxMultiselect filterSelect = (VComboBoxMultiselect) widget; 377 | 378 | switch (deltaMode) { 379 | case DOM_DELTA_LINE: 380 | if (deltaY >= 0) { 381 | filterSelect.suggestionPopup.selectNextItem(); 382 | } else { 383 | filterSelect.suggestionPopup.selectPrevItem(); 384 | } 385 | break; 386 | case DOM_DELTA_PAGE: 387 | if (deltaY >= 0) { 388 | filterSelect.selectNextPage(); 389 | } else { 390 | filterSelect.selectPrevPage(); 391 | } 392 | break; 393 | case DOM_DELTA_PIXEL: 394 | default: 395 | // Accumulate dampened deltas 396 | deltaSum += Math.pow(Math.abs(deltaY), 0.7) * Math.signum(deltaY); 397 | 398 | // "Scroll" if change exceeds item height 399 | while (Math.abs(deltaSum) >= SCROLL_UNIT_PX) { 400 | if (!filterSelect.dataReceivedHandler.isWaitingForFilteringResponse()) { 401 | // Move selection if page flip is not in progress 402 | if (deltaSum < 0) { 403 | filterSelect.suggestionPopup.selectPrevItem(); 404 | } else { 405 | filterSelect.suggestionPopup.selectNextItem(); 406 | } 407 | } 408 | deltaSum -= SCROLL_UNIT_PX * Math.signum(deltaSum); 409 | } 410 | break; 411 | } 412 | } 413 | } 414 | } 415 | 416 | /** 417 | * Represents the popup box with the selection options. Wraps a suggestion 418 | * menu. 419 | */ 420 | public class SuggestionPopup extends VOverlay implements PositionCallback, CloseHandler { 421 | 422 | private static final int Z_INDEX = 30000; 423 | 424 | /** For internal use only. May be removed or replaced in the future. */ 425 | public final SuggestionMenu menu; 426 | 427 | private final Element up = DOM.createDiv(); 428 | private final Element down = DOM.createDiv(); 429 | private final Element status = DOM.createDiv(); 430 | 431 | private boolean isPagingEnabled = true; 432 | 433 | private long lastAutoClosed; 434 | 435 | private int popupOuterPadding = -1; 436 | 437 | private int topPosition; 438 | private int leftPosition; 439 | 440 | private final MouseWheeler mouseWheeler = new MouseWheeler(); 441 | 442 | private boolean scrollPending = false; 443 | 444 | /** 445 | * Default constructor 446 | */ 447 | SuggestionPopup() { 448 | super(true, false); 449 | debug("VComboBoxMultiselect.SP: constructor()"); 450 | setOwner(VComboBoxMultiselect.this); 451 | this.menu = new SuggestionMenu(); 452 | setWidget(this.menu); 453 | 454 | getElement().getStyle() 455 | .setZIndex(Z_INDEX); 456 | 457 | final Element root = getContainerElement(); 458 | 459 | this.up.setInnerHTML("Prev"); 460 | DOM.sinkEvents(this.up, Event.ONCLICK); 461 | 462 | this.down.setInnerHTML("Next"); 463 | DOM.sinkEvents(this.down, Event.ONCLICK); 464 | 465 | root.insertFirst(this.up); 466 | root.appendChild(this.down); 467 | root.appendChild(this.status); 468 | 469 | DOM.sinkEvents(root, Event.ONMOUSEDOWN | Event.ONMOUSEWHEEL); 470 | addCloseHandler(this); 471 | 472 | Roles.getListRole() 473 | .set(getElement()); 474 | 475 | setPreviewingAllNativeEvents(true); 476 | } 477 | 478 | public MenuItem getMenuItem(Command command) { 479 | for (MenuItem menuItem : this.menu.getItems()) { 480 | if (command.equals(menuItem.getCommand())) { 481 | return menuItem; 482 | } 483 | } 484 | return null; 485 | } 486 | 487 | @Override 488 | protected void onLoad() { 489 | super.onLoad(); 490 | 491 | // Register mousewheel listener on paged select 492 | if (VComboBoxMultiselect.this.pageLength > 0) { 493 | this.mouseWheeler.attachMousewheelListener(getElement()); 494 | } 495 | } 496 | 497 | @Override 498 | protected void onUnload() { 499 | this.mouseWheeler.detachMousewheelListener(getElement()); 500 | super.onUnload(); 501 | } 502 | 503 | /** 504 | * Shows the popup where the user can see the filtered options that have 505 | * been set with a call to 506 | * {@link SuggestionMenu#setSuggestions(Collection)}. 507 | * 508 | * @param currentPage 509 | * The current page number 510 | */ 511 | public void showSuggestions(final int currentPage) { 512 | debug("VComboBoxMultiselect.SP: showSuggestions(" + currentPage + ", " + getTotalSuggestions() + ")"); 513 | 514 | final SuggestionPopup popup = this; 515 | // Add TT anchor point 516 | getElement().setId("VAADIN_COMBOBOX_OPTIONLIST"); 517 | 518 | this.leftPosition = getDesiredLeftPosition(); 519 | this.topPosition = getDesiredTopPosition(); 520 | 521 | setPopupPosition(this.leftPosition, this.topPosition); 522 | 523 | final int first = currentPage * VComboBoxMultiselect.this.pageLength + 1; 524 | final int last = first + VComboBoxMultiselect.this.currentSuggestions.size() - 1; 525 | final int matches = getTotalSuggestions(); 526 | if (last > 0) { 527 | // nullsel not counted, as requested by user 528 | this.status.setInnerText((matches == 0 ? 0 : first) + "-" + last + "/" + matches); 529 | } else { 530 | this.status.setInnerText(""); 531 | } 532 | // We don't need to show arrows or statusbar if there is 533 | // only one page 534 | if (matches <= VComboBoxMultiselect.this.pageLength || VComboBoxMultiselect.this.pageLength == 0) { 535 | setPagingEnabled(false); 536 | } else { 537 | setPagingEnabled(true); 538 | } 539 | setPrevButtonActive(first > 1); 540 | setNextButtonActive(last < matches); 541 | 542 | // clear previously fixed width 543 | this.menu.setWidth(""); 544 | this.menu.getElement() 545 | .getFirstChildElement() 546 | .getStyle() 547 | .clearWidth(); 548 | 549 | setPopupPositionAndShow(popup); 550 | } 551 | 552 | private int getDesiredTopPosition() { 553 | return toInt32(WidgetUtil.getBoundingClientRect(VComboBoxMultiselect.this.tb.getElement()) 554 | .getBottom()) + Window.getScrollTop(); 555 | } 556 | 557 | private int getDesiredLeftPosition() { 558 | return toInt32(WidgetUtil.getBoundingClientRect(VComboBoxMultiselect.this.getElement()) 559 | .getLeft()); 560 | } 561 | 562 | private native int toInt32(double val) 563 | /*-{ 564 | return val | 0; 565 | }-*/; 566 | 567 | /** 568 | * Should the next page button be visible to the user? 569 | * 570 | * @param active 571 | */ 572 | private void setNextButtonActive(boolean active) { 573 | debug("VComboBoxMultiselect.SP: setNextButtonActive(" + active + ")"); 574 | 575 | if (active) { 576 | DOM.sinkEvents(this.down, Event.ONCLICK); 577 | this.down.setClassName(VComboBoxMultiselect.this.getStylePrimaryName() + "-nextpage"); 578 | } else { 579 | DOM.sinkEvents(this.down, 0); 580 | this.down.setClassName(VComboBoxMultiselect.this.getStylePrimaryName() + "-nextpage-off"); 581 | } 582 | } 583 | 584 | /** 585 | * Should the previous page button be visible to the user 586 | * 587 | * @param active 588 | */ 589 | private void setPrevButtonActive(boolean active) { 590 | debug("VComboBoxMultiselect.SP: setPrevButtonActive(" + active + ")"); 591 | 592 | if (active) { 593 | DOM.sinkEvents(this.up, Event.ONCLICK); 594 | this.up.setClassName(VComboBoxMultiselect.this.getStylePrimaryName() + "-prevpage"); 595 | } else { 596 | DOM.sinkEvents(this.up, 0); 597 | this.up.setClassName(VComboBoxMultiselect.this.getStylePrimaryName() + "-prevpage-off"); 598 | } 599 | 600 | } 601 | 602 | /** 603 | * Selects the next item in the filtered selections. 604 | */ 605 | public void selectNextItem() { 606 | debug("VComboBoxMultiselect.SP: selectNextItem()"); 607 | 608 | final int index = this.menu.getSelectedIndex() + 1; 609 | if (this.menu.getItems() 610 | .size() > index) { 611 | selectItem(this.menu.getItems() 612 | .get(index)); 613 | 614 | } else { 615 | selectNextPage(); 616 | } 617 | } 618 | 619 | /** 620 | * Selects the previous item in the filtered selections. 621 | */ 622 | public void selectPrevItem() { 623 | debug("VComboBoxMultiselect.SP: selectPrevItem()"); 624 | 625 | final int index = this.menu.getSelectedIndex() - 1; 626 | if (index > -1) { 627 | selectItem(this.menu.getItems() 628 | .get(index)); 629 | 630 | } else if (index == -1) { 631 | selectPrevPage(); 632 | 633 | } else { 634 | if (!this.menu.getItems() 635 | .isEmpty()) { 636 | selectLastItem(); 637 | } 638 | } 639 | } 640 | 641 | /** 642 | * Select the first item of the suggestions list popup. 643 | * 644 | * @since 7.2.6 645 | */ 646 | public void selectFirstItem() { 647 | debug("VFS.SP: selectFirstItem()"); 648 | int index = 0; 649 | List items = menu.getItems(); 650 | 651 | if (items != null) { 652 | if (!items.isEmpty() && items.size() > 1) { 653 | if (VComboBoxMultiselect.this.showClearButton && VComboBoxMultiselect.this.showSelectAllButton) { 654 | index = 2; 655 | } else if (VComboBoxMultiselect.this.showClearButton 656 | || VComboBoxMultiselect.this.showSelectAllButton) { 657 | index = 1; 658 | } 659 | } 660 | 661 | if (!items.isEmpty()) { 662 | selectItem(getFirstNotSelectedItem(index)); 663 | } 664 | } 665 | } 666 | 667 | /** 668 | * returns first not checked item, if all are checked first item will be 669 | * returned 670 | * 671 | * @param mi 672 | */ 673 | private MenuItem getFirstNotSelectedItem(int index) { 674 | MenuItem found = getFirstNotSelectedItemRecursive(index); 675 | return found == null ? this.menu.getItems() 676 | .get(index) : found; 677 | } 678 | 679 | private MenuItem getFirstNotSelectedItemRecursive(int index) { 680 | if (index >= this.menu.getItems() 681 | .size()) { 682 | return null; 683 | } 684 | 685 | MenuItem mi = this.menu.getItems() 686 | .get(index); 687 | 688 | if (mi == null) { 689 | return null; 690 | } 691 | 692 | ComboBoxMultiselectSuggestion suggestion = (ComboBoxMultiselectSuggestion) mi.getCommand(); 693 | 694 | if (suggestion.isChecked()) { 695 | return getFirstNotSelectedItemRecursive(index + 1); 696 | } 697 | return mi; 698 | } 699 | 700 | /** 701 | * Select the last item of the suggestions list popup. 702 | * 703 | * @since 7.2.6 704 | */ 705 | public void selectLastItem() { 706 | debug("VComboBoxMultiselect.SP: selectLastItem()"); 707 | selectItem(this.menu.getLastItem()); 708 | } 709 | 710 | /* 711 | * Sets the selected item in the popup menu. 712 | */ 713 | private void selectItem(final MenuItem newSelectedItem) { 714 | this.menu.selectItem(newSelectedItem); 715 | } 716 | 717 | /** 718 | * Selects the item at the given index 719 | * 720 | * @param index 721 | * item at index to select 722 | */ 723 | public void selectItemAtIndex(int index) { 724 | if (index == -1) { 725 | return; 726 | } 727 | if (VComboBoxMultiselect.this.showSelectAllButton) { 728 | index++; 729 | } 730 | if (VComboBoxMultiselect.this.showClearButton) { 731 | index++; 732 | } 733 | selectItem(this.menu.getItems() 734 | .get(index)); 735 | } 736 | 737 | /* 738 | * Using a timer to scroll up or down the pages so when we receive lots 739 | * of consecutive mouse wheel events the pages does not flicker. 740 | */ 741 | private LazyPageScroller lazyPageScroller = new LazyPageScroller(); 742 | 743 | private class LazyPageScroller extends Timer { 744 | private int pagesToScroll = 0; 745 | 746 | @Override 747 | public void run() { 748 | debug("VComboBoxMultiselect.SP.LPS: run()"); 749 | 750 | if (this.pagesToScroll != 0) { 751 | if (!VComboBoxMultiselect.this.dataReceivedHandler.isWaitingForFilteringResponse()) { 752 | /* 753 | * Avoid scrolling while we are waiting for a response 754 | * because otherwise the waiting flag will be reset in 755 | * the first response and the second response will be 756 | * ignored, causing an empty popup... 757 | * 758 | * As long as the scrolling delay is suitable 759 | * double/triple clicks will work by scrolling two or 760 | * three pages at a time and this should not be a 761 | * problem. 762 | */ 763 | // this makes sure that we don't close the popup 764 | VComboBoxMultiselect.this.dataReceivedHandler.setNavigationCallback(() -> { 765 | }); 766 | filterOptions( VComboBoxMultiselect.this.currentPage + this.pagesToScroll, 767 | VComboBoxMultiselect.this.lastFilter); 768 | } 769 | this.pagesToScroll = 0; 770 | } 771 | } 772 | 773 | public void scrollUp() { 774 | debug("VComboBoxMultiselect.SP.LPS: scrollUp()"); 775 | if (VComboBoxMultiselect.this.pageLength > 0 776 | && VComboBoxMultiselect.this.currentPage + this.pagesToScroll > 0) { 777 | this.pagesToScroll--; 778 | cancel(); 779 | schedule(200); 780 | } 781 | } 782 | 783 | public void scrollDown() { 784 | debug("VComboBoxMultiselect.SP.LPS: scrollDown()"); 785 | if (VComboBoxMultiselect.this.pageLength > 0 786 | && getTotalSuggestions() > (VComboBoxMultiselect.this.currentPage + this.pagesToScroll + 1) 787 | * VComboBoxMultiselect.this.pageLength) { 788 | this.pagesToScroll++; 789 | cancel(); 790 | schedule(200); 791 | } 792 | } 793 | } 794 | 795 | private void scroll(double deltaY) { 796 | boolean scrollActive = this.menu.isScrollActive(); 797 | 798 | debug("VComboBoxMultiselect.SP: scroll() scrollActive: " + scrollActive); 799 | 800 | if (!scrollActive) { 801 | if (deltaY > 0d) { 802 | this.lazyPageScroller.scrollDown(); 803 | } else { 804 | this.lazyPageScroller.scrollUp(); 805 | } 806 | } 807 | } 808 | 809 | @Override 810 | public void onBrowserEvent(Event event) { 811 | debug("VComboBoxMultiselect.SP: onBrowserEvent()"); 812 | 813 | if (event.getTypeInt() == Event.ONCLICK) { 814 | final Element target = DOM.eventGetTarget(event); 815 | if (target == this.up || target == DOM.getChild(this.up, 0)) { 816 | this.lazyPageScroller.scrollUp(); 817 | } else if (target == this.down || target == DOM.getChild(this.down, 0)) { 818 | this.lazyPageScroller.scrollDown(); 819 | } 820 | 821 | } 822 | 823 | /* 824 | * Prevent the keyboard focus from leaving the textfield by 825 | * preventing the default behaviour of the browser. Fixes #4285. 826 | */ 827 | handleMouseDownEvent(event); 828 | } 829 | 830 | @Override 831 | protected void onPreviewNativeEvent(NativePreviewEvent event) { 832 | // Check all events outside the combobox to see if they scroll the 833 | // page. We cannot use e.g. Window.addScrollListener() because the 834 | // scrolled element can be at any level on the page. 835 | 836 | // Normally this is only called when the popup is showing, but make 837 | // sure we don't accidentally process all events when not showing. 838 | if (!this.scrollPending && isShowing() 839 | && !DOM.isOrHasChild(SuggestionPopup.this.getElement(), Element.as(event.getNativeEvent() 840 | .getEventTarget()))) { 841 | if (getDesiredLeftPosition() != this.leftPosition || getDesiredTopPosition() != this.topPosition) { 842 | updatePopupPositionOnScroll(); 843 | } 844 | } 845 | 846 | super.onPreviewNativeEvent(event); 847 | } 848 | 849 | /** 850 | * Make the popup follow the position of the ComboBoxMultiselect when 851 | * the page is scrolled. 852 | */ 853 | private void updatePopupPositionOnScroll() { 854 | if (!this.scrollPending) { 855 | AnimationScheduler.get() 856 | .requestAnimationFrame(timestamp -> { 857 | if (isShowing()) { 858 | this.leftPosition = getDesiredLeftPosition(); 859 | this.topPosition = getDesiredTopPosition(); 860 | setPopupPosition(this.leftPosition, this.topPosition); 861 | } 862 | this.scrollPending = false; 863 | }); 864 | this.scrollPending = true; 865 | } 866 | } 867 | 868 | /** 869 | * Should paging be enabled. If paging is enabled then only a certain 870 | * amount of items are visible at a time and a scrollbar or buttons are 871 | * visible to change page. If paging is turned of then all options are 872 | * rendered into the popup menu. 873 | * 874 | * @param paging 875 | * Should the paging be turned on? 876 | */ 877 | public void setPagingEnabled(boolean paging) { 878 | debug("VComboBoxMultiselect.SP: setPagingEnabled(" + paging + ")"); 879 | if (this.isPagingEnabled == paging) { 880 | return; 881 | } 882 | if (paging) { 883 | this.down.getStyle() 884 | .clearDisplay(); 885 | this.up.getStyle() 886 | .clearDisplay(); 887 | this.status.getStyle() 888 | .clearDisplay(); 889 | } else { 890 | this.down.getStyle() 891 | .setDisplay(Display.NONE); 892 | this.up.getStyle() 893 | .setDisplay(Display.NONE); 894 | this.status.getStyle() 895 | .setDisplay(Display.NONE); 896 | } 897 | this.isPagingEnabled = paging; 898 | } 899 | 900 | @Override 901 | public void setPosition(int offsetWidth, int offsetHeight) { 902 | debug("VComboBoxMultiselect.SP: setPosition(" + offsetWidth + ", " + offsetHeight + ")"); 903 | 904 | int top = this.topPosition; 905 | int left = getPopupLeft(); 906 | 907 | // reset menu size and retrieve its "natural" size 908 | this.menu.setHeight(""); 909 | if (VComboBoxMultiselect.this.currentPage > 0 && !hasNextPage()) { 910 | // fix height to avoid height change when getting to last page 911 | this.menu.fixHeightTo(VComboBoxMultiselect.this.pageLength); 912 | } 913 | 914 | // ignoring the parameter as in V7 915 | offsetHeight = getOffsetHeight(); 916 | final int desiredHeight = offsetHeight; 917 | final int desiredWidth = getMainWidth(); 918 | 919 | debug("VComboBoxMultiselect.SP: desired[" + desiredWidth + ", " + desiredHeight + "]"); 920 | 921 | Element menuFirstChild = this.menu.getElement() 922 | .getFirstChildElement(); 923 | int naturalMenuWidth; 924 | if (BrowserInfo.get() 925 | .isIE() 926 | && BrowserInfo.get() 927 | .getBrowserMajorVersion() < 10) { 928 | // On IE 8 & 9 visibility is set to hidden and measuring 929 | // elements while they are hidden yields incorrect results 930 | String before = this.menu.getElement() 931 | .getParentElement() 932 | .getStyle() 933 | .getVisibility(); 934 | this.menu.getElement() 935 | .getParentElement() 936 | .getStyle() 937 | .setVisibility(Visibility.VISIBLE); 938 | naturalMenuWidth = WidgetUtil.getRequiredWidth(menuFirstChild); 939 | this.menu.getElement() 940 | .getParentElement() 941 | .getStyle() 942 | .setProperty("visibility", before); 943 | } else { 944 | naturalMenuWidth = WidgetUtil.getRequiredWidth(menuFirstChild); 945 | } 946 | 947 | if (this.popupOuterPadding == -1) { 948 | this.popupOuterPadding = WidgetUtil.measureHorizontalPaddingAndBorder(this.menu.getElement(), 2) 949 | + WidgetUtil 950 | .measureHorizontalPaddingAndBorder( VComboBoxMultiselect.this.suggestionPopup.getElement(), 951 | 0); 952 | } 953 | 954 | updateMenuWidth(desiredWidth, naturalMenuWidth); 955 | 956 | if (BrowserInfo.get() 957 | .isIE() 958 | && BrowserInfo.get() 959 | .getBrowserMajorVersion() < 11) { 960 | // Must take margin,border,padding manually into account for 961 | // menu element as we measure the element child and set width to 962 | // the element parent 963 | 964 | double naturalMenuOuterWidth; 965 | if (BrowserInfo.get() 966 | .getBrowserMajorVersion() < 10) { 967 | // On IE 8 & 9 visibility is set to hidden and measuring 968 | // elements while they are hidden yields incorrect results 969 | String before = this.menu.getElement() 970 | .getParentElement() 971 | .getStyle() 972 | .getVisibility(); 973 | this.menu.getElement() 974 | .getParentElement() 975 | .getStyle() 976 | .setVisibility(Visibility.VISIBLE); 977 | naturalMenuOuterWidth = WidgetUtil.getRequiredWidthDouble(menuFirstChild) 978 | + getMarginBorderPaddingWidth(this.menu.getElement()); 979 | this.menu.getElement() 980 | .getParentElement() 981 | .getStyle() 982 | .setProperty("visibility", before); 983 | } else { 984 | naturalMenuOuterWidth = WidgetUtil.getRequiredWidthDouble(menuFirstChild) 985 | + getMarginBorderPaddingWidth(this.menu.getElement()); 986 | } 987 | 988 | /* 989 | * IE requires us to specify the width for the container 990 | * element. Otherwise it will be 100% wide 991 | */ 992 | double rootWidth = Math.max(desiredWidth - this.popupOuterPadding, naturalMenuOuterWidth); 993 | getContainerElement().getStyle() 994 | .setWidth(rootWidth, Unit.PX); 995 | } 996 | 997 | final int textInputHeight = VComboBoxMultiselect.this.getOffsetHeight(); 998 | final int textInputTopOnPage = VComboBoxMultiselect.this.tb.getAbsoluteTop(); 999 | final int viewportOffset = Document.get() 1000 | .getScrollTop(); 1001 | final int textInputTopInViewport = textInputTopOnPage - viewportOffset; 1002 | final int textInputBottomInViewport = textInputTopInViewport + textInputHeight; 1003 | 1004 | final int spaceAboveInViewport = textInputTopInViewport; 1005 | final int spaceBelowInViewport = Window.getClientHeight() - textInputBottomInViewport; 1006 | 1007 | if (spaceBelowInViewport < offsetHeight && spaceBelowInViewport < spaceAboveInViewport) { 1008 | // popup on top of input instead 1009 | if (offsetHeight > spaceAboveInViewport) { 1010 | // Shrink popup height to fit above 1011 | offsetHeight = spaceAboveInViewport; 1012 | } 1013 | top = textInputTopOnPage - offsetHeight; 1014 | } else { 1015 | // Show below, position calculated in showSuggestions for some 1016 | // strange reason 1017 | top = this.topPosition; 1018 | offsetHeight = Math.min(offsetHeight, spaceBelowInViewport); 1019 | } 1020 | 1021 | // fetch real width (mac FF bugs here due GWT popups overflow:auto ) 1022 | offsetWidth = menuFirstChild.getOffsetWidth(); 1023 | 1024 | if (offsetHeight < desiredHeight) { 1025 | int menuHeight = offsetHeight; 1026 | if (this.isPagingEnabled) { 1027 | menuHeight -= this.up.getOffsetHeight() + this.down.getOffsetHeight() 1028 | + this.status.getOffsetHeight(); 1029 | } else { 1030 | final ComputedStyle s = new ComputedStyle(this.menu.getElement()); 1031 | menuHeight -= s.getIntProperty("marginBottom") + s.getIntProperty("marginTop"); 1032 | } 1033 | 1034 | // If the available page height is really tiny then this will be 1035 | // negative and an exception will be thrown on setHeight. 1036 | int menuElementHeight = this.menu.getItemOffsetHeight(); 1037 | if (menuHeight < menuElementHeight) { 1038 | menuHeight = menuElementHeight; 1039 | } 1040 | 1041 | this.menu.setHeight(menuHeight + "px"); 1042 | 1043 | if (VComboBoxMultiselect.this.suggestionPopupWidth == null) { 1044 | final int naturalMenuWidthPlusScrollBar = naturalMenuWidth + WidgetUtil.getNativeScrollbarSize(); 1045 | if (offsetWidth < naturalMenuWidthPlusScrollBar) { 1046 | this.menu.setWidth(naturalMenuWidthPlusScrollBar + "px"); 1047 | } 1048 | } 1049 | } 1050 | 1051 | if (offsetWidth + left > Window.getClientWidth()) { 1052 | left = VComboBoxMultiselect.this.getAbsoluteLeft() + VComboBoxMultiselect.this.getOffsetWidth() 1053 | - offsetWidth; 1054 | if (left < 0) { 1055 | left = 0; 1056 | this.menu.setWidth(Window.getClientWidth() + "px"); 1057 | 1058 | } 1059 | } 1060 | 1061 | setPopupPosition(left, top); 1062 | this.menu.scrollSelectionIntoView(); 1063 | } 1064 | 1065 | /** 1066 | * Adds in-line CSS rules to the DOM according to the 1067 | * suggestionPopupWidth field 1068 | * 1069 | * @param desiredWidth 1070 | * @param naturalMenuWidth 1071 | */ 1072 | private void updateMenuWidth(final int desiredWidth, int naturalMenuWidth) { 1073 | /** 1074 | * Three different width modes for the suggestion pop-up: 1075 | * 1076 | * 1. Legacy "null"-mode: width is determined by the longest item 1077 | * caption for each page while still maintaining minimum width of 1078 | * (desiredWidth - popupOuterPadding) 1079 | * 1080 | * 2. relative to the component itself 1081 | * 1082 | * 3. fixed width 1083 | */ 1084 | String width = "auto"; 1085 | if (VComboBoxMultiselect.this.suggestionPopupWidth == null) { 1086 | if (naturalMenuWidth < desiredWidth) { 1087 | naturalMenuWidth = desiredWidth - this.popupOuterPadding; 1088 | width = desiredWidth - this.popupOuterPadding + "px"; 1089 | } 1090 | } else if (isrelativeUnits(VComboBoxMultiselect.this.suggestionPopupWidth)) { 1091 | float mainComponentWidth = desiredWidth - this.popupOuterPadding; 1092 | // convert percentage value to fraction 1093 | int widthInPx = Math 1094 | .round(mainComponentWidth * asFraction(VComboBoxMultiselect.this.suggestionPopupWidth)); 1095 | width = widthInPx + "px"; 1096 | } else { 1097 | // use as fixed width CSS definition 1098 | width = WidgetUtil.escapeAttribute(VComboBoxMultiselect.this.suggestionPopupWidth); 1099 | } 1100 | this.menu.setWidth(width); 1101 | } 1102 | 1103 | /** 1104 | * Returns the percentage value as a fraction, e.g. 42% -> 0.42 1105 | * 1106 | * @param percentage 1107 | */ 1108 | private float asFraction(String percentage) { 1109 | String trimmed = percentage.trim(); 1110 | String withoutPercentSign = trimmed.substring(0, trimmed.length() - 1); 1111 | float asFraction = Float.parseFloat(withoutPercentSign) / 100; 1112 | return asFraction; 1113 | } 1114 | 1115 | /** 1116 | * @since 7.7 1117 | * @param suggestionPopupWidth 1118 | * @return 1119 | */ 1120 | private boolean isrelativeUnits(String suggestionPopupWidth) { 1121 | return suggestionPopupWidth.trim() 1122 | .endsWith("%"); 1123 | } 1124 | 1125 | /** 1126 | * Was the popup just closed? 1127 | * 1128 | * @return true if popup was just closed 1129 | */ 1130 | public boolean isJustClosed() { 1131 | debug("VComboBoxMultiselect.SP: justClosed()"); 1132 | final long now = new Date().getTime(); 1133 | return this.lastAutoClosed > 0 && now - this.lastAutoClosed < 200; 1134 | } 1135 | 1136 | /* 1137 | * (non-Javadoc) 1138 | * 1139 | * @see 1140 | * com.google.gwt.event.logical.shared.CloseHandler#onClose(com.google 1141 | * .gwt.event.logical.shared.CloseEvent) 1142 | */ 1143 | 1144 | @Override 1145 | public void onClose(CloseEvent event) { 1146 | debug("VComboBoxMultiselect.SP: onClose(" + event.isAutoClosed() + ")"); 1147 | 1148 | if (event.isAutoClosed()) { 1149 | this.lastAutoClosed = new Date().getTime(); 1150 | } 1151 | 1152 | connector.sendBlurEvent(); 1153 | } 1154 | 1155 | /** 1156 | * Updates style names in suggestion popup to help theme building. 1157 | * 1158 | * @param componentState 1159 | * shared state of the combo box 1160 | */ 1161 | public void updateStyleNames(AbstractComponentState componentState) { 1162 | debug("VComboBoxMultiselect.SP: updateStyleNames()"); 1163 | setStyleName(VComboBoxMultiselect.this.getStylePrimaryName() + "-suggestpopup"); 1164 | this.menu.setStyleName(VComboBoxMultiselect.this.getStylePrimaryName() + "-suggestmenu"); 1165 | this.status.setClassName(VComboBoxMultiselect.this.getStylePrimaryName() + "-status"); 1166 | if (ComponentStateUtil.hasStyles(componentState)) { 1167 | for (String style : componentState.styles) { 1168 | if (!"".equals(style)) { 1169 | addStyleDependentName(style); 1170 | } 1171 | } 1172 | } 1173 | } 1174 | 1175 | } 1176 | 1177 | /** 1178 | * The menu where the suggestions are rendered 1179 | */ 1180 | public class SuggestionMenu extends MenuBar implements SubPartAware, LoadHandler { 1181 | 1182 | private VLazyExecutor delayedImageLoadExecutioner = new VLazyExecutor(100, new ScheduledCommand() { 1183 | 1184 | @Override 1185 | public void execute() { 1186 | debug("VComboBoxMultiselect.SM: delayedImageLoadExecutioner()"); 1187 | if (VComboBoxMultiselect.this.suggestionPopup.isVisible() 1188 | && VComboBoxMultiselect.this.suggestionPopup.isAttached()) { 1189 | setWidth(""); 1190 | getElement().getFirstChildElement() 1191 | .getStyle() 1192 | .clearWidth(); 1193 | VComboBoxMultiselect.this.suggestionPopup 1194 | .setPopupPositionAndShow(VComboBoxMultiselect.this.suggestionPopup); 1195 | } 1196 | 1197 | } 1198 | }); 1199 | 1200 | /** 1201 | * Default constructor 1202 | */ 1203 | SuggestionMenu() { 1204 | super(true); 1205 | debug("VComboBoxMultiselect.SM: constructor()"); 1206 | addDomHandler(this, LoadEvent.getType()); 1207 | 1208 | setScrollEnabled(true); 1209 | } 1210 | 1211 | /** 1212 | * Fixes menus height to use same space as full page would use. Needed 1213 | * to avoid height changes when quickly "scrolling" to last page. 1214 | * 1215 | * @param pageItemsCount 1216 | * height items count 1217 | */ 1218 | public void fixHeightTo(int pageItemsCount) { 1219 | setHeight(getPreferredHeight(pageItemsCount)); 1220 | } 1221 | 1222 | /* 1223 | * Gets the preferred height of the menu including pageItemsCount items. 1224 | */ 1225 | String getPreferredHeight(int pageItemsCount) { 1226 | if (VComboBoxMultiselect.this.currentSuggestions.size() > 0) { 1227 | final int pixels = getPreferredHeight() / VComboBoxMultiselect.this.currentSuggestions.size() 1228 | * pageItemsCount; 1229 | return pixels + "px"; 1230 | } else { 1231 | return ""; 1232 | } 1233 | } 1234 | 1235 | /** 1236 | * Sets the suggestions rendered in the menu. 1237 | * 1238 | * @param suggestions 1239 | * The suggestions to be rendered in the menu 1240 | */ 1241 | public void setSuggestions(Collection suggestions) { 1242 | debug("VComboBoxMultiselect.SM: setSuggestions(" + suggestions + ")"); 1243 | 1244 | clearItems(); 1245 | 1246 | if (VComboBoxMultiselect.this.showClearButton) { 1247 | MenuItem clearMenuItem = new MenuItem(VComboBoxMultiselect.this.clearButtonCaption, false, 1248 | VComboBoxMultiselect.this.clearCmd); 1249 | clearMenuItem.getElement() 1250 | .setId(DOM.createUniqueId()); 1251 | clearMenuItem.addStyleName("align-center"); 1252 | Property.LABEL.set(clearMenuItem.getElement(), VComboBoxMultiselect.this.clearButtonCaption); 1253 | this.addItem(clearMenuItem); 1254 | } 1255 | 1256 | if (VComboBoxMultiselect.this.showSelectAllButton) { 1257 | MenuItem selectAllMenuItem = new MenuItem(VComboBoxMultiselect.this.selectAllButtonCaption, false, 1258 | VComboBoxMultiselect.this.selectAllCmd); 1259 | selectAllMenuItem.getElement() 1260 | .setId(DOM.createUniqueId()); 1261 | selectAllMenuItem.addStyleName("align-center"); 1262 | Property.LABEL.set(selectAllMenuItem.getElement(), VComboBoxMultiselect.this.selectAllButtonCaption); 1263 | this.addItem(selectAllMenuItem); 1264 | } 1265 | 1266 | final Iterator it = suggestions.iterator(); 1267 | int currentSuggestionIndex = VComboBoxMultiselect.this.currentPage * VComboBoxMultiselect.this.pageLength; 1268 | 1269 | while (it.hasNext()) { 1270 | final ComboBoxMultiselectSuggestion suggestion = it.next(); 1271 | final MenuItem mi = new MenuItem(suggestion.getDisplayString(), true, suggestion); 1272 | 1273 | String style = suggestion.getStyle(); 1274 | if (style != null) { 1275 | mi.addStyleName("v-filterselect-item-" + style); 1276 | } 1277 | Roles.getListitemRole() 1278 | .set(mi.getElement()); 1279 | 1280 | WidgetUtil.sinkOnloadForImages(mi.getElement()); 1281 | 1282 | boolean isSelected = VComboBoxMultiselect.this.selectedOptionKeys != null 1283 | && VComboBoxMultiselect.this.selectedOptionKeys.contains(suggestion.getOptionKey()); 1284 | 1285 | suggestion.setChecked(isSelected); 1286 | mi.getElement() 1287 | .insertFirst(suggestion.getCheckBox() 1288 | .getElement()); 1289 | 1290 | Property.LABEL.set(mi.getElement(), suggestion.getAriaLabel()); 1291 | Property.SETSIZE.set(mi.getElement(), getTotalSuggestions()); 1292 | Property.POSINSET.set(mi.getElement(), ++currentSuggestionIndex); 1293 | State.CHECKED.set(mi.getElement(), CheckedValue.of(isSelected)); 1294 | 1295 | this.addItem(mi); 1296 | 1297 | } 1298 | 1299 | VComboBoxMultiselect.this.suggestionPopup.selectFirstItem(); 1300 | } 1301 | 1302 | /** 1303 | * Create/select a suggestion based on the used entered string. This 1304 | * method is called after filtering has completed with the given string. 1305 | * 1306 | * @param enteredItemValue 1307 | * user entered string 1308 | */ 1309 | public void actOnEnteredValueAfterFiltering(String enteredItemValue) { 1310 | debug("VComboBoxMultiselect.SM: doPostFilterSelectedItemAction()"); 1311 | final MenuItem item = getSelectedItem(); 1312 | 1313 | // check for exact match in menu 1314 | int p = getItems().size(); 1315 | if (p > 0) { 1316 | for (int i = 0; i < p; i++) { 1317 | final MenuItem potentialExactMatch = getItems().get(i); 1318 | if (potentialExactMatch.getText() 1319 | .equals(enteredItemValue)) { 1320 | selectItem(potentialExactMatch); 1321 | // do not send a value change event if null was and 1322 | // stays selected 1323 | if (!"".equals(enteredItemValue) || VComboBoxMultiselect.this.selectedOptionKeys != null 1324 | && !VComboBoxMultiselect.this.selectedOptionKeys.isEmpty()) { 1325 | doItemAction(potentialExactMatch, true); 1326 | } 1327 | return; 1328 | } 1329 | } 1330 | } 1331 | if (VComboBoxMultiselect.this.allowNewItems) { 1332 | if (!enteredItemValue.equals(VComboBoxMultiselect.this.lastNewItemString)) { 1333 | // Store last sent new item string to avoid double sends 1334 | VComboBoxMultiselect.this.lastNewItemString = enteredItemValue; 1335 | VComboBoxMultiselect.this.connector.sendNewItem(enteredItemValue); 1336 | // TODO try to select the new value if it matches what was 1337 | // sent for V7 compatibility 1338 | } 1339 | } else if (item != null && !"".equals(VComboBoxMultiselect.this.lastFilter) && item.getText() 1340 | .toLowerCase() 1341 | .contains(VComboBoxMultiselect.this.lastFilter.toLowerCase())) { 1342 | doItemAction(item, true); 1343 | } else { 1344 | // currentSuggestion has key="" for nullselection 1345 | if (VComboBoxMultiselect.this.currentSuggestion != null 1346 | && !"".equals(VComboBoxMultiselect.this.currentSuggestion.key)) { 1347 | // An item (not null) selected 1348 | String text = VComboBoxMultiselect.this.currentSuggestion.getReplacementString(); 1349 | setText(text); 1350 | VComboBoxMultiselect.this.selectedOptionKeys.add(VComboBoxMultiselect.this.currentSuggestion.key); 1351 | } 1352 | } 1353 | } 1354 | 1355 | private static final String SUBPART_PREFIX = "item"; 1356 | 1357 | @Override 1358 | public com.google.gwt.user.client.Element getSubPartElement(String subPart) { 1359 | int index = Integer.parseInt(subPart.substring(SUBPART_PREFIX.length())); 1360 | 1361 | MenuItem item = getItems().get(index); 1362 | 1363 | return item.getElement(); 1364 | } 1365 | 1366 | @Override 1367 | public String getSubPartName(com.google.gwt.user.client.Element subElement) { 1368 | if (!getElement().isOrHasChild(subElement)) { 1369 | return null; 1370 | } 1371 | 1372 | Element menuItemRoot = subElement; 1373 | while (menuItemRoot != null && !menuItemRoot.getTagName() 1374 | .equalsIgnoreCase("td")) { 1375 | menuItemRoot = menuItemRoot.getParentElement() 1376 | .cast(); 1377 | } 1378 | // "menuItemRoot" is now the root of the menu item 1379 | 1380 | final int itemCount = getItems().size(); 1381 | for (int i = 0; i < itemCount; i++) { 1382 | if (getItems().get(i) 1383 | .getElement() == menuItemRoot) { 1384 | String name = SUBPART_PREFIX + i; 1385 | return name; 1386 | } 1387 | } 1388 | return null; 1389 | } 1390 | 1391 | @Override 1392 | public void onLoad(LoadEvent event) { 1393 | debug("VComboBoxMultiselect.SM: onLoad()"); 1394 | // Handle icon onload events to ensure shadow is resized 1395 | // correctly 1396 | this.delayedImageLoadExecutioner.trigger(); 1397 | 1398 | } 1399 | 1400 | /** 1401 | * @deprecated use {@link SuggestionPopup#selectFirstItem()} instead. 1402 | */ 1403 | @Deprecated 1404 | public void selectFirstItem() { 1405 | debug("VComboBoxMultiselect.SM: selectFirstItem()"); 1406 | MenuItem firstItem = getItems().get(0); 1407 | selectItem(firstItem); 1408 | } 1409 | 1410 | /** 1411 | * @deprecated use {@link SuggestionPopup#selectLastItem()} instead. 1412 | */ 1413 | @Deprecated 1414 | public void selectLastItem() { 1415 | debug("VComboBoxMultiselect.SM: selectLastItem()"); 1416 | List items = getItems(); 1417 | MenuItem lastItem = items.get(items.size() - 1); 1418 | selectItem(lastItem); 1419 | } 1420 | 1421 | /* 1422 | * Gets the height of one menu item. 1423 | */ 1424 | int getItemOffsetHeight() { 1425 | List items = getItems(); 1426 | return items != null && items.size() > 0 ? items.get(0) 1427 | .getOffsetHeight() : 0; 1428 | } 1429 | 1430 | /* 1431 | * Gets the width of one menu item. 1432 | */ 1433 | int getItemOffsetWidth() { 1434 | List items = getItems(); 1435 | return items != null && items.size() > 0 ? items.get(0) 1436 | .getOffsetWidth() : 0; 1437 | } 1438 | 1439 | /** 1440 | * Returns true if the scroll is active on the menu element or if the 1441 | * menu currently displays the last page with less items then the 1442 | * maximum visibility (in which case the scroll is not active, but the 1443 | * scroll is active for any other page in general). 1444 | * 1445 | * @since 7.2.6 1446 | */ 1447 | @Override 1448 | public boolean isScrollActive() { 1449 | String height = getElement().getStyle() 1450 | .getHeight(); 1451 | String preferredHeight = getPreferredHeight(VComboBoxMultiselect.this.pageLength); 1452 | 1453 | return !(height == null || height.length() == 0 || height.equals(preferredHeight)); 1454 | } 1455 | 1456 | /** 1457 | * Highlight (select) an item matching the current text box content 1458 | * without triggering its action. 1459 | */ 1460 | public void highlightSelectedItem() { 1461 | int p = getItems().size(); 1462 | // first check if there is a key match to handle items with 1463 | // identical captions 1464 | String currentKey = VComboBoxMultiselect.this.currentSuggestion != null 1465 | ? VComboBoxMultiselect.this.currentSuggestion.getOptionKey() : ""; 1466 | for (int i = 0; i < p; i++) { 1467 | final MenuItem potentialExactMatch = getItems().get(i); 1468 | if (currentKey.equals(getSuggestionKey(potentialExactMatch)) && VComboBoxMultiselect.this.tb.getText() 1469 | .equals(potentialExactMatch.getText())) { 1470 | selectItem(potentialExactMatch); 1471 | VComboBoxMultiselect.this.tb.setSelectionRange(VComboBoxMultiselect.this.tb.getText() 1472 | .length(), 0); 1473 | return; 1474 | } 1475 | } 1476 | // then check for exact string match in menu 1477 | String text = VComboBoxMultiselect.this.tb.getText(); 1478 | for (int i = 0; i < p; i++) { 1479 | final MenuItem potentialExactMatch = getItems().get(i); 1480 | if (potentialExactMatch.getText() 1481 | .equals(text)) { 1482 | selectItem(potentialExactMatch); 1483 | VComboBoxMultiselect.this.tb.setSelectionRange(VComboBoxMultiselect.this.tb.getText() 1484 | .length(), 0); 1485 | return; 1486 | } 1487 | } 1488 | } 1489 | } 1490 | 1491 | private String getSuggestionKey(MenuItem item) { 1492 | if (item != null && item.getCommand() != null && item.getCommand() instanceof ComboBoxMultiselectSuggestion) { 1493 | return ((ComboBoxMultiselectSuggestion) item.getCommand()).getOptionKey(); 1494 | } 1495 | return ""; 1496 | } 1497 | 1498 | /** 1499 | * TextBox variant used as input element for filter selects, which prevents 1500 | * selecting text when disabled. 1501 | * 1502 | * @since 7.1.5 1503 | */ 1504 | public class FilterSelectTextBox extends TextBox { 1505 | 1506 | /** 1507 | * Creates a new filter select text box. 1508 | * 1509 | * @since 7.6.4 1510 | */ 1511 | public FilterSelectTextBox() { 1512 | /*- 1513 | * Stop the browser from showing its own suggestion popup. 1514 | * 1515 | * Using an invalid value instead of "off" as suggested by 1516 | * https://developer.mozilla.org/en-US/docs/Web/Security/Securing_your_site/Turning_off_form_autocompletion 1517 | * 1518 | * Leaving the non-standard Safari options autocapitalize and 1519 | * autocorrect untouched since those do not interfere in the same 1520 | * way, and they might be useful in a combo box where new items are 1521 | * allowed. 1522 | */ 1523 | getElement().setAttribute("autocomplete", "nope"); 1524 | } 1525 | 1526 | /** 1527 | * Overridden to avoid selecting text when text input is disabled 1528 | */ 1529 | @Override 1530 | public void setSelectionRange(int pos, int length) { 1531 | if (VComboBoxMultiselect.this.textInputEnabled) { 1532 | /* 1533 | * set selection range with a backwards direction: anchor at the 1534 | * back, focus at the front. This means that items that are too 1535 | * long to display will display from the start and not the end 1536 | * even on Firefox. 1537 | * 1538 | * We need the JSNI function to set selection range so that we 1539 | * can use the optional direction attribute to set the anchor to 1540 | * the end and the focus to the start. This makes Firefox work 1541 | * the same way as other browsers (#13477) 1542 | */ 1543 | WidgetUtil.setSelectionRange(getElement(), pos, length, "backward"); 1544 | 1545 | } else { 1546 | /* 1547 | * Setting the selectionrange for an uneditable textbox leads to 1548 | * unwanted behaviour when the width of the textbox is narrower 1549 | * than the width of the entry: the end of the entry is shown 1550 | * instead of the beginning. (see #13477) 1551 | * 1552 | * To avoid this, we set the caret to the beginning of the line. 1553 | */ 1554 | 1555 | super.setSelectionRange(0, 0); 1556 | } 1557 | } 1558 | 1559 | } 1560 | 1561 | /** 1562 | * Handler receiving notifications from the connector and updating the 1563 | * widget state accordingly. 1564 | * 1565 | * This class is still subject to change and should not be considered as 1566 | * public stable API. 1567 | * 1568 | * @since 8.0 1569 | */ 1570 | public class DataReceivedHandler { 1571 | 1572 | private Runnable navigationCallback = null; 1573 | /** 1574 | * Set true when popupopened has been clicked. Cleared on each 1575 | * UIDL-update. This handles the special case where are not filtering 1576 | * yet and the selected value has changed on the server-side. See #2119 1577 | *

1578 | * For internal use only. May be removed or replaced in the future. 1579 | */ 1580 | private boolean popupOpenerClicked = false; 1581 | /** For internal use only. May be removed or replaced in the future. */ 1582 | private boolean waitingForFilteringResponse = false; 1583 | private boolean initialData = true; 1584 | private String pendingUserInput = null; 1585 | private boolean showPopup = false; 1586 | private boolean blurUpdate = false; 1587 | 1588 | /** 1589 | * Called by the connector when new data for the last requested filter 1590 | * is received from the server. 1591 | */ 1592 | public void dataReceived() { 1593 | if (this.initialData || this.blurUpdate) { 1594 | VComboBoxMultiselect.this.suggestionPopup.menu 1595 | .setSuggestions(VComboBoxMultiselect.this.currentSuggestions); 1596 | performSelection(VComboBoxMultiselect.this.serverSelectedKeys, true, true); 1597 | updateSuggestionPopupMinWidth(); 1598 | updateRootWidth(); 1599 | this.initialData = false; 1600 | return; 1601 | } 1602 | 1603 | VComboBoxMultiselect.this.suggestionPopup.menu.setSuggestions(VComboBoxMultiselect.this.currentSuggestions); 1604 | if (!this.waitingForFilteringResponse && VComboBoxMultiselect.this.suggestionPopup.isAttached()) { 1605 | this.showPopup = true; 1606 | } 1607 | if (this.showPopup) { 1608 | VComboBoxMultiselect.this.suggestionPopup.showSuggestions(VComboBoxMultiselect.this.currentPage); 1609 | if (VComboBoxMultiselect.this.currentSuggestion != null 1610 | && VComboBoxMultiselect.this.currentSuggestions != null) { 1611 | 1612 | VComboBoxMultiselect.this.suggestionPopup 1613 | .selectItemAtIndex(VComboBoxMultiselect.this.currentSuggestions 1614 | .indexOf(VComboBoxMultiselect.this.currentSuggestion)); 1615 | } 1616 | } 1617 | 1618 | this.waitingForFilteringResponse = false; 1619 | 1620 | if (this.pendingUserInput != null) { 1621 | VComboBoxMultiselect.this.suggestionPopup.menu.actOnEnteredValueAfterFiltering(this.pendingUserInput); 1622 | this.pendingUserInput = null; 1623 | } else if (this.popupOpenerClicked) { 1624 | // make sure the current item is selected in the popup 1625 | VComboBoxMultiselect.this.suggestionPopup.menu.highlightSelectedItem(); 1626 | } else { 1627 | navigateItemAfterPageChange(); 1628 | } 1629 | 1630 | this.popupOpenerClicked = false; 1631 | } 1632 | 1633 | /** 1634 | * Perform filtering with the user entered string and when the results 1635 | * are received, perform any action appropriate for the user input 1636 | * (select an item or create a new one). 1637 | * 1638 | * @param value 1639 | * user input 1640 | */ 1641 | public void reactOnInputWhenReady(String value) { 1642 | this.pendingUserInput = value; 1643 | this.showPopup = false; 1644 | filterOptions(0, value); 1645 | } 1646 | 1647 | /* 1648 | * This method navigates to the proper item in the combobox page. This 1649 | * should be executed after setSuggestions() method which is called from 1650 | * VComboBoxMultiselect.showSuggestions(). ShowSuggestions() method 1651 | * builds the page content. As far as setSuggestions() method is called 1652 | * as deferred, navigateItemAfterPageChange method should be also be 1653 | * called as deferred. #11333 1654 | */ 1655 | private void navigateItemAfterPageChange() { 1656 | if (this.navigationCallback != null) { 1657 | // navigationCallback is not reset here but after any server 1658 | // request in case you are in between two requests both changing 1659 | // the page back and forth 1660 | 1661 | // we're paging w/ arrows 1662 | this.navigationCallback.run(); 1663 | this.navigationCallback = null; 1664 | } 1665 | } 1666 | 1667 | /** 1668 | * Called by the connector any pending navigation operations should be 1669 | * cleared. 1670 | */ 1671 | public void clearPendingNavigation() { 1672 | this.navigationCallback = null; 1673 | } 1674 | 1675 | /** 1676 | * Set a callback that is invoked when a page change occurs if there 1677 | * have not been intervening requests to the server. The callback is 1678 | * reset when any additional request is made to the server. 1679 | * 1680 | * @param callback 1681 | * method to call after filtering has completed 1682 | */ 1683 | public void setNavigationCallback(Runnable callback) { 1684 | this.showPopup = true; 1685 | this.navigationCallback = callback; 1686 | } 1687 | 1688 | /** 1689 | * Record that the popup opener has been clicked and the popup should be 1690 | * opened on the next request. 1691 | * 1692 | * This handles the special case where are not filtering yet and the 1693 | * selected value has changed on the server-side. See #2119. The flag is 1694 | * cleared on each server reply. 1695 | */ 1696 | public void popupOpenerClicked() { 1697 | this.popupOpenerClicked = true; 1698 | this.showPopup = true; 1699 | } 1700 | 1701 | /** 1702 | * Cancel a pending request to perform post-filtering actions. 1703 | */ 1704 | private void cancelPendingPostFiltering() { 1705 | this.pendingUserInput = null; 1706 | } 1707 | 1708 | /** 1709 | * Called by the connector when it has finished handling any reply from 1710 | * the server, regardless of what was updated. 1711 | */ 1712 | public void serverReplyHandled() { 1713 | this.popupOpenerClicked = false; 1714 | VComboBoxMultiselect.this.lastNewItemString = null; 1715 | 1716 | // if (!initDone) { 1717 | // debug("VComboBoxMultiselect: init done, updating widths"); 1718 | // // Calculate minimum textarea width 1719 | // updateSuggestionPopupMinWidth(); 1720 | // updateRootWidth(); 1721 | // initDone = true; 1722 | // } 1723 | } 1724 | 1725 | /** 1726 | * For internal use only - this method will be removed in the future. 1727 | * 1728 | * @return true if the combo box is waiting for a reply from the server 1729 | * with a new page of data, false otherwise 1730 | */ 1731 | public boolean isWaitingForFilteringResponse() { 1732 | return this.waitingForFilteringResponse; 1733 | } 1734 | 1735 | /** 1736 | * For internal use only - this method will be removed in the future. 1737 | * 1738 | * @return true if the combo box is waiting for initial data from the 1739 | * server, false otherwise 1740 | */ 1741 | public boolean isWaitingForInitialData() { 1742 | return this.initialData; 1743 | } 1744 | 1745 | /** 1746 | * Set a flag that filtering of options is pending a response from the 1747 | * server. 1748 | */ 1749 | private void startWaitingForFilteringResponse() { 1750 | this.waitingForFilteringResponse = true; 1751 | } 1752 | 1753 | /** 1754 | * Perform selection (if appropriate) based on a reply from the server. 1755 | * When this method is called, the suggestions have been reset if new 1756 | * ones (different from the previous list) were received from the 1757 | * server. 1758 | * 1759 | * @param selectedKeys 1760 | * new selected keys or null if none given by the server 1761 | * @param selectedCaption 1762 | * new selected item caption if sent by the server or null - 1763 | * this is used when the selected item is not on the current 1764 | * page 1765 | */ 1766 | public void updateSelectionFromServer(Set selectedKeys, String selectedCaption) { 1767 | boolean oldSuggestionTextMatchTheOldSelection = VComboBoxMultiselect.this.currentSuggestion != null 1768 | && VComboBoxMultiselect.this.currentSuggestion.getReplacementString() 1769 | .equals(VComboBoxMultiselect.this.tb.getText()); 1770 | 1771 | // VComboBoxMultiselect.this.serverSelectedKeys = selectedKeys; 1772 | VComboBoxMultiselect.this.serverSelectedKeys.clear(); 1773 | if (selectedKeys != null) { 1774 | for (String selectedKey : selectedKeys) { 1775 | VComboBoxMultiselect.this.serverSelectedKeys.add(selectedKey); 1776 | } 1777 | } 1778 | 1779 | performSelection( selectedKeys, oldSuggestionTextMatchTheOldSelection, 1780 | !isWaitingForFilteringResponse() || this.popupOpenerClicked); 1781 | 1782 | cancelPendingPostFiltering(); 1783 | 1784 | if (!VComboBoxMultiselect.this.suggestionPopup.isShowing()) { 1785 | setSelectedCaption(selectedCaption); 1786 | } 1787 | } 1788 | 1789 | public void setBlurUpdate(boolean blurUpdate) { 1790 | this.blurUpdate = blurUpdate; 1791 | } 1792 | 1793 | } 1794 | 1795 | // TODO decide whether this should change - affects themes and v7 1796 | public static final String CLASSNAME = "v-filterselect"; 1797 | private static final String STYLE_NO_INPUT = "no-input"; 1798 | 1799 | /** For internal use only. May be removed or replaced in the future. */ 1800 | public int pageLength; 1801 | 1802 | /** For internal use only. May be removed or replaced in the future. */ 1803 | public String clearButtonCaption = "clear"; 1804 | 1805 | /** For internal use only. May be removed or replaced in the future. */ 1806 | public boolean showClearButton; 1807 | 1808 | /** For internal use only. May be removed or replaced in the future. */ 1809 | public String selectAllButtonCaption = "select all"; 1810 | 1811 | /** For internal use only. May be removed or replaced in the future. */ 1812 | public boolean showSelectAllButton; 1813 | 1814 | /** For internal use only. May be removed or replaced in the future. */ 1815 | Command clearCmd = new Command() { 1816 | 1817 | @Override 1818 | public void execute() { 1819 | debug("VFS: clearCmd()"); 1820 | 1821 | String filter = VComboBoxMultiselect.this.tb.getText(); 1822 | VComboBoxMultiselect.this.connector.clear(filter); 1823 | 1824 | setText(""); 1825 | filterOptions(0, ""); 1826 | } 1827 | }; 1828 | 1829 | /** For internal use only. May be removed or replaced in the future. */ 1830 | Command selectAllCmd = new Command() { 1831 | 1832 | @Override 1833 | public void execute() { 1834 | debug("VFS: selectAllCmd()"); 1835 | String filter = VComboBoxMultiselect.this.tb.getText(); 1836 | VComboBoxMultiselect.this.connector.selectAll(filter); 1837 | 1838 | setText(""); 1839 | filterOptions(0, ""); 1840 | } 1841 | }; 1842 | 1843 | private boolean enableDebug = false; 1844 | 1845 | private final FlowPanel panel = new FlowPanel(); 1846 | 1847 | /** 1848 | * The text box where the filter is written 1849 | *

1850 | * For internal use only. May be removed or replaced in the future. 1851 | */ 1852 | public final TextBox tb; 1853 | 1854 | /** For internal use only. May be removed or replaced in the future. */ 1855 | public final SuggestionPopup suggestionPopup; 1856 | 1857 | /** 1858 | * Used when measuring the width of the popup 1859 | */ 1860 | private final HTML popupOpener = new HTML(""); 1861 | 1862 | private class IconWidget extends Widget { 1863 | IconWidget(Icon icon) { 1864 | setElement(icon.getElement()); 1865 | } 1866 | } 1867 | 1868 | private IconWidget selectedItemIcon; 1869 | 1870 | /** For internal use only. May be removed or replaced in the future. */ 1871 | public ComboBoxMultiselectConnector connector; 1872 | 1873 | /** For internal use only. May be removed or replaced in the future. */ 1874 | public int currentPage; 1875 | 1876 | /** 1877 | * A collection of available suggestions (options) as received from the 1878 | * server. 1879 | *

1880 | * For internal use only. May be removed or replaced in the future. 1881 | */ 1882 | public final List currentSuggestions = new ArrayList<>(); 1883 | 1884 | /** For internal use only. May be removed or replaced in the future. */ 1885 | public Set serverSelectedKeys = new LinkedHashSet<>(); 1886 | /** For internal use only. May be removed or replaced in the future. */ 1887 | public Set selectedOptionKeys = new LinkedHashSet<>(); 1888 | 1889 | /** For internal use only. May be removed or replaced in the future. */ 1890 | public boolean initDone = false; 1891 | 1892 | /** For internal use only. May be removed or replaced in the future. */ 1893 | public String lastFilter = ""; 1894 | 1895 | /** 1896 | * The current suggestion selected from the dropdown. This is one of the 1897 | * values in currentSuggestions except when filtering, in this case 1898 | * currentSuggestion might not be in currentSuggestions. 1899 | *

1900 | * For internal use only. May be removed or replaced in the future. 1901 | */ 1902 | public ComboBoxMultiselectSuggestion currentSuggestion; 1903 | 1904 | /** For internal use only. May be removed or replaced in the future. */ 1905 | public boolean allowNewItems; 1906 | 1907 | /** Total number of suggestions, excluding null selection item. */ 1908 | private int totalSuggestions; 1909 | 1910 | /** For internal use only. May be removed or replaced in the future. */ 1911 | public boolean enabled; 1912 | 1913 | /** For internal use only. May be removed or replaced in the future. */ 1914 | public boolean readonly; 1915 | 1916 | /** For internal use only. May be removed or replaced in the future. */ 1917 | public String inputPrompt = ""; 1918 | 1919 | /** For internal use only. May be removed or replaced in the future. */ 1920 | public int suggestionPopupMinWidth = 0; 1921 | 1922 | public String suggestionPopupWidth = null; 1923 | 1924 | private int popupWidth = -1; 1925 | /** 1926 | * Stores the last new item string to avoid double submissions. Cleared on 1927 | * uidl updates. 1928 | *

1929 | * For internal use only. May be removed or replaced in the future. 1930 | */ 1931 | public String lastNewItemString; 1932 | 1933 | /** For internal use only. May be removed or replaced in the future. */ 1934 | public boolean focused = false; 1935 | 1936 | /** 1937 | * If set to false, the component should not allow entering text to the 1938 | * field even for filtering. 1939 | */ 1940 | private boolean textInputEnabled = true; 1941 | 1942 | private final DataReceivedHandler dataReceivedHandler = new DataReceivedHandler(); 1943 | 1944 | /** 1945 | * Default constructor. 1946 | */ 1947 | public VComboBoxMultiselect() { 1948 | this.tb = createTextBox(); 1949 | this.suggestionPopup = createSuggestionPopup(); 1950 | 1951 | this.popupOpener.addMouseDownHandler(VComboBoxMultiselect.this); 1952 | Roles.getButtonRole() 1953 | .setAriaHiddenState(this.popupOpener.getElement(), true); 1954 | Roles.getButtonRole() 1955 | .set(this.popupOpener.getElement()); 1956 | 1957 | this.panel.add(this.tb); 1958 | this.panel.add(this.popupOpener); 1959 | initWidget(this.panel); 1960 | Roles.getComboboxRole() 1961 | .set(this.panel.getElement()); 1962 | 1963 | this.tb.addKeyDownHandler(this); 1964 | this.tb.addKeyUpHandler(this); 1965 | 1966 | this.tb.addFocusHandler(this); 1967 | this.tb.addBlurHandler(this); 1968 | 1969 | this.panel.addDomHandler(this, ClickEvent.getType()); 1970 | 1971 | setStyleName(CLASSNAME); 1972 | 1973 | sinkEvents(Event.ONPASTE); 1974 | } 1975 | 1976 | private static double getMarginBorderPaddingWidth(Element element) { 1977 | final ComputedStyle s = new ComputedStyle(element); 1978 | return s.getMarginWidth() + s.getBorderWidth() + s.getPaddingWidth(); 1979 | 1980 | } 1981 | 1982 | /* 1983 | * (non-Javadoc) 1984 | * 1985 | * @see 1986 | * com.google.gwt.user.client.ui.Composite#onBrowserEvent(com.google.gwt 1987 | * .user.client.Event) 1988 | */ 1989 | @Override 1990 | public void onBrowserEvent(Event event) { 1991 | super.onBrowserEvent(event); 1992 | 1993 | if (event.getTypeInt() == Event.ONPASTE) { 1994 | if (this.textInputEnabled) { 1995 | filterOptions(this.currentPage); 1996 | } 1997 | } 1998 | } 1999 | 2000 | /** 2001 | * This method will create the TextBox used by the VComboBoxMultiselect 2002 | * instance. It is invoked during the Constructor and should only be 2003 | * overridden if a custom TextBox shall be used. The overriding method 2004 | * cannot use any instance variables. 2005 | * 2006 | * @since 7.1.5 2007 | * @return TextBox instance used by this VComboBoxMultiselect 2008 | */ 2009 | protected TextBox createTextBox() { 2010 | return new FilterSelectTextBox(); 2011 | } 2012 | 2013 | /** 2014 | * This method will create the SuggestionPopup used by the 2015 | * VComboBoxMultiselect instance. It is invoked during the Constructor and 2016 | * should only be overridden if a custom SuggestionPopup shall be used. The 2017 | * overriding method cannot use any instance variables. 2018 | * 2019 | * @since 7.1.5 2020 | * @return SuggestionPopup instance used by this VComboBoxMultiselect 2021 | */ 2022 | protected SuggestionPopup createSuggestionPopup() { 2023 | return new SuggestionPopup(); 2024 | } 2025 | 2026 | @Override 2027 | public void setStyleName(String style) { 2028 | super.setStyleName(style); 2029 | updateStyleNames(); 2030 | } 2031 | 2032 | @Override 2033 | public void setStylePrimaryName(String style) { 2034 | super.setStylePrimaryName(style); 2035 | updateStyleNames(); 2036 | } 2037 | 2038 | protected void updateStyleNames() { 2039 | this.tb.setStyleName(getStylePrimaryName() + "-input"); 2040 | this.popupOpener.setStyleName(getStylePrimaryName() + "-button"); 2041 | this.suggestionPopup.setStyleName(getStylePrimaryName() + "-suggestpopup"); 2042 | } 2043 | 2044 | /** 2045 | * Does the Select have more pages? 2046 | * 2047 | * @return true if a next page exists, else false if the current page is the 2048 | * last page 2049 | */ 2050 | public boolean hasNextPage() { 2051 | return this.pageLength > 0 && getTotalSuggestions() > (this.currentPage + 1) * this.pageLength; 2052 | } 2053 | 2054 | /** 2055 | * Filters the options at a certain page. Uses the text box input as a 2056 | * filter and ensures the popup is opened when filtering results are 2057 | * available. 2058 | * 2059 | * @param page 2060 | * The page which items are to be filtered 2061 | */ 2062 | public void filterOptions(int page) { 2063 | this.dataReceivedHandler.popupOpenerClicked(); 2064 | filterOptions(page, this.tb.getText()); 2065 | } 2066 | 2067 | /** 2068 | * Filters the options at certain page using the given filter. 2069 | * 2070 | * @param page 2071 | * The page to filter 2072 | * @param filter 2073 | * The filter to apply to the components 2074 | */ 2075 | public void filterOptions(int page, String filter) { 2076 | debug("VComboBoxMultiselect: filterOptions(" + page + ", " + filter + ")"); 2077 | 2078 | if (filter.equals(this.lastFilter) && this.currentPage == page && this.suggestionPopup.isAttached()) { 2079 | // already have the page 2080 | this.dataReceivedHandler.dataReceived(); 2081 | return; 2082 | } 2083 | 2084 | if (!filter.equals(this.lastFilter)) { 2085 | // when filtering, let the server decide the page unless we've 2086 | // set the filter to empty and explicitly said that we want to see 2087 | // the results starting from page 0. 2088 | if ("".equals(filter) && page != 0) { 2089 | // let server decide 2090 | page = -1; 2091 | } else { 2092 | page = 0; 2093 | } 2094 | } 2095 | 2096 | this.dataReceivedHandler.startWaitingForFilteringResponse(); 2097 | this.connector.requestPage(page, filter); 2098 | 2099 | this.lastFilter = filter; 2100 | 2101 | // If the data was updated from cache, the page has been updated too, if 2102 | // not, update 2103 | if (this.dataReceivedHandler.isWaitingForFilteringResponse()) { 2104 | this.currentPage = page; 2105 | } 2106 | } 2107 | 2108 | /** For internal use only. May be removed or replaced in the future. */ 2109 | public void updateReadOnly() { 2110 | debug("VComboBoxMultiselect: updateReadOnly()"); 2111 | this.tb.setReadOnly(this.readonly || !this.textInputEnabled); 2112 | } 2113 | 2114 | public void setTextInputAllowed(boolean textInputAllowed) { 2115 | debug("VComboBoxMultiselect: setTextInputAllowed()"); 2116 | // Always update styles as they might have been overwritten 2117 | if (textInputAllowed) { 2118 | removeStyleDependentName(STYLE_NO_INPUT); 2119 | Roles.getTextboxRole() 2120 | .removeAriaReadonlyProperty(this.tb.getElement()); 2121 | } else { 2122 | addStyleDependentName(STYLE_NO_INPUT); 2123 | Roles.getTextboxRole() 2124 | .setAriaReadonlyProperty(this.tb.getElement(), true); 2125 | } 2126 | 2127 | if (this.textInputEnabled == textInputAllowed) { 2128 | return; 2129 | } 2130 | 2131 | this.textInputEnabled = textInputAllowed; 2132 | updateReadOnly(); 2133 | } 2134 | 2135 | /** 2136 | * Sets the text in the text box. 2137 | * 2138 | * @param text 2139 | * the text to set in the text box 2140 | */ 2141 | public void setText(final String text) { 2142 | /** 2143 | * To leave caret in the beginning of the line. SetSelectionRange 2144 | * wouldn't work on IE (see #13477) 2145 | */ 2146 | Direction previousDirection = this.tb.getDirection(); 2147 | this.tb.setDirection(Direction.RTL); 2148 | this.tb.setText(text); 2149 | this.tb.setDirection(previousDirection); 2150 | } 2151 | 2152 | /** 2153 | * Set or reset the placeholder attribute for the text field. 2154 | * 2155 | * @param placeholder 2156 | * new placeholder string or null for none 2157 | */ 2158 | public void setPlaceholder(String placeholder) { 2159 | this.inputPrompt = placeholder; 2160 | updatePlaceholder(); 2161 | } 2162 | 2163 | /** 2164 | * Update placeholder visibility (hidden when read-only or disabled). 2165 | */ 2166 | public void updatePlaceholder() { 2167 | if (this.inputPrompt != null && this.enabled && !this.readonly) { 2168 | this.tb.getElement() 2169 | .setAttribute("placeholder", this.inputPrompt); 2170 | } else { 2171 | this.tb.getElement() 2172 | .removeAttribute("placeholder"); 2173 | } 2174 | } 2175 | 2176 | /** 2177 | * Triggered when a suggestion is selected. 2178 | * 2179 | * @param suggestion 2180 | * The suggestion that just got selected. 2181 | */ 2182 | public void onSuggestionSelected(ComboBoxMultiselectSuggestion suggestion) { 2183 | debug("VComboBoxMultiselect: onSuggestionSelected(" + suggestion.caption + ": " + suggestion.key + ")"); 2184 | 2185 | this.dataReceivedHandler.cancelPendingPostFiltering(); 2186 | 2187 | this.currentSuggestion = suggestion; 2188 | String newKey = suggestion.getOptionKey(); 2189 | 2190 | if (!this.selectedOptionKeys.contains(newKey)) { 2191 | this.selectedOptionKeys.add(newKey); 2192 | this.connector.sendSelections(new HashSet<>(Arrays.asList(newKey)), new HashSet<>()); 2193 | } else { 2194 | this.selectedOptionKeys.remove(newKey); 2195 | this.connector.sendSelections(new HashSet<>(), new HashSet<>(Arrays.asList(newKey))); 2196 | } 2197 | } 2198 | 2199 | /** 2200 | * Perform selection based on a message from the server. 2201 | * 2202 | * The special case where the selected item is not on the current page is 2203 | * handled separately by the caller. 2204 | * 2205 | * @param selectedKeys 2206 | * non-empty selected item keys 2207 | * @param forceUpdateText 2208 | * true to force the text box value to match the suggestion text 2209 | * @param updatePromptAndSelectionIfMatchFound 2210 | */ 2211 | private void performSelection(Set selectedKeys, boolean forceUpdateText, 2212 | boolean updatePromptAndSelectionIfMatchFound) { 2213 | this.selectedOptionKeys = selectedKeys; 2214 | 2215 | // some item selected 2216 | for (ComboBoxMultiselectSuggestion suggestion : this.currentSuggestions) { 2217 | String suggestionKey = suggestion.getOptionKey(); 2218 | if (selectedKeys == null || !selectedKeys.contains(suggestionKey)) { 2219 | continue; 2220 | } 2221 | // at this point, suggestion key matches the new selection key 2222 | if (updatePromptAndSelectionIfMatchFound && !this.selectedOptionKeys.contains(suggestionKey) 2223 | || suggestion.getReplacementString() 2224 | .equals(this.tb.getText()) 2225 | || forceUpdateText) { 2226 | this.selectedOptionKeys.add(suggestionKey); 2227 | } 2228 | } 2229 | } 2230 | 2231 | private void forceReflow() { 2232 | WidgetUtil.setStyleTemporarily(this.tb.getElement(), "zoom", "1"); 2233 | } 2234 | 2235 | /** 2236 | * Positions the icon vertically in the middle. Should be called after the 2237 | * icon has loaded 2238 | */ 2239 | private void updateSelectedIconPosition() { 2240 | // Position icon vertically to middle 2241 | int availableHeight = 0; 2242 | availableHeight = getOffsetHeight(); 2243 | 2244 | int iconHeight = WidgetUtil.getRequiredHeight(this.selectedItemIcon); 2245 | int marginTop = (availableHeight - iconHeight) / 2; 2246 | this.selectedItemIcon.getElement() 2247 | .getStyle() 2248 | .setMarginTop(marginTop, Unit.PX); 2249 | } 2250 | 2251 | private static Set navigationKeyCodes = new HashSet<>(); 2252 | static { 2253 | navigationKeyCodes.add(KeyCodes.KEY_DOWN); 2254 | navigationKeyCodes.add(KeyCodes.KEY_UP); 2255 | navigationKeyCodes.add(KeyCodes.KEY_PAGEDOWN); 2256 | navigationKeyCodes.add(KeyCodes.KEY_PAGEUP); 2257 | navigationKeyCodes.add(KeyCodes.KEY_ENTER); 2258 | } 2259 | 2260 | /* 2261 | * (non-Javadoc) 2262 | * 2263 | * @see 2264 | * com.google.gwt.event.dom.client.KeyDownHandler#onKeyDown(com.google.gwt 2265 | * .event.dom.client.KeyDownEvent) 2266 | */ 2267 | 2268 | @Override 2269 | public void onKeyDown(KeyDownEvent event) { 2270 | if (this.enabled && !this.readonly) { 2271 | int keyCode = event.getNativeKeyCode(); 2272 | 2273 | debug("VComboBoxMultiselect: key down: " + keyCode); 2274 | 2275 | if (this.dataReceivedHandler.isWaitingForFilteringResponse() && navigationKeyCodes.contains(keyCode) 2276 | && (!this.allowNewItems || keyCode != KeyCodes.KEY_ENTER)) { 2277 | /* 2278 | * Keyboard navigation events should not be handled while we are 2279 | * waiting for a response. This avoids flickering, disappearing 2280 | * items, wrongly interpreted responses and more. 2281 | */ 2282 | debug("Ignoring " + keyCode + " because we are waiting for a filtering response"); 2283 | 2284 | DOM.eventPreventDefault(DOM.eventGetCurrentEvent()); 2285 | event.stopPropagation(); 2286 | return; 2287 | } 2288 | 2289 | if (this.suggestionPopup.isAttached()) { 2290 | debug("Keycode " + keyCode + " target is popup"); 2291 | popupKeyDown(event); 2292 | } else { 2293 | debug("Keycode " + keyCode + " target is text field"); 2294 | inputFieldKeyDown(event); 2295 | } 2296 | } 2297 | } 2298 | 2299 | private void debug(String string) { 2300 | if (this.enableDebug) { 2301 | VConsole.error(string); 2302 | } 2303 | } 2304 | 2305 | /** 2306 | * Triggered when a key is pressed in the text box 2307 | * 2308 | * @param event 2309 | * The KeyDownEvent 2310 | */ 2311 | private void inputFieldKeyDown(KeyDownEvent event) { 2312 | debug("VComboBoxMultiselect: inputFieldKeyDown(" + event.getNativeKeyCode() + ")"); 2313 | 2314 | switch (event.getNativeKeyCode()) { 2315 | case KeyCodes.KEY_DOWN: 2316 | case KeyCodes.KEY_UP: 2317 | case KeyCodes.KEY_PAGEDOWN: 2318 | case KeyCodes.KEY_PAGEUP: 2319 | // open popup as from gadget 2320 | filterOptions(-1, ""); 2321 | this.tb.selectAll(); 2322 | this.dataReceivedHandler.popupOpenerClicked(); 2323 | break; 2324 | case KeyCodes.KEY_ENTER: 2325 | /* 2326 | * This only handles the case when new items is allowed, a text is 2327 | * entered, the popup opener button is clicked to close the popup 2328 | * and enter is then pressed (see #7560). 2329 | */ 2330 | if (!this.allowNewItems) { 2331 | return; 2332 | } 2333 | 2334 | if (this.currentSuggestion != null && this.tb.getText() 2335 | .equals(this.currentSuggestion.getReplacementString())) { 2336 | // Retain behavior from #6686 by returning without stopping 2337 | // propagation if there's nothing to do 2338 | return; 2339 | } 2340 | this.dataReceivedHandler.reactOnInputWhenReady(this.tb.getText()); 2341 | 2342 | event.stopPropagation(); 2343 | break; 2344 | } 2345 | 2346 | } 2347 | 2348 | /** 2349 | * Triggered when a key was pressed in the suggestion popup. 2350 | * 2351 | * @param event 2352 | * The KeyDownEvent of the key 2353 | */ 2354 | private void popupKeyDown(KeyDownEvent event) { 2355 | debug("VComboBoxMultiselect: popupKeyDown(" + event.getNativeKeyCode() + ")"); 2356 | 2357 | // Propagation of handled events is stopped so other handlers such as 2358 | // shortcut key handlers do not also handle the same events. 2359 | switch (event.getNativeKeyCode()) { 2360 | case KeyCodes.KEY_DOWN: 2361 | this.suggestionPopup.selectNextItem(); 2362 | 2363 | DOM.eventPreventDefault(DOM.eventGetCurrentEvent()); 2364 | event.stopPropagation(); 2365 | break; 2366 | case KeyCodes.KEY_UP: 2367 | this.suggestionPopup.selectPrevItem(); 2368 | 2369 | DOM.eventPreventDefault(DOM.eventGetCurrentEvent()); 2370 | event.stopPropagation(); 2371 | break; 2372 | case KeyCodes.KEY_PAGEDOWN: 2373 | selectNextPage(); 2374 | event.stopPropagation(); 2375 | break; 2376 | case KeyCodes.KEY_PAGEUP: 2377 | selectPrevPage(); 2378 | event.stopPropagation(); 2379 | break; 2380 | case KeyCodes.KEY_ESCAPE: 2381 | reset(); 2382 | DOM.eventPreventDefault(DOM.eventGetCurrentEvent()); 2383 | event.stopPropagation(); 2384 | break; 2385 | case KeyCodes.KEY_TAB: 2386 | case KeyCodes.KEY_ENTER: 2387 | 2388 | // queue this, may be cancelled by selection 2389 | int selectedIndex = this.suggestionPopup.menu.getSelectedIndex(); 2390 | if (!this.allowNewItems && selectedIndex != -1) { 2391 | 2392 | debug("index before: " + selectedIndex); 2393 | if (this.showClearButton) { 2394 | selectedIndex = selectedIndex - 1; 2395 | } 2396 | if (this.showSelectAllButton) { 2397 | selectedIndex = selectedIndex - 1; 2398 | } 2399 | 2400 | debug("index after: " + selectedIndex); 2401 | if (selectedIndex == -2) { 2402 | this.clearCmd.execute(); 2403 | } else if (selectedIndex == -1) { 2404 | if (this.showSelectAllButton) { 2405 | this.selectAllCmd.execute(); 2406 | } else { 2407 | this.clearCmd.execute(); 2408 | } 2409 | } 2410 | 2411 | debug("entered suggestion: " + this.currentSuggestions.get(selectedIndex).caption); 2412 | onSuggestionSelected(this.currentSuggestions.get(selectedIndex)); 2413 | } else { 2414 | this.dataReceivedHandler.reactOnInputWhenReady(this.tb.getText()); 2415 | } 2416 | 2417 | event.stopPropagation(); 2418 | break; 2419 | } 2420 | } 2421 | 2422 | /* 2423 | * Show the prev page. 2424 | */ 2425 | private void selectPrevPage() { 2426 | if (this.currentPage > 0) { 2427 | this.dataReceivedHandler.setNavigationCallback(() -> this.suggestionPopup.selectLastItem()); 2428 | filterOptions(this.currentPage - 1, this.lastFilter); 2429 | } 2430 | } 2431 | 2432 | /* 2433 | * Show the next page. 2434 | */ 2435 | private void selectNextPage() { 2436 | if (hasNextPage()) { 2437 | this.dataReceivedHandler.setNavigationCallback(() -> this.suggestionPopup.selectFirstItem()); 2438 | filterOptions(this.currentPage + 1, this.lastFilter); 2439 | } 2440 | } 2441 | 2442 | /** 2443 | * Triggered when a key was depressed. 2444 | * 2445 | * @param event 2446 | * The KeyUpEvent of the key depressed 2447 | */ 2448 | @Override 2449 | public void onKeyUp(KeyUpEvent event) { 2450 | debug("VComboBoxMultiselect: onKeyUp(" + event.getNativeKeyCode() + ")"); 2451 | 2452 | if (this.enabled && !this.readonly) { 2453 | switch (event.getNativeKeyCode()) { 2454 | case KeyCodes.KEY_ENTER: 2455 | case KeyCodes.KEY_TAB: 2456 | case KeyCodes.KEY_SHIFT: 2457 | case KeyCodes.KEY_CTRL: 2458 | case KeyCodes.KEY_ALT: 2459 | case KeyCodes.KEY_DOWN: 2460 | case KeyCodes.KEY_UP: 2461 | case KeyCodes.KEY_PAGEDOWN: 2462 | case KeyCodes.KEY_PAGEUP: 2463 | case KeyCodes.KEY_ESCAPE: 2464 | // NOP 2465 | break; 2466 | default: 2467 | if (this.textInputEnabled) { 2468 | // when filtering, we always want to see the results on the 2469 | // first page first. 2470 | filterOptions(0); 2471 | } 2472 | break; 2473 | } 2474 | } 2475 | } 2476 | 2477 | /** 2478 | * Resets the ComboBoxMultiselect to its initial state. 2479 | */ 2480 | private void reset() { 2481 | debug("VComboBoxMultiselect: reset()"); 2482 | 2483 | // just fetch selected information from state 2484 | String text = this.connector.getState().selectedItemsCaption; 2485 | setText(text == null ? "" : text); 2486 | this.selectedOptionKeys = this.connector.getState().selectedItemKeys; 2487 | if (this.selectedOptionKeys == null || this.selectedOptionKeys.isEmpty()) { 2488 | this.selectedOptionKeys = null; 2489 | updatePlaceholder(); 2490 | } 2491 | this.currentSuggestion = null; // #13217 2492 | // else { 2493 | // this.currentSuggestion = this.currentSuggestions.stream() 2494 | // .filter(suggestion -> 2495 | // this.selectedOptionKeys.contains(suggestion.getOptionKey())) 2496 | // .findAny() 2497 | // .orElse(null); 2498 | // } 2499 | 2500 | this.suggestionPopup.hide(); 2501 | } 2502 | 2503 | /** 2504 | * Listener for popupopener. 2505 | */ 2506 | @Override 2507 | public void onClick(ClickEvent event) { 2508 | debug("VComboBoxMultiselect: onClick()"); 2509 | if (this.enabled && !this.readonly) { 2510 | getDataReceivedHandler().blurUpdate = false; 2511 | // ask suggestionPopup if it was just closed, we are using GWT 2512 | // Popup's auto close feature 2513 | if (!this.suggestionPopup.isJustClosed()) { 2514 | filterOptions(-1, ""); 2515 | this.dataReceivedHandler.popupOpenerClicked(); 2516 | } 2517 | DOM.eventPreventDefault(DOM.eventGetCurrentEvent()); 2518 | focus(); 2519 | setText(""); 2520 | } 2521 | } 2522 | 2523 | /** 2524 | * Update minimum width for combo box textarea based on input prompt and 2525 | * suggestions. 2526 | *

2527 | * For internal use only. May be removed or replaced in the future. 2528 | */ 2529 | public void updateSuggestionPopupMinWidth() { 2530 | debug("VComboBoxMultiselect: updateSuggestionPopupMinWidth()"); 2531 | 2532 | // used only to calculate minimum width 2533 | String captions = WidgetUtil.escapeHTML(this.inputPrompt); 2534 | 2535 | for (ComboBoxMultiselectSuggestion suggestion : this.currentSuggestions) { 2536 | // Collect captions so we can calculate minimum width for 2537 | // textarea 2538 | if (captions.length() > 0) { 2539 | captions += "|"; 2540 | } 2541 | captions += WidgetUtil.escapeHTML(suggestion.getReplacementString()); 2542 | } 2543 | 2544 | // Calculate minimum textarea width 2545 | this.suggestionPopupMinWidth = minWidth(captions); 2546 | } 2547 | 2548 | /** 2549 | * Calculate minimum width for FilterSelect textarea. 2550 | *

2551 | * For internal use only. May be removed or replaced in the future. 2552 | * 2553 | * @param captions 2554 | * pipe separated string listing all the captions to measure 2555 | * @return minimum width in pixels 2556 | */ 2557 | public native int minWidth(String captions) 2558 | /*-{ 2559 | if(!captions || captions.length <= 0) 2560 | return 0; 2561 | captions = captions.split("|"); 2562 | var d = $wnd.document.createElement("div"); 2563 | var html = ""; 2564 | for(var i=0; i < captions.length; i++) { 2565 | html += "

" + captions[i] + "
"; 2566 | // TODO apply same CSS classname as in suggestionmenu 2567 | } 2568 | d.style.position = "absolute"; 2569 | d.style.top = "0"; 2570 | d.style.left = "0"; 2571 | d.style.visibility = "hidden"; 2572 | d.innerHTML = html; 2573 | $wnd.document.body.appendChild(d); 2574 | var w = d.offsetWidth; 2575 | $wnd.document.body.removeChild(d); 2576 | return w; 2577 | }-*/; 2578 | 2579 | /** 2580 | * A flag which prevents a focus event from taking place. 2581 | */ 2582 | boolean iePreventNextFocus = false; 2583 | 2584 | /* 2585 | * (non-Javadoc) 2586 | * 2587 | * @see 2588 | * com.google.gwt.event.dom.client.FocusHandler#onFocus(com.google.gwt.event 2589 | * .dom.client.FocusEvent) 2590 | */ 2591 | 2592 | @Override 2593 | public void onFocus(FocusEvent event) { 2594 | debug("VComboBoxMultiselect: onFocus()"); 2595 | 2596 | /* 2597 | * When we disable a blur event in ie we need to refocus the textfield. 2598 | * This will cause a focus event we do not want to process, so in that 2599 | * case we just ignore it. 2600 | */ 2601 | if (BrowserInfo.get() 2602 | .isIE() && this.iePreventNextFocus) { 2603 | this.iePreventNextFocus = false; 2604 | return; 2605 | } 2606 | 2607 | this.focused = true; 2608 | updatePlaceholder(); 2609 | addStyleDependentName("focus"); 2610 | 2611 | this.connector.sendFocusEvent(); 2612 | 2613 | this.connector.getConnection() 2614 | .getVTooltip() 2615 | .showAssistive(this.connector.getTooltipInfo(getElement())); 2616 | } 2617 | 2618 | /** 2619 | * A flag which cancels the blur event and sets the focus back to the 2620 | * textfield if the Browser is IE. 2621 | */ 2622 | boolean preventNextBlurEventInIE = false; 2623 | 2624 | private String explicitSelectedCaption; 2625 | 2626 | /* 2627 | * (non-Javadoc) 2628 | * 2629 | * @see 2630 | * com.google.gwt.event.dom.client.BlurHandler#onBlur(com.google.gwt.event 2631 | * .dom.client.BlurEvent) 2632 | */ 2633 | 2634 | @Override 2635 | public void onBlur(BlurEvent event) { 2636 | debug("VComboBoxMultiselect: onBlur()"); 2637 | 2638 | if (BrowserInfo.get() 2639 | .isIE() && this.preventNextBlurEventInIE) { 2640 | /* 2641 | * Clicking in the suggestion popup or on the popup button in IE 2642 | * causes a blur event to be sent for the field. In other browsers 2643 | * this is prevented by canceling/preventing default behavior for 2644 | * the focus event, in IE we handle it here by refocusing the text 2645 | * field and ignoring the resulting focus event for the textfield 2646 | * (in onFocus). 2647 | */ 2648 | this.preventNextBlurEventInIE = false; 2649 | 2650 | Element focusedElement = WidgetUtil.getFocusedElement(); 2651 | if (getElement().isOrHasChild(focusedElement) || this.suggestionPopup.getElement() 2652 | .isOrHasChild(focusedElement)) { 2653 | 2654 | // IF the suggestion popup or another part of the 2655 | // VComboBoxMultiselect 2656 | // was focused, move the focus back to the textfield and prevent 2657 | // the triggered focus event (in onFocus). 2658 | this.iePreventNextFocus = true; 2659 | this.tb.setFocus(true); 2660 | return; 2661 | } 2662 | } 2663 | 2664 | this.focused = false; 2665 | updatePlaceholder(); 2666 | removeStyleDependentName("focus"); 2667 | 2668 | // Send new items when clicking out with the mouse. 2669 | if (!this.readonly) { 2670 | if (this.textInputEnabled && this.allowNewItems && (this.currentSuggestion == null || this.tb.getText() 2671 | .equals(this.currentSuggestion.getReplacementString()))) { 2672 | this.dataReceivedHandler.reactOnInputWhenReady(this.tb.getText()); 2673 | } else { 2674 | reset(); 2675 | } 2676 | this.suggestionPopup.hide(); 2677 | } 2678 | 2679 | this.connector.sendBlurEvent(); 2680 | } 2681 | 2682 | /* 2683 | * (non-Javadoc) 2684 | * 2685 | * @see com.vaadin.client.Focusable#focus() 2686 | */ 2687 | 2688 | @Override 2689 | public void focus() { 2690 | debug("VComboBoxMultiselect: focus()"); 2691 | this.focused = true; 2692 | updatePlaceholder(); 2693 | this.tb.setFocus(true); 2694 | } 2695 | 2696 | /** 2697 | * Calculates the width of the select if the select has undefined width. 2698 | * Should be called when the width changes or when the icon changes. 2699 | *

2700 | * For internal use only. May be removed or replaced in the future. 2701 | */ 2702 | public void updateRootWidth() { 2703 | debug("VComboBoxMultiselect: updateRootWidth()"); 2704 | 2705 | if (this.connector.isUndefinedWidth()) { 2706 | 2707 | /* 2708 | * When the select has a undefined with we need to check that we are 2709 | * only setting the text box width relative to the first page width 2710 | * of the items. If this is not done the text box width will change 2711 | * when the popup is used to view longer items than the text box is 2712 | * wide. 2713 | */ 2714 | int w = WidgetUtil.getRequiredWidth(this); 2715 | 2716 | if (this.dataReceivedHandler.isWaitingForInitialData() && this.suggestionPopupMinWidth > w) { 2717 | /* 2718 | * We want to compensate for the paddings just to preserve the 2719 | * exact size as in Vaadin 6.x, but we get here before 2720 | * MeasuredSize has been initialized. 2721 | * Util.measureHorizontalPaddingAndBorder does not work with 2722 | * border-box, so we must do this the hard way. 2723 | */ 2724 | Style style = getElement().getStyle(); 2725 | String originalPadding = style.getPadding(); 2726 | String originalBorder = style.getBorderWidth(); 2727 | style.setPaddingLeft(0, Unit.PX); 2728 | style.setBorderWidth(0, Unit.PX); 2729 | style.setProperty("padding", originalPadding); 2730 | style.setProperty("borderWidth", originalBorder); 2731 | 2732 | // Use util.getRequiredWidth instead of getOffsetWidth here 2733 | 2734 | int iconWidth = this.selectedItemIcon == null ? 0 : WidgetUtil.getRequiredWidth(this.selectedItemIcon); 2735 | int buttonWidth = this.popupOpener == null ? 0 : WidgetUtil.getRequiredWidth(this.popupOpener); 2736 | 2737 | /* 2738 | * Instead of setting the width of the wrapper, set the width of 2739 | * the combobox. Subtract the width of the icon and the 2740 | * popupopener 2741 | */ 2742 | 2743 | this.tb.setWidth(this.suggestionPopupMinWidth - iconWidth - buttonWidth + "px"); 2744 | } 2745 | 2746 | /* 2747 | * Lock the textbox width to its current value if it's not already 2748 | * locked. This can happen after setWidth("") which resets the 2749 | * textbox width to "100%". 2750 | */ 2751 | if (!this.tb.getElement() 2752 | .getStyle() 2753 | .getWidth() 2754 | .endsWith("px")) { 2755 | int iconWidth = this.selectedItemIcon == null ? 0 : this.selectedItemIcon.getOffsetWidth(); 2756 | this.tb.setWidth(this.tb.getOffsetWidth() - iconWidth + "px"); 2757 | } 2758 | } 2759 | } 2760 | 2761 | /** 2762 | * Get the width of the select in pixels where the text area and icon has 2763 | * been included. 2764 | * 2765 | * @return The width in pixels 2766 | */ 2767 | private int getMainWidth() { 2768 | return getOffsetWidth(); 2769 | } 2770 | 2771 | @Override 2772 | public void setWidth(String width) { 2773 | super.setWidth(width); 2774 | if (width.length() != 0) { 2775 | this.tb.setWidth("100%"); 2776 | } 2777 | } 2778 | 2779 | /** 2780 | * Handles special behavior of the mouse down event. 2781 | * 2782 | * @param event 2783 | */ 2784 | private void handleMouseDownEvent(Event event) { 2785 | /* 2786 | * Prevent the keyboard focus from leaving the textfield by preventing 2787 | * the default behaviour of the browser. Fixes #4285. 2788 | */ 2789 | if (event.getTypeInt() == Event.ONMOUSEDOWN) { 2790 | debug("VComboBoxMultiselect: blocking mouseDown event to avoid blur"); 2791 | 2792 | event.preventDefault(); 2793 | event.stopPropagation(); 2794 | 2795 | /* 2796 | * In IE the above wont work, the blur event will still trigger. So, 2797 | * we set a flag here to prevent the next blur event from happening. 2798 | * This is not needed if do not already have focus, in that case 2799 | * there will not be any blur event and we should not cancel the 2800 | * next blur. 2801 | */ 2802 | if (BrowserInfo.get() 2803 | .isIE() && this.focused) { 2804 | this.preventNextBlurEventInIE = true; 2805 | debug("VComboBoxMultiselect: Going to prevent next blur event on IE"); 2806 | } 2807 | } 2808 | } 2809 | 2810 | @Override 2811 | public void onMouseDown(MouseDownEvent event) { 2812 | debug("VComboBoxMultiselect.onMouseDown(): blocking mouseDown event to avoid blur"); 2813 | 2814 | event.preventDefault(); 2815 | event.stopPropagation(); 2816 | 2817 | /* 2818 | * In IE the above wont work, the blur event will still trigger. So, we 2819 | * set a flag here to prevent the next blur event from happening. This 2820 | * is not needed if do not already have focus, in that case there will 2821 | * not be any blur event and we should not cancel the next blur. 2822 | */ 2823 | if (BrowserInfo.get() 2824 | .isIE() && this.focused) { 2825 | this.preventNextBlurEventInIE = true; 2826 | debug("VComboBoxMultiselect: Going to prevent next blur event on IE"); 2827 | } 2828 | } 2829 | 2830 | @Override 2831 | protected void onDetach() { 2832 | super.onDetach(); 2833 | this.suggestionPopup.hide(); 2834 | } 2835 | 2836 | @Override 2837 | public com.google.gwt.user.client.Element getSubPartElement(String subPart) { 2838 | String[] parts = subPart.split("/"); 2839 | if ("textbox".equals(parts[0])) { 2840 | return this.tb.getElement(); 2841 | } else if ("button".equals(parts[0])) { 2842 | return this.popupOpener.getElement(); 2843 | } else if ("popup".equals(parts[0]) && this.suggestionPopup.isAttached()) { 2844 | if (parts.length == 2) { 2845 | return this.suggestionPopup.menu.getSubPartElement(parts[1]); 2846 | } 2847 | return this.suggestionPopup.getElement(); 2848 | } 2849 | return null; 2850 | } 2851 | 2852 | @Override 2853 | public String getSubPartName(com.google.gwt.user.client.Element subElement) { 2854 | if (this.tb.getElement() 2855 | .isOrHasChild(subElement)) { 2856 | return "textbox"; 2857 | } else if (this.popupOpener.getElement() 2858 | .isOrHasChild(subElement)) { 2859 | return "button"; 2860 | } else if (this.suggestionPopup.getElement() 2861 | .isOrHasChild(subElement)) { 2862 | return "popup"; 2863 | } 2864 | return null; 2865 | } 2866 | 2867 | @Override 2868 | public void setAriaRequired(boolean required) { 2869 | AriaHelper.handleInputRequired(this.tb, required); 2870 | } 2871 | 2872 | @Override 2873 | public void setAriaInvalid(boolean invalid) { 2874 | AriaHelper.handleInputInvalid(this.tb, invalid); 2875 | } 2876 | 2877 | @Override 2878 | public void bindAriaCaption(com.google.gwt.user.client.Element captionElement) { 2879 | AriaHelper.bindCaption(this.tb, captionElement); 2880 | } 2881 | 2882 | @Override 2883 | public boolean isWorkPending() { 2884 | return this.dataReceivedHandler.isWaitingForFilteringResponse() 2885 | || this.suggestionPopup.lazyPageScroller.isRunning(); 2886 | } 2887 | 2888 | /** 2889 | * Sets the caption of selected item, if "scroll to page" is disabled. This 2890 | * method is meant for internal use and may change in future versions. 2891 | * 2892 | * @since 7.7 2893 | * @param selectedCaption 2894 | * the caption of selected item 2895 | */ 2896 | public void setSelectedCaption(String selectedCaption) { 2897 | this.explicitSelectedCaption = selectedCaption; 2898 | if (selectedCaption != null) { 2899 | setText(selectedCaption); 2900 | } 2901 | } 2902 | 2903 | /** 2904 | * This method is meant for internal use and may change in future versions. 2905 | * 2906 | * @since 7.7 2907 | * @return the caption of selected item, if "scroll to page" is disabled 2908 | */ 2909 | public String getSelectedCaption() { 2910 | return this.explicitSelectedCaption; 2911 | } 2912 | 2913 | /** 2914 | * Returns a handler receiving notifications from the connector about 2915 | * communications. 2916 | * 2917 | * @return the dataReceivedHandler 2918 | */ 2919 | public DataReceivedHandler getDataReceivedHandler() { 2920 | return this.dataReceivedHandler; 2921 | } 2922 | 2923 | /** 2924 | * Sets the number of items to show per page, or 0 for showing all items. 2925 | * 2926 | * @param pageLength 2927 | * new page length or 0 for all items 2928 | */ 2929 | public void setPageLength(int pageLength) { 2930 | this.pageLength = pageLength; 2931 | } 2932 | 2933 | /** 2934 | * Sets the caption of the clear button. 2935 | * 2936 | * @param clearButtonCaption 2937 | * caption of the clear button 2938 | */ 2939 | public void setClearButtonCaption(String clearButtonCaption) { 2940 | this.clearButtonCaption = clearButtonCaption; 2941 | } 2942 | 2943 | /** 2944 | * Sets the caption of the selectAll button. 2945 | * 2946 | * @param selectAllButtonCaption 2947 | * caption of the selectAll button 2948 | */ 2949 | public void setSelectAllButtonCaption(String selectAllButtonCaption) { 2950 | this.selectAllButtonCaption = selectAllButtonCaption; 2951 | } 2952 | 2953 | /** 2954 | * Sets the clear button visible. 2955 | * 2956 | * @param showClearButton 2957 | * visible 2958 | */ 2959 | public void setShowClearButton(boolean showClearButton) { 2960 | this.showClearButton = showClearButton; 2961 | } 2962 | 2963 | /** 2964 | * Sets the select all button visible. 2965 | * 2966 | * @param showSelectAllButton 2967 | * visible 2968 | */ 2969 | public void setShowSelectAllButton(boolean showSelectAllButton) { 2970 | this.showSelectAllButton = showSelectAllButton; 2971 | } 2972 | 2973 | /** 2974 | * Sets the suggestion pop-up's width as a CSS string. By using relative 2975 | * units (e.g. "50%") it's possible to set the popup's width relative to the 2976 | * ComboBoxMultiselect itself. 2977 | * 2978 | * @param suggestionPopupWidth 2979 | * new popup width as CSS string, null for old default width 2980 | * calculation based on items 2981 | */ 2982 | public void setSuggestionPopupWidth(String suggestionPopupWidth) { 2983 | this.suggestionPopupWidth = suggestionPopupWidth; 2984 | } 2985 | 2986 | /** 2987 | * Sets whether creation of new items when there is no match is allowed or 2988 | * not. 2989 | * 2990 | * @param allowNewItems 2991 | * true to allow creation of new items, false to only allow 2992 | * selection of existing items 2993 | */ 2994 | public void setAllowNewItems(boolean allowNewItems) { 2995 | this.allowNewItems = allowNewItems; 2996 | } 2997 | 2998 | /** 2999 | * Sets the total number of suggestions. 3000 | *

3001 | * NOTE: this excluded the possible null selection item! 3002 | *

3003 | * NOTE: this just updates the state, but doesn't update any UI. 3004 | * 3005 | * @since 8.0 3006 | * @param totalSuggestions 3007 | * total number of suggestions 3008 | */ 3009 | public void setTotalSuggestions(int totalSuggestions) { 3010 | this.totalSuggestions = totalSuggestions; 3011 | } 3012 | 3013 | /** 3014 | * Gets the total number of suggestions, excluding the null selection item. 3015 | * 3016 | * @since 8.0 3017 | * @return total number of suggestions 3018 | */ 3019 | public int getTotalSuggestions() { 3020 | return this.totalSuggestions; 3021 | } 3022 | 3023 | } 3024 | --------------------------------------------------------------------------------