package org.ovirt.engine.ui.common.widget.editor;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.gwtbootstrap3.client.ui.constants.Styles;
import org.ovirt.engine.ui.common.idhandler.HasElementId;
import org.ovirt.engine.ui.uicompat.external.StringUtils;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.SpanElement;
import com.google.gwt.dom.client.UListElement;
import com.google.gwt.editor.client.adapters.TakesValueEditor;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.KeyDownEvent;
import com.google.gwt.event.dom.client.KeyDownHandler;
import com.google.gwt.event.dom.client.KeyPressEvent;
import com.google.gwt.event.dom.client.KeyPressHandler;
import com.google.gwt.event.dom.client.KeyUpEvent;
import com.google.gwt.event.dom.client.KeyUpHandler;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.event.shared.HasHandlers;
import com.google.gwt.resources.client.CssResource;
import com.google.gwt.safehtml.client.SafeHtmlTemplates;
import com.google.gwt.safehtml.shared.SafeHtml;
import com.google.gwt.safehtml.shared.SafeHtmlUtils;
import com.google.gwt.text.shared.Renderer;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.ComplexPanel;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.HasConstrainedValue;
import com.google.gwt.user.client.ui.HasEnabled;
import com.google.gwt.user.client.ui.Widget;
/**
* List box widget that adapts to UiCommon list model items.
*
* @param <T>
* List box item type.
*/
public class ListModelListBox<T> extends Composite implements EditorWidget<T, TakesValueEditor<T>>,
HasConstrainedValue<T>, HasHandlers, HasElementId, HasEnabled {
protected static final String DATA_TOGGLE = "data-toggle"; //$NON-NLS-1$
protected static final String DROPDOWN_MENU = "dropdownMenu"; //$NON-NLS-1$
protected static final String ID = "id"; //$NON-NLS-1$
protected static final String NBSP = " "; //$NON-NLS-1$
protected static final String BTN_DEFAULT = "btn-default"; //$NON-NLS-1$
protected static final String GWT_BUTTON = "gwt-Button"; //$NON-NLS-1$
protected static final String FILTER_OPTION = "filter-option"; //$NON-NLS-1$
protected static final String ARIA_LABELLEDBY = "aria-labelledby"; //$NON-NLS-1$
protected static final String SELECTED = "selected"; //$NON-NLS-1$
protected static final String ROLE = "role"; //$NON-NLS-1$
protected static final String MENU = "menu"; //$NON-NLS-1$
protected static final String PRESENTATION = "presentation"; //$NON-NLS-1$
private TakesConstrainedValueEditor<T> editor;
interface ListModelListBoxUiBinder extends UiBinder<FlowPanel, ListModelListBox<?>> {
ListModelListBoxUiBinder uiBinder = GWT.create(ListModelListBoxUiBinder.class);
}
interface Style extends CssResource {
String scrollableMenu();
String selected();
String liPosition();
String inlineBlock();
String checkIcon();
String labelContainer();
String label();
String selectedValue();
String button();
}
interface ListItemTemplate extends SafeHtmlTemplates {
@Template("<span class=\"text {1}\">{0}</span><span class=\"glyphicon glyphicon-ok check-mark {1} {2}\"></span>")
SafeHtml multiSelectListItem(String text, String textSpanStyle, String iconStyle);
}
interface ButtonTextSpan extends SafeHtmlTemplates {
@Template("<span class=\"caret pull-right\" style=\"position: absolute; margin-top: 5px; right: 6px;\"></span>")
SafeHtml selectedValue();
}
interface AnchorText extends SafeHtmlTemplates {
@Template("<a tabindex=\"-1\" role=\"menuitem\" class=\"{1}\">{0}</a>")
SafeHtml anchor(SafeHtml text, String className);
}
private final Renderer<T> renderer;
private final List<T> valueList = new ArrayList<>();
private T currentValue;
@UiField
protected FlowPanel container;
@UiField
protected FlowPanel dropdownPanel;
@UiField(provided=true)
protected Button dropdownButton;
@UiField
protected Style style;
private final ListItemTemplate listItemTemplate = GWT.create(ListItemTemplate.class);
private final ButtonTextSpan buttonSelectedValueSpan = GWT.create(ButtonTextSpan.class);
private final AnchorText anchorText = GWT.create(AnchorText.class);
protected SpanElement selectedValue;
protected UnorderedListPanel listPanel;
private boolean changing = false;
protected boolean isMultiSelect = false;
/**
* Creates a list box that renders its items using the specified {@link Renderer}.
*
* @param renderer
* Renderer for list box items.
*/
public ListModelListBox(Renderer<T> renderer) {
this.renderer = renderer;
dropdownButton = createButton();
initWidget(ListModelListBoxUiBinder.uiBinder.createAndBindUi(this));
listPanel = getListPanel();
listPanel.addStyleName(style.scrollableMenu());
dropdownButton.addStyleName(style.button());
selectedValue.addClassName(style.selectedValue());
dropdownButton.getElement().setInnerHTML(selectedValue.getString()
+ buttonSelectedValueSpan.selectedValue().asString());
dropdownPanel.add(listPanel);
dropdownButton.addClickHandler(event -> listPanel.scrollToSelected());
}
public void setDropdownWidth(String width) {
listPanel.setWidth(width);
}
protected UnorderedListPanel getListPanel() {
return new UnorderedListPanel();
}
private Button createButton() {
Button button = new Button();
button.removeStyleName(GWT_BUTTON);
button.addStyleName(Styles.BTN);
button.addStyleName(BTN_DEFAULT);
button.addStyleName(Styles.DROPDOWN_TOGGLE);
button.addStyleName(Styles.FORM_CONTROL);
button.getElement().setAttribute(ID, DROPDOWN_MENU);
button.getElement().setAttribute(DATA_TOGGLE, Styles.DROPDOWN);
selectedValue = Document.get().createSpanElement();
selectedValue.addClassName(FILTER_OPTION);
selectedValue.addClassName(Styles.PULL_LEFT); //$NON-NLS-1$
selectedValue.setInnerHTML(NBSP);
return button;
}
@Override
public TakesConstrainedValueEditor<T> asEditor() {
if (editor == null) {
editor = TakesConstrainedValueEditor.of(this, this, this);
}
return editor;
}
@Override
public HandlerRegistration addKeyUpHandler(KeyUpHandler handler) {
return dropdownPanel.addDomHandler(handler, KeyUpEvent.getType());
}
@Override
public HandlerRegistration addKeyDownHandler(KeyDownHandler handler) {
return dropdownPanel.addDomHandler(handler, KeyDownEvent.getType());
}
@Override
public HandlerRegistration addKeyPressHandler(KeyPressHandler handler) {
return dropdownPanel.addDomHandler(handler, KeyPressEvent.getType());
}
@Override
public void onAttach() {
super.onAttach();
getElement().removeClassName(Styles.FORM_CONTROL);
}
@Override
public void setValue(T value) {
setValue(value, false);
}
@Override
public void setValue(T value, boolean fireEvents) {
setValue(value, fireEvents, false);
}
protected void setValue(T value, boolean fireEvents, boolean fromClick) {
if (changing) {
return;
} else if (value == null) {
updateCurrentValue(null, fireEvents);
} else {
boolean found = false;
for (T listItem: this.valueList) {
if (listItem == value || (listItem != null && listItem.equals(value))) {
//Found the value, show the right thing on the button.
updateCurrentValue(value, fireEvents);
found = true;
break;
}
}
if (!found) {
addValue(value);
updateCurrentValue(value, fireEvents);
}
}
}
private void updateCurrentValue(final T value, boolean fireEvents) {
setChanging(!ignoreChanging());
String renderedValue = renderer.render(value);
if (StringUtils.isEmpty(renderedValue)) {
renderedValue = NBSP;
dropdownButton.setTitle(""); //$NON-NLS-1$
} else {
renderedValue = SafeHtmlUtils.htmlEscape(renderedValue);
dropdownButton.setTitle(renderedValue);
}
((Element)dropdownButton.getElement().getChild(0)).setInnerHTML(renderedValue);
Scheduler.get().scheduleDeferred(() -> listPanel.setSelected(value));
currentValue = value;
if (fireEvents) {
Scheduler.get().scheduleDeferred(() -> {
ValueChangeEvent.fire(ListModelListBox.this, currentValue);
setChanging(false);
});
} else {
setChanging(false);
}
}
/**
* Return true if you want ignore the changing variable. This variable is used to avoid excessive changing
* of values during setting acceptable values. Override if you want to ignore this optimization.
* @return True if you want to ignore the 'changing' variable, false otherwise.
*/
protected boolean ignoreChanging() {
return false;
}
public T getValue() {
return currentValue;
}
public void setChanging(boolean value) {
changing = value;
}
public boolean getChaging() {
return changing;
}
@Override
public HandlerRegistration addValueChangeHandler(final ValueChangeHandler<T> handler) {
return addHandler(handler, ValueChangeEvent.getType());
}
@Override
public void setAcceptableValues(Collection<T> values) {
valueList.clear();
listPanel.clear();
if (values.isEmpty()) {
updateCurrentValue(null, false);
}
for(T value: values) {
addValue(value);
}
}
private void addValue(T value) {
this.valueList.add(value);
//Make sure to not pass null to the list panel or the SafeHtml.escapeHtml will bomb.
String text = renderer.render(value) == null ? "" : renderer.render(value); //$NON-NLS-1$
listPanel.addListItem(text, value);
}
@Override
public Widget asWidget() {
return this;
}
@Override
public boolean isEnabled() {
return dropdownButton.isEnabled();
}
@Override
public void setEnabled(boolean enabled) {
dropdownButton.setEnabled(enabled);
if (!enabled) {
dropdownButton.addStyleName(Styles.DISABLED);
} else {
dropdownButton.removeStyleName(Styles.DISABLED);
}
}
@Override
public int getTabIndex() {
return dropdownButton.getTabIndex();
}
@Override
public void setAccessKey(char key) {
dropdownButton.setAccessKey(key);
}
@Override
public void setFocus(boolean focused) {
dropdownButton.setFocus(focused);
}
@Override
public void setTabIndex(int index) {
dropdownButton.setTabIndex(index);
}
@Override
public void setElementId(String elementId) {
dropdownButton.getElement().setAttribute(ID, elementId);
listPanel.setAriaLabelledBy(elementId);
}
protected Renderer<T> getRenderer() {
return this.renderer;
}
protected class UnorderedListPanel extends ComplexPanel {
private final List<HandlerRegistration> clickHandlers = new ArrayList<>();
private final UListElement uListElement;
UnorderedListPanel() {
uListElement = Document.get().createULElement();
uListElement.addClassName(Styles.DROPDOWN_MENU);
uListElement.setAttribute(ROLE, MENU);
setAriaLabelledBy(DROPDOWN_MENU);
setElement(uListElement);
}
public void scrollToSelected() {
for (Widget child : getChildren()) {
if (child instanceof ListModelListBox.ListItem) {
final ListItem item = (ListModelListBox<T>.ListItem) child;
if (item.isSelected()) {
Scheduler.get().scheduleDeferred(() -> item.getElement().scrollIntoView());
}
}
}
}
void setAriaLabelledBy(String id) {
uListElement.setAttribute(ARIA_LABELLEDBY, id); //$NON-NLS-1$
}
void addListItem(String text, T value) {
String nonEmptyText = "".equals(text) ? NBSP : text;
ListItem li = getListItem(nonEmptyText, value);
getClickHandlers().add(li.addClickHandler(event -> {
@SuppressWarnings("unchecked")
ListItem item = (ListItem) event.getSource();
ListModelListBox.this.setValue(item.getValue(), true, true);
if (ListModelListBox.this.isMultiSelect) {
event.stopPropagation();
}
}));
add(li, getElement());
}
protected ListModelListBox<T>.ListItem getListItem(String text, T value) {
return new ListItem(text, value);
}
@SuppressWarnings("unchecked")
public void setSelected(T value) {
for(Widget child: getChildren()) {
if (child instanceof ListModelListBox.ListItem) {
ListItem item = (ListModelListBox<T>.ListItem) child;
//Clear any selection first
item.removeSelected();
if (value instanceof List) {
if (((List<T>)value).contains(((List<T>)item.getValue()).get(0))) {
item.setSelected();
}
} else {
if (item.getValue() == value || (item.getValue() != null && item.getValue().equals(value))) {
item.setSelected();
}
}
}
}
}
List<HandlerRegistration> getClickHandlers() {
return clickHandlers;
}
@Override
public void clear() {
for (HandlerRegistration handler: getClickHandlers()) {
handler.removeHandler();
}
getClickHandlers().clear();
getElement().removeAllChildren();
}
}
protected class ListItem extends ComplexPanel {
private final String anchorText;
private final T value;
public ListItem(String text, T value) {
this.value = value;
Element li = Document.get().createLIElement();
li.setAttribute(ROLE, PRESENTATION);
li.addClassName(style.liPosition());
this.anchorText = text;
li.setInnerHTML(ListModelListBox.this.anchorText.anchor(SafeHtmlUtils.fromTrustedString(text), "").asString()); //$NON-NLS-1$
setElement(li);
}
public T getValue() {
return value;
}
public void setSelected() {
getElement().addClassName(SELECTED);
SafeHtml anchor;
if (ListModelListBox.this.isMultiSelect) {
anchor = ListModelListBox.this.anchorText.anchor(
ListModelListBox.this.listItemTemplate.multiSelectListItem(this.anchorText,
style.inlineBlock(), style.checkIcon()), style.selected());
} else {
anchor = ListModelListBox.this.anchorText.anchor(SafeHtmlUtils.fromTrustedString(anchorText),
style.selected());
}
getElement().setInnerHTML(anchor.asString());
}
public boolean isSelected() {
return getElement().getClassName().contains(SELECTED);
}
public void removeSelected() {
getElement().removeClassName(SELECTED);
getElement().setInnerHTML(ListModelListBox.this.anchorText.anchor(
SafeHtmlUtils.fromTrustedString(anchorText), "").asString()); //$NON-NLS-1$
}
public HandlerRegistration addClickHandler(ClickHandler handler) {
return addDomHandler(handler, ClickEvent.getType());
}
}
}