You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

AbstractMultiSelect.java 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527
  1. /*
  2. * Copyright 2000-2018 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 com.vaadin.ui;
  17. import java.util.ArrayList;
  18. import java.util.Collection;
  19. import java.util.Collections;
  20. import java.util.HashSet;
  21. import java.util.LinkedHashSet;
  22. import java.util.List;
  23. import java.util.Objects;
  24. import java.util.Optional;
  25. import java.util.Set;
  26. import java.util.stream.Collectors;
  27. import org.jsoup.nodes.Element;
  28. import com.vaadin.data.HasValue;
  29. import com.vaadin.data.SelectionModel;
  30. import com.vaadin.data.SelectionModel.Multi;
  31. import com.vaadin.data.provider.DataGenerator;
  32. import com.vaadin.data.provider.DataProvider;
  33. import com.vaadin.event.selection.MultiSelectionEvent;
  34. import com.vaadin.event.selection.MultiSelectionListener;
  35. import com.vaadin.server.Resource;
  36. import com.vaadin.server.ResourceReference;
  37. import com.vaadin.server.SerializableConsumer;
  38. import com.vaadin.server.SerializablePredicate;
  39. import com.vaadin.shared.Registration;
  40. import com.vaadin.shared.data.selection.MultiSelectServerRpc;
  41. import com.vaadin.shared.ui.ListingJsonConstants;
  42. import com.vaadin.shared.ui.abstractmultiselect.AbstractMultiSelectState;
  43. import com.vaadin.ui.declarative.DesignContext;
  44. import com.vaadin.ui.declarative.DesignException;
  45. import elemental.json.JsonObject;
  46. /**
  47. * Base class for listing components that allow selecting multiple items.
  48. * <p>
  49. * Sends selection information individually for each item.
  50. *
  51. * @param <T>
  52. * item type
  53. * @author Vaadin Ltd
  54. * @since 8.0
  55. */
  56. public abstract class AbstractMultiSelect<T> extends AbstractListing<T>
  57. implements MultiSelect<T> {
  58. private List<T> selection = new ArrayList<>();
  59. private class MultiSelectServerRpcImpl implements MultiSelectServerRpc {
  60. @Override
  61. public void updateSelection(Set<String> selectedItemKeys,
  62. Set<String> deselectedItemKeys) {
  63. AbstractMultiSelect.this.updateSelection(
  64. getItemsForSelectionChange(selectedItemKeys),
  65. getItemsForSelectionChange(deselectedItemKeys), true);
  66. }
  67. private Set<T> getItemsForSelectionChange(Set<String> keys) {
  68. return keys.stream().map(key -> getItemForSelectionChange(key))
  69. .filter(Optional::isPresent).map(Optional::get)
  70. .collect(Collectors.toSet());
  71. }
  72. private Optional<T> getItemForSelectionChange(String key) {
  73. T item = getDataCommunicator().getKeyMapper().get(key);
  74. if (item == null || !getItemEnabledProvider().test(item)) {
  75. return Optional.empty();
  76. }
  77. return Optional.of(item);
  78. }
  79. }
  80. private final class MultiSelectDataGenerator implements DataGenerator<T> {
  81. @Override
  82. public void generateData(T data, JsonObject jsonObject) {
  83. String caption = getItemCaptionGenerator().apply(data);
  84. if (caption != null) {
  85. jsonObject.put(ListingJsonConstants.JSONKEY_ITEM_VALUE,
  86. caption);
  87. } else {
  88. jsonObject.put(ListingJsonConstants.JSONKEY_ITEM_VALUE, "");
  89. }
  90. Resource icon = getItemIconGenerator().apply(data);
  91. if (icon != null) {
  92. String iconUrl = ResourceReference
  93. .create(icon, AbstractMultiSelect.this, null).getURL();
  94. jsonObject.put(ListingJsonConstants.JSONKEY_ITEM_ICON, iconUrl);
  95. }
  96. if (!getItemEnabledProvider().test(data)) {
  97. jsonObject.put(ListingJsonConstants.JSONKEY_ITEM_DISABLED,
  98. true);
  99. }
  100. if (isSelected(data)) {
  101. jsonObject.put(ListingJsonConstants.JSONKEY_ITEM_SELECTED,
  102. true);
  103. }
  104. }
  105. @Override
  106. public void destroyData(T data) {
  107. }
  108. @Override
  109. public void destroyAllData() {
  110. AbstractMultiSelect.this.deselectAll();
  111. }
  112. @Override
  113. public void refreshData(T item) {
  114. refreshSelectedItem(item);
  115. }
  116. }
  117. /**
  118. * The item enabled status provider. It is up to the implementing class to
  119. * support this or not.
  120. */
  121. private SerializablePredicate<T> itemEnabledProvider = item -> true;
  122. /**
  123. * Creates a new multi select with an empty data provider.
  124. */
  125. protected AbstractMultiSelect() {
  126. registerRpc(new MultiSelectServerRpcImpl());
  127. // #FIXME it should be the responsibility of the SelectionModel
  128. // (AbstractSelectionModel) to add selection data for item
  129. addDataGenerator(new MultiSelectDataGenerator());
  130. }
  131. /**
  132. * Adds a selection listener that will be called when the selection is
  133. * changed either by the user or programmatically.
  134. *
  135. * @param listener
  136. * the value change listener, not {@code null}
  137. * @return a registration for the listener
  138. */
  139. @Override
  140. public Registration addSelectionListener(
  141. MultiSelectionListener<T> listener) {
  142. return addListener(MultiSelectionEvent.class, listener,
  143. MultiSelectionListener.SELECTION_CHANGE_METHOD);
  144. }
  145. @Override
  146. public ItemCaptionGenerator<T> getItemCaptionGenerator() {
  147. return super.getItemCaptionGenerator();
  148. }
  149. @Override
  150. public void setItemCaptionGenerator(
  151. ItemCaptionGenerator<T> itemCaptionGenerator) {
  152. super.setItemCaptionGenerator(itemCaptionGenerator);
  153. }
  154. /**
  155. * Returns the current value of this object which is an immutable set of the
  156. * currently selected items.
  157. * <p>
  158. * The call is delegated to {@link #getSelectedItems()}
  159. *
  160. * @return the current selection
  161. *
  162. * @see #getSelectedItems()
  163. * @see SelectionModel#getSelectedItems
  164. */
  165. @Override
  166. public Set<T> getValue() {
  167. return getSelectedItems();
  168. }
  169. /**
  170. * Sets the value of this object which is a set of items to select. If the
  171. * new value is not equal to {@code getValue()}, fires a value change event.
  172. * May throw {@code IllegalArgumentException} if the value is not
  173. * acceptable.
  174. * <p>
  175. * The method effectively selects the given items and deselects previously
  176. * selected. The call is delegated to
  177. * {@link Multi#updateSelection(Set, Set)}.
  178. *
  179. * @see Multi#updateSelection(Set, Set)
  180. *
  181. * @param value
  182. * the items to select, not {@code null}
  183. * @throws NullPointerException
  184. * if the value is invalid
  185. */
  186. @Override
  187. public void setValue(Set<T> value) {
  188. Objects.requireNonNull(value);
  189. Set<T> copy = value.stream().map(Objects::requireNonNull)
  190. .collect(Collectors.toCollection(LinkedHashSet::new));
  191. updateSelection(copy, new LinkedHashSet<>(getSelectedItems()));
  192. }
  193. /**
  194. * Adds a value change listener. The listener is called when the selection
  195. * set of this multi select is changed either by the user or
  196. * programmatically.
  197. *
  198. * @see #addSelectionListener(MultiSelectionListener)
  199. *
  200. * @param listener
  201. * the value change listener, not null
  202. * @return a registration for the listener
  203. */
  204. @Override
  205. public Registration addValueChangeListener(
  206. HasValue.ValueChangeListener<Set<T>> listener) {
  207. return addSelectionListener(
  208. event -> listener.valueChange(new ValueChangeEvent<>(this,
  209. event.getOldValue(), event.isUserOriginated())));
  210. }
  211. /**
  212. * Returns the item enabled provider for this multiselect.
  213. * <p>
  214. * <em>Implementation note:</em> Override this method and
  215. * {@link #setItemEnabledProvider(SerializablePredicate)} as {@code public}
  216. * and invoke {@code super} methods to support this feature in the
  217. * multiselect component.
  218. *
  219. * @return the item enabled provider, not {@code null}
  220. * @see #setItemEnabledProvider(SerializablePredicate)
  221. */
  222. protected SerializablePredicate<T> getItemEnabledProvider() {
  223. return itemEnabledProvider;
  224. }
  225. /**
  226. * Sets the item enabled predicate for this multiselect. The predicate is
  227. * applied to each item to determine whether the item should be enabled (
  228. * {@code true}) or disabled ({@code false}). Disabled items are displayed
  229. * as grayed out and the user cannot select them. The default predicate
  230. * always returns {@code true} (all the items are enabled).
  231. * <p>
  232. * <em>Implementation note:</em> Override this method and
  233. * {@link #getItemEnabledProvider()} as {@code public} and invoke
  234. * {@code super} methods to support this feature in the multiselect
  235. * component.
  236. *
  237. * @param itemEnabledProvider
  238. * the item enabled provider to set, not {@code null}
  239. */
  240. protected void setItemEnabledProvider(
  241. SerializablePredicate<T> itemEnabledProvider) {
  242. Objects.requireNonNull(itemEnabledProvider);
  243. this.itemEnabledProvider = itemEnabledProvider;
  244. }
  245. @Override
  246. public void setRequiredIndicatorVisible(boolean visible) {
  247. super.setRequiredIndicatorVisible(visible);
  248. }
  249. @Override
  250. public boolean isRequiredIndicatorVisible() {
  251. return super.isRequiredIndicatorVisible();
  252. }
  253. @Override
  254. protected AbstractMultiSelectState getState() {
  255. return (AbstractMultiSelectState) super.getState();
  256. }
  257. @Override
  258. protected AbstractMultiSelectState getState(boolean markAsDirty) {
  259. return (AbstractMultiSelectState) super.getState(markAsDirty);
  260. }
  261. @Override
  262. public void setReadOnly(boolean readOnly) {
  263. super.setReadOnly(readOnly);
  264. }
  265. @Override
  266. public boolean isReadOnly() {
  267. return super.isReadOnly();
  268. }
  269. @Override
  270. public void updateSelection(Set<T> addedItems, Set<T> removedItems) {
  271. updateSelection(addedItems, removedItems, false);
  272. }
  273. /**
  274. * Updates the selection by adding and removing the given items.
  275. *
  276. * @param addedItems
  277. * the items added to selection, not {@code} null
  278. * @param removedItems
  279. * the items removed from selection, not {@code} null
  280. * @param userOriginated
  281. * {@code true} if this was used originated, {@code false} if not
  282. */
  283. protected void updateSelection(Set<T> addedItems, Set<T> removedItems,
  284. boolean userOriginated) {
  285. Objects.requireNonNull(addedItems);
  286. Objects.requireNonNull(removedItems);
  287. // if there are duplicates, some item is both added & removed, just
  288. // discard that and leave things as was before
  289. DataProvider<T, ?> dataProvider = internalGetDataProvider();
  290. addedItems.removeIf(item -> {
  291. Object addedId = dataProvider.getId(item);
  292. return removedItems.stream().map(dataProvider::getId).anyMatch(
  293. addedId::equals) ? removedItems.remove(item) : false;
  294. });
  295. if (isAllSelected(addedItems) && isNoneSelected(removedItems)) {
  296. return;
  297. }
  298. updateSelection(set -> {
  299. // order of add / remove does not matter since no duplicates
  300. set.removeIf(item -> {
  301. Object itemId = dataProvider.getId(item);
  302. return removedItems.stream().map(dataProvider::getId)
  303. .anyMatch(itemId::equals);
  304. });
  305. set.addAll(addedItems);
  306. }, userOriginated);
  307. }
  308. @Override
  309. public Set<T> getSelectedItems() {
  310. return Collections.unmodifiableSet(new LinkedHashSet<>(selection));
  311. }
  312. @Override
  313. public void deselectAll() {
  314. if (selection.isEmpty()) {
  315. return;
  316. }
  317. updateSelection(Collection::clear, false);
  318. }
  319. @Override
  320. public boolean isSelected(T item) {
  321. DataProvider<T, ?> dataProvider = internalGetDataProvider();
  322. Object id = dataProvider.getId(item);
  323. return selection.stream().map(dataProvider::getId).anyMatch(id::equals);
  324. }
  325. private boolean isAllSelected(Collection<T> items) {
  326. for (T item : items) {
  327. if (!isSelected(item)) {
  328. return false;
  329. }
  330. }
  331. return true;
  332. }
  333. private boolean isNoneSelected(Collection<T> items) {
  334. for (T item : items) {
  335. if (isSelected(item)) {
  336. return false;
  337. }
  338. }
  339. return true;
  340. }
  341. /**
  342. * Deselects the given item. If the item is not currently selected, does
  343. * nothing.
  344. *
  345. * @param item
  346. * the item to deselect, not null
  347. * @param userOriginated
  348. * {@code true} if this was used originated, {@code false} if not
  349. */
  350. protected void deselect(T item, boolean userOriginated) {
  351. if (!selection.contains(item)) {
  352. return;
  353. }
  354. updateSelection(set -> set.remove(item), userOriginated);
  355. }
  356. /**
  357. * Removes the given items. Any item that is not currently selected, is
  358. * ignored. If none of the items are selected, does nothing.
  359. *
  360. * @param items
  361. * the items to deselect, not {@code null}
  362. * @param userOriginated
  363. * {@code true} if this was used originated, {@code false} if not
  364. */
  365. protected void deselect(Set<T> items, boolean userOriginated) {
  366. Objects.requireNonNull(items);
  367. if (items.stream().noneMatch(i -> isSelected(i))) {
  368. return;
  369. }
  370. updateSelection(set -> set.removeAll(items), userOriginated);
  371. }
  372. /**
  373. * Selects the given item. Depending on the implementation, may cause other
  374. * items to be deselected. If the item is already selected, does nothing.
  375. *
  376. * @param item
  377. * the item to select, not null
  378. * @param userOriginated
  379. * {@code true} if this was used originated, {@code false} if not
  380. */
  381. protected void select(T item, boolean userOriginated) {
  382. if (selection.contains(item)) {
  383. return;
  384. }
  385. updateSelection(set -> set.add(item), userOriginated);
  386. }
  387. @Override
  388. protected Collection<String> getCustomAttributes() {
  389. Collection<String> attributes = super.getCustomAttributes();
  390. // "value" is not an attribute for the component. "selected" attribute
  391. // is used in "option"'s tag to mark selection which implies value for
  392. // multiselect component
  393. attributes.add("value");
  394. return attributes;
  395. }
  396. @Override
  397. protected Element writeItem(Element design, T item, DesignContext context) {
  398. Element element = super.writeItem(design, item, context);
  399. if (isSelected(item)) {
  400. element.attr("selected", true);
  401. }
  402. return element;
  403. }
  404. @Override
  405. protected void readItems(Element design, DesignContext context) {
  406. Set<T> selected = new HashSet<>();
  407. List<T> items = design.children().stream()
  408. .map(child -> readItem(child, selected, context))
  409. .collect(Collectors.toList());
  410. deselectAll();
  411. if (!items.isEmpty()) {
  412. setItems(items);
  413. }
  414. selected.forEach(this::select);
  415. }
  416. /**
  417. * Reads an Item from a design and inserts it into the data source.
  418. * Hierarchical select components should override this method to recursively
  419. * recursively read any child items as well.
  420. *
  421. * @param child
  422. * a child element representing the item
  423. * @param selected
  424. * A set accumulating selected items. If the item that is read is
  425. * marked as selected, its item id should be added to this set.
  426. * @param context
  427. * the DesignContext instance used in parsing
  428. * @return the item id of the new item
  429. *
  430. * @throws DesignException
  431. * if the tag name of the {@code child} element is not
  432. * {@code option}.
  433. */
  434. protected T readItem(Element child, Set<T> selected,
  435. DesignContext context) {
  436. T item = readItem(child, context);
  437. if (child.hasAttr("selected")) {
  438. selected.add(item);
  439. }
  440. return item;
  441. }
  442. private void updateSelection(SerializableConsumer<Collection<T>> handler,
  443. boolean userOriginated) {
  444. LinkedHashSet<T> oldSelection = new LinkedHashSet<>(selection);
  445. handler.accept(selection);
  446. fireEvent(new MultiSelectionEvent<>(AbstractMultiSelect.this,
  447. oldSelection, userOriginated));
  448. getDataCommunicator().reset();
  449. }
  450. private final void refreshSelectedItem(T item) {
  451. DataProvider<T, ?> dataProvider = internalGetDataProvider();
  452. Object id = dataProvider.getId(item);
  453. for (int i = 0; i < selection.size(); ++i) {
  454. if (id.equals(dataProvider.getId(selection.get(i)))) {
  455. selection.set(i, item);
  456. return;
  457. }
  458. }
  459. }
  460. }