com.vaadin.client.ui.VTwinColSelect Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of vaadin-client Show documentation
Show all versions of vaadin-client Show documentation
Vaadin is a web application framework for Rich Internet Applications (RIA).
Vaadin enables easy development and maintenance of fast and
secure rich web
applications with a stunning look and feel and a wide browser support.
It features a server-side architecture with the majority of the logic
running
on the server. Ajax technology is used at the browser-side to ensure a
rich
and interactive user experience.
/*
* 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.client.ui;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;
import com.google.gwt.dom.client.Style.Overflow;
import com.google.gwt.dom.client.Style.Position;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.DoubleClickEvent;
import com.google.gwt.event.dom.client.DoubleClickHandler;
import com.google.gwt.event.dom.client.HasDoubleClickHandlers;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyDownEvent;
import com.google.gwt.event.dom.client.KeyDownHandler;
import com.google.gwt.event.dom.client.MouseDownEvent;
import com.google.gwt.event.dom.client.MouseDownHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.HasEnabled;
import com.google.gwt.user.client.ui.ListBox;
import com.google.gwt.user.client.ui.Panel;
import com.google.gwt.user.client.ui.Widget;
import com.vaadin.client.Focusable;
import com.vaadin.client.StyleConstants;
import com.vaadin.client.WidgetUtil;
import com.vaadin.client.connectors.AbstractMultiSelectConnector.MultiSelectWidget;
import com.vaadin.shared.Registration;
import elemental.json.JsonObject;
/**
* A list builder widget that has two selects; one for selectable options,
* another for selected options, and buttons for selecting and deselecting the
* items.
*
* @author Vaadin Ltd
*/
public class VTwinColSelect extends Composite implements MultiSelectWidget,
Field, ClickHandler, Focusable, HasEnabled, KeyDownHandler,
MouseDownHandler, DoubleClickHandler, SubPartAware {
private static final String SUBPART_OPTION_SELECT = "leftSelect";
private static final String SUBPART_OPTION_SELECT_ITEM = SUBPART_OPTION_SELECT
+ "-item";
private static final String SUBPART_SELECTION_SELECT = "rightSelect";
private static final String SUBPART_SELECTION_SELECT_ITEM = SUBPART_SELECTION_SELECT
+ "-item";
private static final String SUBPART_LEFT_CAPTION = "leftCaption";
private static final String SUBPART_RIGHT_CAPTION = "rightCaption";
private static final String SUBPART_ADD_BUTTON = "add";
private static final String SUBPART_REMOVE_BUTTON = "remove";
/** Primary style name for twin col select. */
public static final String CLASSNAME = "v-select-twincol";
private static final int VISIBLE_COUNT = 10;
private static final int DEFAULT_COLUMN_COUNT = 10;
private final DoubleClickListBox optionsListBox;
private final DoubleClickListBox selectionsListBox;
private final FlowPanel optionsContainer;
private final FlowPanel captionWrapper;
private final VButton addItemsLeftToRightButton;
private final VButton removeItemsRightToLeftButton;
private final FlowPanel buttons;
private final Panel panel;
private HTML optionsCaption = null;
private HTML selectionsCaption = null;
private List, Set>> selectionChangeListeners;
private boolean enabled;
private boolean readOnly;
private int rows = 0;
/**
* A multiselect ListBox which catches double clicks.
*/
public class DoubleClickListBox extends ListBox
implements HasDoubleClickHandlers {
/**
* Constructs a new DoubleClickListBox.
*/
public DoubleClickListBox() {
setMultipleSelect(true);
}
@Override
public HandlerRegistration addDoubleClickHandler(
DoubleClickHandler handler) {
return addDomHandler(handler, DoubleClickEvent.getType());
}
}
/**
* Constructs a new VTwinColSelect.
*/
public VTwinColSelect() {
selectionChangeListeners = new ArrayList<>();
optionsContainer = new FlowPanel();
initWidget(optionsContainer);
optionsContainer.setStyleName(CLASSNAME);
captionWrapper = new FlowPanel();
optionsListBox = new DoubleClickListBox();
optionsListBox.addClickHandler(this);
optionsListBox.addDoubleClickHandler(this);
optionsListBox.setVisibleItemCount(VISIBLE_COUNT);
optionsListBox.setStyleName(CLASSNAME + "-options");
selectionsListBox = new DoubleClickListBox();
selectionsListBox.addClickHandler(this);
selectionsListBox.addDoubleClickHandler(this);
selectionsListBox.setVisibleItemCount(VISIBLE_COUNT);
selectionsListBox.setStyleName(CLASSNAME + "-selections");
buttons = new FlowPanel();
buttons.setStyleName(CLASSNAME + "-buttons");
addItemsLeftToRightButton = new VButton();
addItemsLeftToRightButton.setText(">>");
addItemsLeftToRightButton.addClickHandler(this);
removeItemsRightToLeftButton = new VButton();
removeItemsRightToLeftButton.setText("<<");
removeItemsRightToLeftButton.addClickHandler(this);
panel = optionsContainer;
panel.add(captionWrapper);
captionWrapper.getElement().getStyle().setOverflow(Overflow.HIDDEN);
// Hide until there actually is a caption to prevent IE from rendering
// extra empty space
captionWrapper.setVisible(false);
panel.add(optionsListBox);
buttons.add(addItemsLeftToRightButton);
final HTML br = new HTML("");
br.setStyleName(CLASSNAME + "-deco");
buttons.add(br);
buttons.add(removeItemsRightToLeftButton);
panel.add(buttons);
panel.add(selectionsListBox);
optionsListBox.addKeyDownHandler(this);
optionsListBox.addMouseDownHandler(this);
selectionsListBox.addMouseDownHandler(this);
selectionsListBox.addKeyDownHandler(this);
updateEnabledState();
}
/**
* Gets the options caption HTML Widget.
*
* @return the options caption widget
*/
protected HTML getOptionsCaption() {
if (optionsCaption == null) {
optionsCaption = new HTML();
optionsCaption.setStyleName(CLASSNAME + "-caption-left");
optionsCaption.getElement().getStyle()
.setFloat(com.google.gwt.dom.client.Style.Float.LEFT);
captionWrapper.add(optionsCaption);
}
return optionsCaption;
}
/**
* Gets the selections caption HTML widget.
*
* @return the selections caption widget
*/
protected HTML getSelectionsCaption() {
if (selectionsCaption == null) {
selectionsCaption = new HTML();
selectionsCaption.setStyleName(CLASSNAME + "-caption-right");
selectionsCaption.getElement().getStyle()
.setFloat(com.google.gwt.dom.client.Style.Float.RIGHT);
captionWrapper.add(selectionsCaption);
}
return selectionsCaption;
}
/**
* For internal use only. May be removed or replaced in the future.
*
* @return the caption wrapper widget
*/
public Widget getCaptionWrapper() {
return captionWrapper;
}
/**
* Sets the number of visible items for the list boxes.
*
* @param rows
* the number of items to show
* @see ListBox#setVisibleItemCount(int)
*/
public void setRows(int rows) {
if (this.rows != rows) {
this.rows = rows;
optionsListBox.setVisibleItemCount(rows);
selectionsListBox.setVisibleItemCount(rows);
}
}
/**
* Returns the number of visible items for the list boxes.
*
* @return the number of items to show
* @see ListBox#setVisibleItemCount(int)
*/
public int getRows() {
return rows;
}
/**
* Updates the captions above the left (options) and right (selections)
* columns. {code null} value clear the caption.
*
* @param leftCaption
* the left caption to set, or {@code null} to clear
* @param rightCaption
* the right caption to set, or {@code null} to clear
*/
public void updateCaptions(String leftCaption, String rightCaption) {
boolean hasCaptions = leftCaption != null || rightCaption != null;
if (leftCaption == null) {
removeOptionsCaption();
} else {
getOptionsCaption().setText(leftCaption);
}
if (rightCaption == null) {
removeSelectionsCaption();
} else {
getSelectionsCaption().setText(rightCaption);
}
captionWrapper.setVisible(hasCaptions);
}
private void removeOptionsCaption() {
if (optionsCaption == null) {
return;
}
if (optionsCaption.getParent() != null) {
captionWrapper.remove(optionsCaption);
}
optionsCaption = null;
}
private void removeSelectionsCaption() {
if (selectionsCaption == null) {
return;
}
if (selectionsCaption.getParent() != null) {
captionWrapper.remove(selectionsCaption);
}
selectionsCaption = null;
}
@Override
public Registration addSelectionChangeListener(
BiConsumer, Set> listener) {
Objects.nonNull(listener);
selectionChangeListeners.add(listener);
return (Registration) () -> selectionChangeListeners.remove(listener);
}
@Override
public void setItems(List items) {
// filter selected items
List selection = items.stream()
.filter(item -> MultiSelectWidget.isSelected(item))
.collect(Collectors.toList());
items.removeAll(selection);
updateListBox(optionsListBox, items);
updateListBox(selectionsListBox, selection);
}
private static void updateListBox(ListBox listBox,
List options) {
for (int i = 0; i < options.size(); i++) {
final JsonObject item = options.get(i);
// reuse existing option if possible
if (i < listBox.getItemCount()) {
listBox.setItemText(i, MultiSelectWidget.getCaption(item));
listBox.setValue(i, MultiSelectWidget.getKey(item));
} else {
listBox.addItem(MultiSelectWidget.getCaption(item),
MultiSelectWidget.getKey(item));
}
}
// remove extra
for (int i = listBox.getItemCount() - 1; i >= options.size(); i--) {
listBox.removeItem(i);
}
}
private static boolean[] getSelectionBitmap(ListBox listBox) {
final boolean[] selectedIndexes = new boolean[listBox.getItemCount()];
for (int i = 0; i < listBox.getItemCount(); i++) {
if (listBox.isItemSelected(i)) {
selectedIndexes[i] = true;
} else {
selectedIndexes[i] = false;
}
}
return selectedIndexes;
}
private void moveSelectedItemsLeftToRight() {
Set movedItems = moveSelectedItems(optionsListBox,
selectionsListBox);
selectionChangeListeners
.forEach(e -> e.accept(movedItems, Collections.emptySet()));
}
private void moveSelectedItemsRightToLeft() {
Set movedItems = moveSelectedItems(selectionsListBox,
optionsListBox);
selectionChangeListeners
.forEach(e -> e.accept(Collections.emptySet(), movedItems));
}
private static Set moveSelectedItems(ListBox source,
ListBox target) {
final boolean[] sel = getSelectionBitmap(source);
final Set movedItems = new HashSet<>();
for (int i = 0; i < sel.length; i++) {
if (sel[i]) {
final int optionIndex = i
- (sel.length - source.getItemCount());
movedItems.add(source.getValue(optionIndex));
// Move selection to another column
final String text = source.getItemText(optionIndex);
final String value = source.getValue(optionIndex);
target.addItem(text, value);
target.setItemSelected(target.getItemCount() - 1, true);
source.removeItem(optionIndex);
}
}
// If no items are left move the focus to the selections
if (source.getItemCount() == 0) {
target.setFocus(true);
} else {
source.setFocus(true);
}
return movedItems;
}
@Override
public void onClick(ClickEvent event) {
if (event.getSource() == addItemsLeftToRightButton) {
moveSelectedItemsLeftToRight();
} else if (event.getSource() == removeItemsRightToLeftButton) {
moveSelectedItemsRightToLeft();
} else if (event.getSource() == optionsListBox) {
// unselect all in other list, to avoid mistakes (i.e wrong button)
final int count = selectionsListBox.getItemCount();
for (int i = 0; i < count; i++) {
selectionsListBox.setItemSelected(i, false);
}
} else if (event.getSource() == selectionsListBox) {
// unselect all in other list, to avoid mistakes (i.e wrong button)
final int count = optionsListBox.getItemCount();
for (int i = 0; i < count; i++) {
optionsListBox.setItemSelected(i, false);
}
}
}
/** For internal use only. May be removed or replaced in the future. */
public void clearInternalHeights() {
selectionsListBox.setHeight("");
optionsListBox.setHeight("");
}
/** For internal use only. May be removed or replaced in the future. */
public void setInternalHeights() {
int captionHeight = WidgetUtil.getRequiredHeight(captionWrapper);
int totalHeight = getOffsetHeight();
String selectHeight = totalHeight - captionHeight + "px";
selectionsListBox.setHeight(selectHeight);
optionsListBox.setHeight(selectHeight);
}
/** For internal use only. May be removed or replaced in the future. */
public void clearInternalWidths() {
String colWidth = DEFAULT_COLUMN_COUNT + "em";
String containerWidth = 2 * DEFAULT_COLUMN_COUNT + 4 + "em";
// Caption wrapper width == optionsSelect + buttons +
// selectionsSelect
String captionWrapperWidth = 2 * DEFAULT_COLUMN_COUNT + 4 - 0.5 + "em";
optionsListBox.setWidth(colWidth);
if (optionsCaption != null) {
optionsCaption.setWidth(colWidth);
}
selectionsListBox.setWidth(colWidth);
if (selectionsCaption != null) {
selectionsCaption.setWidth(colWidth);
}
buttons.setWidth("3.5em");
optionsContainer.setWidth(containerWidth);
captionWrapper.setWidth(captionWrapperWidth);
}
/** For internal use only. May be removed or replaced in the future. */
public void setInternalWidths() {
getElement().getStyle().setPosition(Position.RELATIVE);
int bordersAndPaddings = WidgetUtil
.measureHorizontalPaddingAndBorder(buttons.getElement(), 0);
int buttonWidth = WidgetUtil.getRequiredWidth(buttons);
int totalWidth = getOffsetWidth();
int spaceForSelect = (totalWidth - buttonWidth - bordersAndPaddings)
/ 2;
optionsListBox.setWidth(spaceForSelect + "px");
if (optionsCaption != null) {
optionsCaption.setWidth(spaceForSelect + "px");
}
selectionsListBox.setWidth(spaceForSelect + "px");
if (selectionsCaption != null) {
selectionsCaption.setWidth(spaceForSelect + "px");
}
captionWrapper.setWidth("100%");
}
/**
* Sets the tab index.
*
* @param tabIndex
* the tab index to set
*/
public void setTabIndex(int tabIndex) {
optionsListBox.setTabIndex(tabIndex);
selectionsListBox.setTabIndex(tabIndex);
addItemsLeftToRightButton.setTabIndex(tabIndex);
removeItemsRightToLeftButton.setTabIndex(tabIndex);
}
/**
* Sets this twin column select as read only, meaning selection cannot be
* changed.
*
* @param readOnly
* {@code true} for read only, {@code false} for not read only
*/
public void setReadOnly(boolean readOnly) {
if (this.readOnly != readOnly) {
this.readOnly = readOnly;
updateEnabledState();
}
}
/**
* Returns {@code true} if this twin column select is in read only mode,
* {@code false} if not.
*
* @return {@code true} for read only, {@code false} for not read only
*/
public boolean isReadOnly() {
return readOnly;
}
@Override
public void setEnabled(boolean enabled) {
if (this.enabled != enabled) {
this.enabled = enabled;
updateEnabledState();
}
}
@Override
public boolean isEnabled() {
return enabled;
}
private void updateEnabledState() {
boolean enabled = isEnabled() && !isReadOnly();
optionsListBox.setEnabled(enabled);
selectionsListBox.setEnabled(enabled);
addItemsLeftToRightButton.setEnabled(enabled);
removeItemsRightToLeftButton.setEnabled(enabled);
addItemsLeftToRightButton.setStyleName(StyleConstants.DISABLED,
!enabled);
removeItemsRightToLeftButton.setStyleName(StyleConstants.DISABLED,
!enabled);
}
@Override
public void focus() {
optionsListBox.setFocus(true);
}
/**
* Get the key that selects an item in the table. By default it is the Enter
* key but by overriding this you can change the key to whatever you want.
*
* @return the key that selects an item
*/
protected int getNavigationSelectKey() {
return KeyCodes.KEY_ENTER;
}
@Override
public void onKeyDown(KeyDownEvent event) {
int keycode = event.getNativeKeyCode();
// Catch tab and move between select:s
if (keycode == KeyCodes.KEY_TAB
&& event.getSource() == optionsListBox) {
// Prevent default behavior
event.preventDefault();
// Remove current selections
for (int i = 0; i < optionsListBox.getItemCount(); i++) {
optionsListBox.setItemSelected(i, false);
}
// Focus selections
selectionsListBox.setFocus(true);
}
if (keycode == KeyCodes.KEY_TAB && event.isShiftKeyDown()
&& event.getSource() == selectionsListBox) {
// Prevent default behavior
event.preventDefault();
// Remove current selections
for (int i = 0; i < selectionsListBox.getItemCount(); i++) {
selectionsListBox.setItemSelected(i, false);
}
// Focus options
optionsListBox.setFocus(true);
}
if (keycode == getNavigationSelectKey()) {
// Prevent default behavior
event.preventDefault();
// Decide which select the selection was made in
if (event.getSource() == optionsListBox) {
// Prevents the selection to become a single selection when
// using Enter key
// as the selection key (default)
optionsListBox.setFocus(false);
moveSelectedItemsLeftToRight();
} else if (event.getSource() == selectionsListBox) {
// Prevents the selection to become a single selection when
// using Enter key
// as the selection key (default)
selectionsListBox.setFocus(false);
moveSelectedItemsRightToLeft();
}
}
}
@Override
public void onMouseDown(MouseDownEvent event) {
// Ensure that items are deselected when selecting
// from a different source. See #3699 for details.
if (event.getSource() == optionsListBox) {
for (int i = 0; i < selectionsListBox.getItemCount(); i++) {
selectionsListBox.setItemSelected(i, false);
}
} else if (event.getSource() == selectionsListBox) {
for (int i = 0; i < optionsListBox.getItemCount(); i++) {
optionsListBox.setItemSelected(i, false);
}
}
}
@Override
public void onDoubleClick(DoubleClickEvent event) {
if (event.getSource() == optionsListBox) {
moveSelectedItemsLeftToRight();
optionsListBox.setSelectedIndex(-1);
optionsListBox.setFocus(false);
} else if (event.getSource() == selectionsListBox) {
moveSelectedItemsRightToLeft();
selectionsListBox.setSelectedIndex(-1);
selectionsListBox.setFocus(false);
}
}
@Override
public com.google.gwt.user.client.Element getSubPartElement(
String subPart) {
if (SUBPART_OPTION_SELECT.equals(subPart)) {
return optionsListBox.getElement();
} else if (subPart.startsWith(SUBPART_OPTION_SELECT_ITEM)) {
String idx = subPart.substring(SUBPART_OPTION_SELECT_ITEM.length());
return (com.google.gwt.user.client.Element) optionsListBox
.getElement().getChild(Integer.parseInt(idx));
} else if (SUBPART_SELECTION_SELECT.equals(subPart)) {
return selectionsListBox.getElement();
} else if (subPart.startsWith(SUBPART_SELECTION_SELECT_ITEM)) {
String idx = subPart
.substring(SUBPART_SELECTION_SELECT_ITEM.length());
return (com.google.gwt.user.client.Element) selectionsListBox
.getElement().getChild(Integer.parseInt(idx));
} else if (optionsCaption != null
&& SUBPART_LEFT_CAPTION.equals(subPart)) {
return optionsCaption.getElement();
} else if (selectionsCaption != null
&& SUBPART_RIGHT_CAPTION.equals(subPart)) {
return selectionsCaption.getElement();
} else if (SUBPART_ADD_BUTTON.equals(subPart)) {
return addItemsLeftToRightButton.getElement();
} else if (SUBPART_REMOVE_BUTTON.equals(subPart)) {
return removeItemsRightToLeftButton.getElement();
}
return null;
}
@Override
public String getSubPartName(
com.google.gwt.user.client.Element subElement) {
if (optionsCaption != null
&& optionsCaption.getElement().isOrHasChild(subElement)) {
return SUBPART_LEFT_CAPTION;
} else if (selectionsCaption != null
&& selectionsCaption.getElement().isOrHasChild(subElement)) {
return SUBPART_RIGHT_CAPTION;
} else if (optionsListBox.getElement().isOrHasChild(subElement)) {
if (optionsListBox.getElement() == subElement) {
return SUBPART_OPTION_SELECT;
} else {
int idx = WidgetUtil.getChildElementIndex(subElement);
return SUBPART_OPTION_SELECT_ITEM + idx;
}
} else if (selectionsListBox.getElement().isOrHasChild(subElement)) {
if (selectionsListBox.getElement() == subElement) {
return SUBPART_SELECTION_SELECT;
} else {
int idx = WidgetUtil.getChildElementIndex(subElement);
return SUBPART_SELECTION_SELECT_ITEM + idx;
}
} else if (addItemsLeftToRightButton.getElement()
.isOrHasChild(subElement)) {
return SUBPART_ADD_BUTTON;
} else if (removeItemsRightToLeftButton.getElement()
.isOrHasChild(subElement)) {
return SUBPART_REMOVE_BUTTON;
}
return null;
}
}