/* * Copyright 2000-2016 Vaadin Ltd. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package com.vaadin.ui; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import org.jsoup.nodes.Element; import com.vaadin.data.HasValue; import com.vaadin.data.SelectionModel.Single; import com.vaadin.data.provider.DataCommunicator; import com.vaadin.event.selection.SingleSelectionEvent; import com.vaadin.event.selection.SingleSelectionListener; import com.vaadin.shared.Registration; import com.vaadin.shared.data.selection.SelectionServerRpc; import com.vaadin.shared.ui.AbstractSingleSelectState; import com.vaadin.ui.declarative.DesignContext; import com.vaadin.ui.declarative.DesignException; import elemental.json.Json; /** * An abstract base class for listing components that only support single * selection and no lazy loading of data items. * * @author Vaadin Ltd. * * @param <T> * the item date type * * @see com.vaadin.data.SelectionModel.Single * * @since 8.0 */ public abstract class AbstractSingleSelect<T> extends AbstractListing<T> implements SingleSelect<T> { /** * Creates a new {@code AbstractListing} with a default data communicator. * <p> */ protected AbstractSingleSelect() { init(); } /** * Creates a new {@code AbstractSingleSelect} with the given custom data * communicator. * <p> * <strong>Note:</strong> This method is for creating an * {@code AbstractSingleSelect} with a custom communicator. In the common * case {@link AbstractSingleSelect#AbstractSingleSelect()} should be used. * <p> * * @param dataCommunicator * the data communicator to use, not null */ protected AbstractSingleSelect(DataCommunicator<T> dataCommunicator) { super(dataCommunicator); init(); } /** * Adds a selection listener to this select. The listener is called when the * selection is changed either by the user or programmatically. * * @param listener * the selection listener, not null * @return a registration for the listener */ public Registration addSelectionListener( SingleSelectionListener<T> listener) { return addListener(SingleSelectionEvent.class, listener, SingleSelectionListener.SELECTION_CHANGE_METHOD); } /** * Returns the currently selected item, or an empty optional if no item is * selected. * * @return an optional of the selected item if any, an empty optional * otherwise */ public Optional<T> getSelectedItem() { return Optional.ofNullable(keyToItem(getSelectedKey())); } /** * Sets the current selection to the given item or clears selection if given * {@code null}. * * @param item * the item to select or {@code null} to clear selection */ public void setSelectedItem(T item) { setSelectedFromServer(item); } /** * Returns the current value of this object which is the currently selected * item. * <p> * The call is delegated to {@link #getSelectedItem()} * * @return the current selection, may be {@code null} * * @see #getSelectedItem() * @see Single#getSelectedItem */ @Override public T getValue() { return getSelectedItem().orElse(null); } /** * Sets the value of this object which is an item to select. If the new * value is not equal to {@code getValue()}, fires a value change event. If * value is {@code null} then it deselects currently selected item. * <p> * The call is delegated to {@link #setSelectedItem(Object)}. * * @see #setSelectedItem(Object) * @see Single#setSelectedItem(Object) * * @param value * the item to select or {@code null} to clear selection */ @Override public void setValue(T value) { setSelectedItem(value); } @Override public Registration addValueChangeListener( HasValue.ValueChangeListener<T> listener) { return addSelectionListener( event -> listener.valueChange(new ValueChangeEvent<>(this, event.getOldValue(), event.isUserOriginated()))); } @Override protected AbstractSingleSelectState getState() { return (AbstractSingleSelectState) super.getState(); } @Override protected AbstractSingleSelectState getState(boolean markAsDirty) { return (AbstractSingleSelectState) super.getState(markAsDirty); } @Override public void setRequiredIndicatorVisible(boolean visible) { super.setRequiredIndicatorVisible(visible); } @Override public boolean isRequiredIndicatorVisible() { return super.isRequiredIndicatorVisible(); } @Override public void setReadOnly(boolean readOnly) { super.setReadOnly(readOnly); } @Override public boolean isReadOnly() { return super.isReadOnly(); } /** * Returns the communication key of the selected item or {@code null} if no * item is selected. * * @return the key of the selected item if any, {@code null} otherwise. */ protected String getSelectedKey() { return getState(false).selectedItemKey; } /** * Sets the selected item based on the given communication key. If the key * is {@code null}, clears the current selection if any. * * @param key * the key of the selected item or {@code null} to clear * selection */ protected void doSetSelectedKey(String key) { getState().selectedItemKey = key; } /** * Sets the selection based on a client request. Does nothing if the select * component is {@linkplain #isReadOnly()} or if the selection would not * change. Otherwise updates the selection and fires a selection change * event with {@code isUserOriginated == true}. * * @param key * the key of the item to select or {@code null} to clear * selection */ protected void setSelectedFromClient(String key) { if (isReadOnly()) { return; } if (isKeySelected(key)) { return; } T oldSelection = getSelectedItem().orElse(getEmptyValue()); doSetSelectedKey(key); // Update diffstate so that a change will be sent to the client if the // selection is changed to its original value updateDiffstate("selectedItemKey", key == null ? Json.createNull() : Json.create(key)); fireEvent(new SingleSelectionEvent<>(AbstractSingleSelect.this, oldSelection, true)); } /** * Sets the selection based on server API call. Does nothing if the * selection would not change; otherwise updates the selection and fires a * selection change event with {@code isUserOriginated == false}. * * @param item * the item to select or {@code null} to clear selection */ protected void setSelectedFromServer(T item) { // TODO creates a key if item not in data provider String key = itemToKey(item); if (isKeySelected(key) || isSelected(item)) { return; } T oldSelection = getSelectedItem().orElse(getEmptyValue()); doSetSelectedKey(key); fireEvent(new SingleSelectionEvent<>(AbstractSingleSelect.this, oldSelection, false)); } /** * Returns whether the given key maps to the currently selected item. * * @param key * the key to test or {@code null} to test whether nothing is * selected * @return {@code true} if the key equals the key of the currently selected * item (or {@code null} if no selection), {@code false} otherwise. */ protected boolean isKeySelected(String key) { return Objects.equals(key, getSelectedKey()); } /** * Returns the communication key assigned to the given item. * * @param item * the item whose key to return * @return the assigned key */ protected String itemToKey(T item) { if (item == null) { return null; } else { // TODO creates a key if item not in data provider return getDataCommunicator().getKeyMapper().key(item); } } /** * Returns the item that the given key is assigned to, or {@code null} if * there is no such item. * * @param key * the key whose item to return * @return the associated item if any, {@code null} otherwise. */ protected T keyToItem(String key) { return getDataCommunicator().getKeyMapper().get(key); } /** * Returns whether the given item is currently selected. * * @param item * the item to check, not null * @return {@code true} if the item is selected, {@code false} otherwise */ public boolean isSelected(T item) { return Objects.equals(getValue(), item); } @Override protected Element writeItem(Element design, T item, DesignContext context) { Element element = super.writeItem(design, item, context); if (isSelected(item)) { element.attr("selected", ""); } return element; } @Override protected void readItems(Element design, DesignContext context) { Set<T> selected = new HashSet<>(); List<T> items = design.children().stream() .map(child -> readItem(child, selected, context)) .collect(Collectors.toList()); if (!items.isEmpty()) { setItems(items); } selected.forEach(this::setValue); } /** * Reads an Item from a design and inserts it into the data source. * Hierarchical select components should override this method to recursively * recursively read any child items as well. * * @param child * a child element representing the item * @param selected * A set accumulating selected items. If the item that is read is * marked as selected, its item id should be added to this set. * @param context * the DesignContext instance used in parsing * @return the item id of the new item * * @throws DesignException * if the tag name of the {@code child} element is not * {@code option}. */ protected T readItem(Element child, Set<T> selected, DesignContext context) { T item = readItem(child, context); if (child.hasAttr("selected")) { selected.add(item); } return item; } @Override protected Collection<String> getCustomAttributes() { Collection<String> attributes = super.getCustomAttributes(); // "value" is not an attribute for the component. "selected" attribute // is used in "option"'s tag to mark selection which implies value for // single select component attributes.add("value"); return attributes; } private void init() { registerRpc(new SelectionServerRpc() { @Override public void select(String key) { setSelectedFromClient(key); } @Override public void deselect(String key) { if (isKeySelected(key)) { setSelectedFromClient(null); } } }); } }