001/* 002 * #%L 003 * GwtMaterial 004 * %% 005 * Copyright (C) 2015 - 2017 GwtMaterialDesign 006 * %% 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, software 014 * distributed under the License is distributed on an "AS IS" BASIS, 015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 016 * See the License for the specific language governing permissions and 017 * limitations under the License. 018 * #L% 019 */ 020package gwt.material.design.addins.client.combobox; 021 022import com.google.gwt.core.client.GWT; 023import com.google.gwt.core.client.Scheduler; 024import com.google.gwt.dom.client.Document; 025import com.google.gwt.dom.client.Style; 026import com.google.gwt.event.logical.shared.*; 027import com.google.gwt.event.shared.HandlerRegistration; 028import com.google.gwt.user.client.ui.Widget; 029import gwt.material.design.addins.client.MaterialAddins; 030import gwt.material.design.addins.client.base.constants.AddinsCssName; 031import gwt.material.design.addins.client.combobox.base.HasUnselectItemHandler; 032import gwt.material.design.addins.client.combobox.events.ComboBoxEvents; 033import gwt.material.design.addins.client.combobox.events.SelectItemEvent; 034import gwt.material.design.addins.client.combobox.events.UnselectItemEvent; 035import gwt.material.design.addins.client.combobox.js.JsComboBox; 036import gwt.material.design.addins.client.combobox.js.JsComboBoxOptions; 037import gwt.material.design.addins.client.combobox.js.LanguageOptions; 038import gwt.material.design.client.MaterialDesignBase; 039import gwt.material.design.client.base.*; 040import gwt.material.design.client.base.mixin.EnabledMixin; 041import gwt.material.design.client.base.mixin.ErrorMixin; 042import gwt.material.design.client.base.mixin.ReadOnlyMixin; 043import gwt.material.design.client.base.mixin.WavesMixin; 044import gwt.material.design.client.constants.CssName; 045import gwt.material.design.client.ui.MaterialLabel; 046import gwt.material.design.client.ui.html.Label; 047import gwt.material.design.client.ui.html.OptGroup; 048import gwt.material.design.client.ui.html.Option; 049import gwt.material.design.jquery.client.api.JQueryElement; 050 051import java.util.*; 052 053import static gwt.material.design.addins.client.combobox.js.JsComboBox.$; 054 055//@formatter:off 056 057/** 058 * ComboBox component used on chat module 059 * <p> 060 * <h3>XML Namespace Declaration</h3> 061 * <pre> 062 * {@code 063 * xmlns:ma='urn:import:gwt.material.design.addins.client' 064 * } 065 * </pre> 066 * <p> 067 * <h3>UiBinder Usage:</h3> 068 * <pre> 069 * {@code 070 * <combobox:MaterialComboBox> 071 * <m:html.Option value="1" text="Sample 1"/> 072 * <m:html.Option value="2" text="Sample 2"/> 073 * <m:html.Option value="3" text="Sample 3"/> 074 * </combobox:MaterialComboBox> 075 * } 076 * </pre> 077 * 078 * @author kevzlou7979 079 * @author Ben Dol 080 * @see <a href="http://gwtmaterialdesign.github.io/gwt-material-demo/#combobox">Material ComboBox</a> 081 * @see <a href="https://github.com/select2/select2">Select2 4.0.3</a> 082 */ 083//@formatter:on 084public class MaterialComboBox<T> extends AbstractValueWidget<List<T>> implements JsLoader, HasPlaceholder, 085 HasOpenHandlers<T>, HasCloseHandlers<T>, HasUnselectItemHandler<T>, HasReadOnly { 086 087 static { 088 if (MaterialAddins.isDebug()) { 089 MaterialDesignBase.injectDebugJs(MaterialComboBoxDebugClientBundle.INSTANCE.select2DebugJs()); 090 MaterialDesignBase.injectCss(MaterialComboBoxDebugClientBundle.INSTANCE.select2DebugCss()); 091 } else { 092 MaterialDesignBase.injectJs(MaterialComboBoxClientBundle.INSTANCE.select2Js()); 093 MaterialDesignBase.injectCss(MaterialComboBoxClientBundle.INSTANCE.select2Css()); 094 } 095 } 096 097 098 private int selectedIndex; 099 private boolean suppressChangeEvent; 100 protected List<T> values = new ArrayList<>(); 101 private Label label = new Label(); 102 private MaterialLabel errorLabel = new MaterialLabel(); 103 protected MaterialWidget listbox = new MaterialWidget(Document.get().createSelectElement()); 104 private KeyFactory<T, String> keyFactory = Object::toString; 105 private JsComboBoxOptions options = JsComboBoxOptions.create(); 106 107 private ErrorMixin<AbstractValueWidget, MaterialLabel> errorMixin; 108 private ReadOnlyMixin<MaterialComboBox, MaterialWidget> readOnlyMixin; 109 private EnabledMixin<MaterialWidget> enabledMixin; 110 private WavesMixin<MaterialWidget> wavesMixin; 111 112 public MaterialComboBox() { 113 super(Document.get().createDivElement(), CssName.INPUT_FIELD, AddinsCssName.COMBOBOX); 114 } 115 116 @Override 117 protected void onLoad() { 118 label.setInitialClasses(AddinsCssName.SELECT2LABEL); 119 super.add(listbox); 120 super.add(label); 121 errorLabel.setMarginTop(15); 122 $(errorLabel.getElement()).insertAfter($(getElement())); 123 listbox.setGwtDisplay(Style.Display.BLOCK); 124 125 super.onLoad(); 126 127 load(); 128 129 registerHandler(addSelectionHandler(valueChangeEvent -> $(getElement()).find("input").val(""))); 130 } 131 132 @Override 133 public void load() { 134 JsComboBox jsComboBox = $(listbox.getElement()); 135 jsComboBox.select2(options); 136 137 jsComboBox.on(ComboBoxEvents.CHANGE, event -> { 138 if (!suppressChangeEvent) { 139 ValueChangeEvent.fire(this, getValue()); 140 } 141 return true; 142 }); 143 144 jsComboBox.on(ComboBoxEvents.SELECT, event -> { 145 SelectItemEvent.fire(this, getValue()); 146 return true; 147 }); 148 149 jsComboBox.on(ComboBoxEvents.UNSELECT, event -> { 150 UnselectItemEvent.fire(this, getValue()); 151 return true; 152 }); 153 154 jsComboBox.on(ComboBoxEvents.OPEN, (event1, o) -> { 155 OpenEvent.fire(this, null); 156 return true; 157 }); 158 159 jsComboBox.on(ComboBoxEvents.CLOSE, (event1, o) -> { 160 CloseEvent.fire(this, null); 161 return true; 162 }); 163 164 if (getTextColor() != null) { 165 $(getElement()).find(".select2-selection__rendered").css("color", getTextColor().getCssName()); 166 } 167 } 168 169 @Override 170 protected void onUnload() { 171 super.onUnload(); 172 173 unload(); 174 } 175 176 @Override 177 public void unload() { 178 JsComboBox jsComboBox = $(listbox.getElement()); 179 jsComboBox.off(ComboBoxEvents.CHANGE); 180 jsComboBox.off(ComboBoxEvents.SELECT); 181 jsComboBox.off(ComboBoxEvents.UNSELECT); 182 jsComboBox.off(ComboBoxEvents.OPEN); 183 jsComboBox.off(ComboBoxEvents.CLOSE); 184 jsComboBox.select2("destroy"); 185 } 186 187 @Override 188 public void reload() { 189 unload(); 190 load(); 191 } 192 193 @Override 194 public void add(Widget child) { 195 if (child instanceof OptGroup) { 196 for (Widget w : ((OptGroup) child).getChildren()) { 197 if (w instanceof Option) { 198 values.add((T) ((Option) w).getValue()); 199 } 200 } 201 } else if (child instanceof Option) { 202 values.add((T) ((Option) child).getValue()); 203 } 204 listbox.add(child); 205 } 206 207 /** 208 * Add OptionGroup directly to combobox component 209 * 210 * @param group - Option Group component 211 */ 212 public void addGroup(OptGroup group) { 213 listbox.add(group); 214 } 215 216 /** 217 * Add item directly to combobox component with existing OptGroup 218 * 219 * @param text - The text you want to labeled on the option item 220 * @param value - The value you want to pass through in this option 221 * @param optGroup - Add directly this option into the existing group 222 */ 223 public void addItem(String text, T value, OptGroup optGroup) { 224 if (!values.contains(value)) { 225 values.add(value); 226 optGroup.add(buildOption(text, value)); 227 } 228 } 229 230 /** 231 * Add Value directly to combobox component 232 * 233 * @param text - The text you want to labeled on the option item 234 * @param value - The value you want to pass through in this option 235 */ 236 public Option addItem(String text, T value) { 237 if (!values.contains(value)) { 238 Option option = buildOption(text, value); 239 values.add(value); 240 listbox.add(option); 241 return option; 242 } 243 return null; 244 } 245 246 public Option addItem(T value) { 247 return addItem(keyFactory.generateKey(value), value); 248 } 249 250 public void setItems(Collection<T> items) { 251 clear(); 252 addItems(items); 253 } 254 255 public void addItems(Collection<T> items) { 256 items.forEach(this::addItem); 257 } 258 259 /** 260 * Build the Option Element with provided params 261 */ 262 protected Option buildOption(String text, T value) { 263 Option option = new Option(); 264 option.setText(text); 265 option.setValue(keyFactory.generateKey(value)); 266 return option; 267 } 268 269 /** 270 * Programmatically open the combobox component 271 */ 272 public void open() { 273 $(listbox.getElement()).select2("open"); 274 } 275 276 /** 277 * Programmatically close the combobox component 278 */ 279 public void close() { 280 $(listbox.getElement()).select2("close"); 281 } 282 283 @Override 284 public void clear() { 285 final Iterator<Widget> it = iterator(); 286 while (it.hasNext()) { 287 final Widget widget = it.next(); 288 if (widget != label && widget != errorLabel && widget != listbox) { 289 it.remove(); 290 } 291 } 292 listbox.clear(); 293 values.clear(); 294 } 295 296 /** 297 * Sets the parent element of the dropdown 298 */ 299 public void setDropdownParent(String dropdownParent) { 300 options.dropdownParent = $(dropdownParent); 301 } 302 303 public JQueryElement getDropdownParent() { 304 return options.dropdownParent; 305 } 306 307 /** 308 * Will get the Selection Results ul element containing all the combobox items. 309 */ 310 public JQueryElement getDropdownResultElement() { 311 String dropdownId = getDropdownContainerElement().attr("id").toString(); 312 if (dropdownId != null && !(dropdownId.isEmpty())) { 313 dropdownId = dropdownId.replace("container", "results"); 314 return $("#" + dropdownId); 315 } else { 316 GWT.log("The element dropdown-result ul element is undefined.", new NullPointerException()); 317 } 318 return null; 319 } 320 321 /** 322 * Will get the Selection dropdown container rendered 323 */ 324 public JQueryElement getDropdownContainerElement() { 325 JQueryElement element = $(getElement()).find(".select2 .selection .select2-selection__rendered"); 326 if (element == null) { 327 GWT.log("The element dropdown-container element is undefined.", new NullPointerException()); 328 } 329 return element; 330 } 331 332 /** 333 * Set the upper label above the combobox 334 */ 335 public void setLabel(String text) { 336 label.setText(text); 337 } 338 339 @Override 340 public String getPlaceholder() { 341 return options.placeholder; 342 } 343 344 @Override 345 public void setPlaceholder(String placeholder) { 346 options.placeholder = placeholder; 347 } 348 349 /** 350 * Check if allow clear option is enabled 351 */ 352 public boolean isAllowClear() { 353 return options.allowClear; 354 } 355 356 /** 357 * Add a clear button on the right side of the combobox 358 */ 359 public void setAllowClear(boolean allowClear) { 360 options.allowClear = allowClear; 361 } 362 363 /** 364 * Get the maximum number of items to be entered on multiple combobox 365 */ 366 public int getLimit() { 367 return options.maximumSelectionLength; 368 } 369 370 /** 371 * Set the maximum number of items to be entered on multiple combobox 372 */ 373 public void setLimit(int limit) { 374 options.maximumSelectionLength = limit; 375 } 376 377 /** 378 * Check whether the search box is enabled on combobox 379 */ 380 public boolean isHideSearch() { 381 return options.minimumResultsForSearch.equals("Infinity"); 382 } 383 384 /** 385 * Set the option to display the search box inside the combobox component 386 */ 387 public void setHideSearch(boolean hideSearch) { 388 if (hideSearch) { 389 options.minimumResultsForSearch = "Infinity"; 390 } 391 } 392 393 /** 394 * Check whether the multiple option is enabled 395 */ 396 public boolean isMultiple() { 397 if (listbox != null) { 398 return listbox.getElement().hasAttribute("multiple"); 399 } 400 return false; 401 } 402 403 404 /** 405 * Sets multi-value select boxes. 406 */ 407 public void setMultiple(boolean multiple) { 408 if (multiple) { 409 $(listbox.getElement()).attr("multiple", "multiple"); 410 } else { 411 $(listbox.getElement()).removeAttr("multiple"); 412 } 413 } 414 415 416 public void setAcceptableValues(Collection<T> values) { 417 setItems(values); 418 } 419 420 @Override 421 public List<T> getValue() { 422 if (!isMultiple()) { 423 int index = getSelectedIndex(); 424 T value; 425 if (index != -1) { 426 427 // Check when the value is a custom tag 428 if (isTags()) { 429 value = (T) $(listbox.getElement()).val(); 430 } else { 431 value = values.get(index); 432 } 433 434 return Collections.singletonList(value); 435 } 436 } else { 437 return getSelectedValues(); 438 } 439 return new ArrayList<>(); 440 } 441 442 /** 443 * Gets the value for currently selected item. If multiple items are 444 * selected, this method will return the value of the first selected item. 445 * 446 * @return the value for selected item, or {@code null} if none is selected 447 */ 448 public List<T> getSelectedValue() { 449 return getValue(); 450 } 451 452 /** 453 * Only return a single value even if multi support is activate. 454 */ 455 public T getSingleValue() { 456 List<T> values = getSelectedValue(); 457 if (!values.isEmpty()) { 458 return values.get(0); 459 } 460 return null; 461 } 462 463 @Override 464 public void setValue(List<T> value) { 465 setValue(value, false); 466 } 467 468 /** 469 * Set the selected value using a single item, generally used 470 * in single selection mode. 471 */ 472 public void setSingleValue(T value) { 473 setValue(Collections.singletonList(value)); 474 } 475 476 @Override 477 public void setValue(List<T> values, boolean fireEvents) { 478 if (!isMultiple()) { 479 if (!values.isEmpty()) { 480 setSingleValue(values.get(0), fireEvents); 481 } 482 } else { 483 setValues(values, fireEvents); 484 } 485 } 486 487 /** 488 * Set the selected value using a single item, generally used 489 * in single selection mode. 490 */ 491 public void setSingleValue(T value, boolean fireEvents) { 492 int index = this.values.indexOf(value); 493 if (index >= 0) { 494 List<T> before = getValue(); 495 setSelectedIndex(index); 496 497 if (fireEvents) { 498 ValueChangeEvent.fireIfNotEqual(this, before, Collections.singletonList(value)); 499 } 500 } 501 } 502 503 /** 504 * Set directly all the values that will be stored into 505 * combobox and build options into it. 506 */ 507 public void setValues(List<T> values) { 508 setValues(values, true); 509 } 510 511 /** 512 * Set directly all the values that will be stored into 513 * combobox and build options into it. 514 */ 515 public void setValues(List<T> values, boolean fireEvents) { 516 String[] stringValues = new String[values.size()]; 517 for (int i = 0; i < values.size(); i++) { 518 stringValues[i] = keyFactory.generateKey(values.get(i)); 519 } 520 suppressChangeEvent = !fireEvents; 521 $(listbox.getElement()).val(stringValues).trigger("change", selectedIndex); 522 suppressChangeEvent = false; 523 } 524 525 /** 526 * Gets the index of the value pass in this method 527 * 528 * @param value - The Object you want to pass as value on combobox 529 */ 530 public int getValueIndex(T value) { 531 return values.indexOf(value); 532 } 533 534 /** 535 * Sets the currently selected index. 536 * <p> 537 * After calling this method, only the specified item in the list will 538 * remain selected. For a ListBox with multiple selection enabled. 539 * 540 * @param selectedIndex - the index of the item to be selected 541 */ 542 public void setSelectedIndex(int selectedIndex) { 543 this.selectedIndex = selectedIndex; 544 if (values.size() > 0) { 545 T value = values.get(selectedIndex); 546 if (value != null) { 547 $(listbox.getElement()).val(keyFactory.generateKey(value)).trigger("change.select2", selectedIndex); 548 } else { 549 GWT.log("Value index is not found.", new IndexOutOfBoundsException()); 550 } 551 } 552 } 553 554 /** 555 * Gets the text for currently selected item. If multiple items are 556 * selected, this method will return the text of the first selected item. 557 * 558 * @return the text for selected item, or {@code null} if none is selected 559 */ 560 public int getSelectedIndex() { 561 Object o = $(getElement()).find("option:selected").last().prop("index"); 562 if (o != null) { 563 return Integer.parseInt(o.toString()); 564 } 565 return -1; 566 } 567 568 /** 569 * Get all the values sets on combobox 570 */ 571 public List<T> getValues() { 572 return values; 573 } 574 575 /** 576 * Get the selected vales from multiple combobox 577 */ 578 public List<T> getSelectedValues() { 579 Object[] curVal = (Object[]) $(listbox.getElement()).val(); 580 581 List<T> selectedValues = new ArrayList<>(); 582 if (curVal == null || curVal.length < 1) { 583 return selectedValues; 584 } 585 586 List<String> keyIndex = getValuesKeyIndex(); 587 for (Object val : curVal) { 588 if (val instanceof String) { 589 int selectedIndex = keyIndex.indexOf(val); 590 if (selectedIndex != -1) { 591 selectedValues.add(values.get(selectedIndex)); 592 } else { 593 if (isTags() && val instanceof String) { 594 selectedValues.add((T) val); 595 } 596 } 597 } 598 } 599 return selectedValues; 600 } 601 602 protected List<String> getValuesKeyIndex() { 603 List<String> keys = new ArrayList<>(); 604 for (T value : values) { 605 keys.add(keyFactory.generateKey(value)); 606 } 607 return keys; 608 } 609 610 /** 611 * Use your own key factory for value keys. 612 */ 613 public void setKeyFactory(KeyFactory<T, String> keyFactory) { 614 this.keyFactory = keyFactory; 615 } 616 617 @Override 618 public void setReadOnly(boolean value) { 619 getReadOnlyMixin().setReadOnly(value); 620 } 621 622 @Override 623 public boolean isReadOnly() { 624 return getReadOnlyMixin().isReadOnly(); 625 } 626 627 @Override 628 public void setToggleReadOnly(boolean toggle) { 629 getReadOnlyMixin().setToggleReadOnly(toggle); 630 registerHandler(addValueChangeHandler(valueChangeEvent -> { 631 if (isToggleReadOnly()) { 632 setReadOnly(true); 633 } 634 })); 635 } 636 637 @Override 638 public boolean isToggleReadOnly() { 639 return getReadOnlyMixin().isToggleReadOnly(); 640 } 641 642 /** 643 * Check whether the dropdown will be close or not when result is selected 644 */ 645 public boolean isCloseOnSelect() { 646 return options.closeOnSelect; 647 } 648 649 /** 650 * Allow or Prevent the dropdown from closing when a result is selected (Default true) 651 */ 652 public void setCloseOnSelect(boolean closeOnSelect) { 653 options.closeOnSelect = closeOnSelect; 654 } 655 656 public MaterialWidget getListbox() { 657 return listbox; 658 } 659 660 public Label getLabel() { 661 return label; 662 } 663 664 public MaterialLabel getErrorLabel() { 665 return errorLabel; 666 } 667 668 public boolean isTags() { 669 return options.tags; 670 } 671 672 /** 673 * Note: Tags will only support String as generic params starting 2.x. 674 */ 675 public void setTags(boolean tags) { 676 if (tags) GWT.log("Note: Tags will only support String as generic params."); 677 options.tags = tags; 678 } 679 680 /** 681 * Will provide a set of text objecs that can be used for i18n language support. 682 */ 683 public void setLanguage(LanguageOptions language) { 684 options.language = language; 685 } 686 687 public LanguageOptions getLanguage() { 688 return options.language; 689 } 690 691 public void scrollTop(int offset) { 692 Scheduler.get().scheduleDeferred(() -> getDropdownResultElement().scrollTop(offset)); 693 } 694 695 @Override 696 public void setEnabled(boolean enabled) { 697 super.setEnabled(enabled); 698 699 getEnabledMixin().updateWaves(enabled, this); 700 } 701 702 public HandlerRegistration addSelectionHandler(SelectItemEvent.SelectComboHandler<T> selectionHandler) { 703 return addHandler(selectionHandler, SelectItemEvent.getType()); 704 } 705 706 @Override 707 public HandlerRegistration addOpenHandler(OpenHandler<T> openHandler) { 708 return addHandler(openHandler, OpenEvent.getType()); 709 } 710 711 @Override 712 public HandlerRegistration addCloseHandler(CloseHandler<T> closeHandler) { 713 return addHandler(closeHandler, CloseEvent.getType()); 714 } 715 716 @Override 717 public HandlerRegistration addRemoveItemHandler(UnselectItemEvent.UnselectComboHandler<T> handler) { 718 return addHandler(handler, UnselectItemEvent.getType()); 719 } 720 721 @Override 722 protected EnabledMixin<MaterialWidget> getEnabledMixin() { 723 if (enabledMixin == null) { 724 enabledMixin = new EnabledMixin<>(listbox); 725 } 726 return enabledMixin; 727 } 728 729 @Override 730 public ErrorMixin<AbstractValueWidget, MaterialLabel> getErrorMixin() { 731 if (errorMixin == null) { 732 errorMixin = new ErrorMixin<>(this, errorLabel, this.asWidget()); 733 } 734 return errorMixin; 735 } 736 737 public ReadOnlyMixin<MaterialComboBox, MaterialWidget> getReadOnlyMixin() { 738 if (readOnlyMixin == null) { 739 readOnlyMixin = new ReadOnlyMixin<>(this, listbox); 740 } 741 return readOnlyMixin; 742 } 743}