zul/src/main/java/org/zkoss/zul/Combobox.java
/* Combobox.java
Purpose:
Description:
History:
Thu Dec 15 17:33:01 2005, Created by tomyeh
Copyright (C) 2005 Potix Corporation. All Rights Reserved.
{{IS_RIGHT
This program is distributed under LGPL Version 2.1 in the hope that
it will be useful, but WITHOUT ANY WARRANTY.
}}IS_RIGHT
*/
package org.zkoss.zul;
import static org.zkoss.lang.Generics.cast;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.zkoss.lang.Classes;
import org.zkoss.lang.Exceptions;
import org.zkoss.xel.VariableResolver;
import org.zkoss.zk.au.AuRequest;
import org.zkoss.zk.au.out.AuInvoke;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.Components;
import org.zkoss.zk.ui.Page;
import org.zkoss.zk.ui.UiException;
import org.zkoss.zk.ui.WrongValueException;
import org.zkoss.zk.ui.event.Event;
import org.zkoss.zk.ui.event.EventListener;
import org.zkoss.zk.ui.event.Events;
import org.zkoss.zk.ui.event.InputEvent;
import org.zkoss.zk.ui.event.OpenEvent;
import org.zkoss.zk.ui.event.SelectEvent;
import org.zkoss.zk.ui.ext.Blockable;
import org.zkoss.zk.ui.sys.BooleanPropertyAccess;
import org.zkoss.zk.ui.sys.PropertyAccess;
import org.zkoss.zk.ui.sys.ShadowElementsCtrl;
import org.zkoss.zk.ui.util.ComponentCloneListener;
import org.zkoss.zk.ui.util.ForEachStatus;
import org.zkoss.zk.ui.util.Template;
import org.zkoss.zul.event.ListDataEvent;
import org.zkoss.zul.event.ListDataListener;
import org.zkoss.zul.event.ZulEvents;
import org.zkoss.zul.ext.Selectable;
import org.zkoss.zul.impl.Utils;
/**
* A combobox.
*
* <p>Non-XUL extension. It is used to replace XUL menulist. This class
* is more flexible than menulist, such as {@link #setAutocomplete}
* {@link #setAutodrop}.
*
* <p>Default {@link #getZclass}: z-combobox.(since 3.5.0)
*
* <p>Events: onOpen, onSelect, onAfterRender<br/>
* Developers can listen to the onOpen event and initializes it
* when {@link org.zkoss.zk.ui.event.OpenEvent#isOpen} is true, and/or
* clean up if false.<br/>
* onAfterRender is sent when the model's data has been rendered.(since 5.0.4)
*
* <p>Besides assign a list model, you could assign a renderer
* (a {@link ComboitemRenderer} instance) to a combobox, such that
* the combobox will use this renderer to render the data returned by
* {@link ListModel#getElementAt}.
* If not assigned, the default renderer, which assumes a label per
* combo item, is used.
* In other words, the default renderer adds a label to
* a row by calling toString against the object returned
* by {@link ListModel#getElementAt}. (since 3.0.2)
*
* <p>Note: to have better performance, onOpen is sent only if
* a non-deferrable event listener is registered
* (see {@link org.zkoss.zk.ui.event.Deferrable}).
*
* <p>Like {@link Datebox},
* the value of a read-only comobobox ({@link #isReadonly}) can be changed
* by dropping down the list and selecting an combo item
* (though users cannot type anything in the input box).
*
* @author tomyeh
* @see Comboitem
*/
@SuppressWarnings("serial")
public class Combobox extends Textbox {
public static final String ICON_SCLASS = "z-icon-caret-down";
private static final Logger log = LoggerFactory.getLogger(Combobox.class);
private boolean _autodrop, _autocomplete = true, _btnVisible = true, _open;
//Note: _selItem is maintained loosely, i.e., its value might not be correct
//unless syncValueToSelection is called. So call getSelectedItem/getSelectedIndex
//if you want the correct value
private transient Comboitem _selItem;
/** The last checked value for selected item.
* If null, it means syncValueToSelection is required.
*/
private transient String _lastCkVal;
private ListModel<?> _model;
/** The submodel used if _model is ListSubModel. */
private Object[] _subModel;
private ComboitemRenderer<?> _renderer;
private transient ListDataListener _dataListener;
private transient EventListener<InputEvent> _eventListener;
/**Used to detect whether to sync Comboitem's index later. */
private boolean _syncItemIndicesLater;
private String _popupWidth;
private String _emptySearchMessage;
private boolean _instantSelect = true;
private String _iconSclass = ICON_SCLASS;
private static final String ATTR_ON_INIT_RENDER = "org.zkoss.zul.Combobox.onInitRender";
static {
addClientEvent(Combobox.class, Events.ON_OPEN, CE_IMPORTANT | CE_DUPLICATE_IGNORE);
addClientEvent(Combobox.class, Events.ON_SELECT, CE_IMPORTANT | CE_DUPLICATE_IGNORE);
}
public Combobox() {
}
public Combobox(String value) throws WrongValueException {
this();
setValue(value);
}
protected String coerceToString(Object value) {
final Constraint constr = getConstraint();
final String val = super.coerceToString(value);
if (val.length() > 0 && constr != null && constr instanceof SimpleConstraint
&& (((SimpleConstraint) constr).getFlags() & SimpleConstraint.STRICT) != 0) {
for (Iterator<Comboitem> it = getItems().iterator(); it.hasNext();) {
final String label = ((Comboitem) it.next()).getLabel();
if (val.equalsIgnoreCase(label))
return label;
}
}
return val;
}
//-- ListModel dependent codes --//
/** Returns the list model associated with this combobox, or null
* if this combobox is not associated with any list data model.
* <p> Note: for implementation of auto-complete, the result of {@link #getItemCount()} is a subset of model.
* So, if the model implemented {@link ListSubModel} interface, you can't use the index of model to find the comboitem by {@link #getItemAtIndex(int)}.
* @since 3.0.2
* @see ListSubModel#getSubModel(Object, int)
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public <T> ListModel<T> getModel() {
return (ListModel) _model;
}
/** Sets the list model associated with this combobox.
* If a non-null model is assigned, no matter whether it is the same as
* the previous, it will always cause re-render.
*
* @param model the list model to associate, or null to dissociate
* any previous model.
* @exception UiException if failed to initialize with the model
* @since 3.0.2
*/
public void setModel(ListModel<?> model) {
if (model != null) {
if (!(model instanceof Selectable))
throw new UiException(model.getClass() + " must implement " + Selectable.class);
if (_model != model) {
if (_model != null) {
_model.removeListDataListener(_dataListener);
}
// ZK-1702: do not clear Comboitems if using Data Binding 1
if (_model != null
&& !_model.getClass().getName().equals("org.zkoss.zkplus.databind.BindingListModelList")) {
// Bug B60-ZK-1202.zul
// Remove current items anyway, when changing models
if (!getItems().isEmpty()) {
getItems().clear();
}
}
_model = model;
_subModel = null; //clean up (generated later)
initDataListener();
setAttribute(Attributes.BEFORE_MODEL_ITEMS_RENDERED, Boolean.TRUE);
}
postOnInitRender(null);
//Since user might setModel and setRender separately or repeatedly,
//we don't handle it right now until the event processing phase
//such that we won't render the same set of data twice
//--
//For better performance, we shall load the first few row now
//(to save a round trip)
} else if (_model != null) {
_model.removeListDataListener(_dataListener);
if (_model instanceof ListSubModel)
removeEventListener(Events.ON_CHANGING, _eventListener);
_model = null;
_subModel = null;
if (!getItems().isEmpty())
getItems().clear();
}
}
/** Sets empty search message.
* This message would be displayed, when no matching results was found.
* Note: it's meaningless if no model case.
*
* @param msg
* @since 8.5.1
*/
public void setEmptySearchMessage(String msg) {
if (!Objects.equals(_emptySearchMessage, msg)) {
_emptySearchMessage = msg;
smartUpdate("emptySearchMessage", _emptySearchMessage);
}
}
/**
* Returns the empty search message if any.
* Default: null
* @since 10.0.0
*/
public String getEmptySearchMessage() {
return _emptySearchMessage;
}
private int INVALIDATE_THRESHOLD = -1;
private void initDataListener() {
if (INVALIDATE_THRESHOLD == -1) {
INVALIDATE_THRESHOLD = Utils.getIntAttribute(this, "org.zkoss.zul.invalidateThreshold", 10, true);
}
if (_dataListener == null)
_dataListener = new ListDataListener() {
public void onChange(ListDataEvent event) {
int type = event.getType();
if (getAttribute(Attributes.BEFORE_MODEL_ITEMS_RENDERED) != null
&& (type == ListDataEvent.INTERVAL_ADDED || type == ListDataEvent.INTERVAL_REMOVED))
return;
final ListModel _model = getModel();
final int newsz = _model.getSize(), oldsz = getItemCount();
int min = event.getIndex0(), max = event.getIndex1(), cnt;
// Bug B30-1906748.zul
switch (type) {
case ListDataEvent.SELECTION_CHANGED:
doSelectionChanged();
return; //nothing changed so need to rerender
case ListDataEvent.MULTIPLE_CHANGED:
return; //nothing to do
case ListDataEvent.INTERVAL_ADDED:
cnt = newsz - oldsz;
if (cnt < 0)
throw new UiException("Adding causes a smaller list?");
if (cnt == 0) //no change, nothing to do here
return;
if ((oldsz <= 0 || cnt > INVALIDATE_THRESHOLD) && !isOpen())//ZK-2704: don't invalidate when the combobox is open
invalidate();
//Also better performance (outer better than remove a lot)
if (min < 0)
if (max < 0)
min = 0;
else
min = max - cnt + 1;
if (min > oldsz)
min = oldsz;
final Renderer renderer = new Renderer();
final Component next = min < oldsz ? getItemAtIndex(min) : null;
int index = min;
try {
ComboitemRenderer cirenderer = null;
while (--cnt >= 0) {
if (cirenderer == null)
cirenderer = (ComboitemRenderer) getRealRenderer();
Comboitem item = newUnloadedItem(cirenderer);
insertBefore(item, next);
renderer.render(item, _model.getElementAt(index), index++);
}
// Fix ZK-5468: the content of the subsequence item might be changed
List<Comboitem> comboitems = new ArrayList<>(getItems());
Iterator<Comboitem> iterator = comboitems.iterator();
int start = 0;
for (int i = max + 1, j = comboitems.size(); i < j && iterator.hasNext(); start++) {
if (start < i) {
iterator.next();
continue;
}
renderer.render(iterator.next(), _model.getElementAt(i), i++);
}
} catch (Throwable ex) {
renderer.doCatch(ex);
} finally {
renderer.doFinally();
}
break;
case ListDataEvent.INTERVAL_REMOVED:
cnt = oldsz - newsz;
if (cnt < 0)
throw new UiException("Removal causes a larger list?");
if (cnt == 0) //no change, nothing to do here
return;
if (min >= 0)
max = min + cnt - 1;
else if (max < 0)
max = cnt - 1; //0 ~ cnt - 1
if (max > oldsz - 1)
max = oldsz - 1;
if ((newsz <= 0 || cnt > INVALIDATE_THRESHOLD) && !isOpen())//ZK-2704: don't invalidate when the combobox is open
invalidate();
//Also better performance (outer better than remove a lot)
//detach from end (due to groupfoot issue)
Component comp = getItemAtIndex(max);
while (--cnt >= 0) {
Component p = comp.getPreviousSibling();
comp.detach();
comp = p;
}
// Fix ZK-5468: the content of the subsequence item might be changed
List<Comboitem> comboitems = new ArrayList<>(getItems());
Iterator<Comboitem> iterator = comboitems.iterator();
final Renderer renderer1 = new Renderer();
try {
int start = 0;
// no need to plus one for "max" here for removal
for (int i = max, j = comboitems.size();
i < j && iterator.hasNext(); start++) {
if (start < i) {
iterator.next();
continue;
}
renderer1.render(iterator.next(), _model.getElementAt(i), i++);
}
} catch (Throwable ex) {
renderer1.doCatch(ex);
} finally {
renderer1.doFinally();
}
break;
default:
postOnInitRender(null);
}
}
};
if (_eventListener == null)
_eventListener = new EventListener<InputEvent>() {
public void onEvent(InputEvent event) throws Exception {
if (getModel() instanceof ListSubModel) {
if (!event.isChangingBySelectBack())
postOnInitRender(event.getValue());
}
}
};
_model.addListDataListener(_dataListener);
if (_model instanceof ListSubModel)
addEventListener(Events.ON_CHANGING, _eventListener);
}
private void doSelectionChanged() {
final Selectable<Object> smodel = getSelectableModel();
if (smodel.isSelectionEmpty()) {
if (_selItem != null)
setSelectedItem(null);
return;
}
if (_selItem != null && smodel.isSelected(getElementAt(_selItem.getIndex())))
return; //nothing changed
int j = 0;
for (final Comboitem item : getItems()) {
if (smodel.isSelected(getElementAt(j++))) {
setSelectedItem(item);
return;
}
}
//Possible to reach here if _model is ListSubModel, because
//getText() might be different (so is the list of comboitems).
if (_model instanceof ListSubModel) {
//Unfortunately, we can't really fix it because the conversion from
//Object to String is done by ComboitemRenderer
//So, we only handle the very simple case
//(though it could be wrong too -- at least less obvious to users)
final Object selObj = smodel.getSelection().iterator().next();
if (selObj instanceof String || selObj == null) {
setValue((String) selObj);
postOnInitRender(null); //cause Comboitem to be generated
}
return;
}
//don't call setSelectedItem(null) either. or, it will clear _value
}
private Object getElementAt(int index) {
return _subModel != null ? _subModel[index] : _model.getElementAt(index);
}
@SuppressWarnings("unchecked")
private Selectable<Object> getSelectableModel() {
return (Selectable<Object>) _model;
}
/** Returns the renderer to render each row, or null if the default
* renderer is used.
* @since 3.0.2
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public <T> ComboitemRenderer<T> getItemRenderer() {
return (ComboitemRenderer) _renderer;
}
/** Sets the renderer which is used to render each row
* if {@link #getModel} is not null.
*
* <p>Note: changing a render will not cause the combobox to re-render.
* If you want it to re-render, you could assign the same model again
* (i.e., setModel(getModel())), or fire an {@link ListDataEvent} event.
*
* @param renderer the renderer, or null to use the default.
* @exception UiException if failed to initialize with the model
* @since 3.0.2
*/
public void setItemRenderer(ComboitemRenderer<?> renderer) {
_renderer = renderer;
}
/** Sets the renderer by use of a class name.
* It creates an instance automatically.
*@since 3.0.2
*/
@SuppressWarnings("rawtypes")
public void setItemRenderer(String clsnm) throws ClassNotFoundException, NoSuchMethodException,
IllegalAccessException, InstantiationException, java.lang.reflect.InvocationTargetException {
if (clsnm != null)
setItemRenderer((ComboitemRenderer) Classes.newInstanceByThread(clsnm));
}
/** Synchronizes the combobox to be consistent with the specified model.
*/
private ListModel<?> syncModel(Object index) {
ComboitemRenderer<?> renderer = null;
final ListModel<?> subset = _model instanceof ListSubModel ? ((ListSubModel<?>) _model).getSubModel(index, -1)
: _model;
final int newsz = subset.getSize();
if (!getItems().isEmpty())
getItems().clear();
for (int j = 0; j < newsz; ++j) {
if (renderer == null)
renderer = getRealRenderer();
newUnloadedItem(renderer).setParent(this);
}
return subset;
}
/** Creates an new and unloaded Comboitem. */
private Comboitem newUnloadedItem(ComboitemRenderer<?> renderer) {
Comboitem item = null;
if (renderer instanceof ComboitemRendererExt)
item = ((ComboitemRendererExt) renderer).newComboitem(this);
if (item == null) {
item = new Comboitem();
item.applyProperties();
}
return item;
}
/** Handles a private event, onInitRender. It is used only for
* implementation, and you rarely need to invoke it explicitly.
* @since 3.0.2
*/
@SuppressWarnings("rawtypes")
public void onInitRender(Event data) {
//Bug #2010389
removeAttribute(ATTR_ON_INIT_RENDER); //clear syncModel flag
final Renderer renderer = new Renderer();
final List<Object> subModel = _model instanceof ListSubModel ? new ArrayList<Object>() : null;
final ListModel subset = syncModel(data.getData() != null ? data.getData() : getRawText());
try {
int pgsz = subset.getSize(), ofs = 0, j = 0;
for (Comboitem item = getItems().size() <= ofs ? null : getItems().get(ofs), nxt; j < pgsz
&& item != null; ++j, item = nxt) {
nxt = (Comboitem) item.getNextSibling(); //store it first
final int index = j + ofs;
final Object value = subset.getElementAt(index);
if (subModel != null)
subModel.add(value);
renderer.render(item, value, index);
}
if (subModel != null)
_subModel = subModel.toArray(new Object[subModel.size()]);
} catch (Throwable ex) {
renderer.doCatch(ex);
} finally {
renderer.doFinally();
}
Events.postEvent("onInitRenderLater", this, null); // notify databinding load-when.
Events.postEvent(ZulEvents.ON_AFTER_RENDER, this, null); // notify the combobox when items have been rendered.
removeAttribute(Attributes.BEFORE_MODEL_ITEMS_RENDERED);
}
private void postOnInitRender(String idx) {
//20080724, Henri Chen: optimize to avoid postOnInitRender twice
if (getAttribute(ATTR_ON_INIT_RENDER) == null) {
//Bug #2010389
setAttribute(ATTR_ON_INIT_RENDER, Boolean.TRUE); //flag syncModel
Events.postEvent("onInitRender", this, idx);
}
}
@SuppressWarnings("rawtypes")
private static final ComboitemRenderer _defRend = new ComboitemRenderer() {
public void render(final Comboitem item, final Object data, final int index) {
final Combobox cb = (Combobox) item.getParent();
final Template tm = cb.getTemplate("model");
if (tm == null) {
item.setLabel(Objects.toString(data));
item.setValue(data);
} else {
final Component[] items = ShadowElementsCtrl
.filterOutShadows(tm.create(item.getParent(), item, new VariableResolver() {
public Object resolveVariable(String name) {
if ("each".equals(name)) {
return data;
} else if ("forEachStatus".equals(name)) {
return new ForEachStatus() {
public ForEachStatus getPrevious() {
return null;
}
public Object getEach() {
return getCurrent();
}
public int getIndex() {
return index;
}
public Integer getBegin() {
return 0;
}
public Integer getEnd() {
return cb.getModel().getSize();
}
public Object getCurrent() {
return data;
}
public boolean isFirst() {
return getCount() == 1;
}
public boolean isLast() {
return getIndex() + 1 == getEnd();
}
public Integer getStep() {
return null;
}
public int getCount() {
return getIndex() + 1;
}
};
} else {
return null;
}
}
}, null));
if (items.length != 1)
throw new UiException("The model template must have exactly one item, not " + items.length);
final Comboitem nci = (Comboitem) items[0];
if (nci.getValue() == null) //template might set it
nci.setValue(data);
item.setAttribute(Attributes.MODEL_RENDERAS, nci);
//indicate a new item is created to replace the existent one
item.detach();
}
}
};
/** Returns the renderer used to render items.
*/
@SuppressWarnings("unchecked")
private <T> ComboitemRenderer<T> getRealRenderer() {
return _renderer != null ? (ComboitemRenderer<T>) _renderer : _defRend;
}
/** Used to render comboitem if _model is specified. */
private class Renderer implements java.io.Serializable {
@SuppressWarnings("rawtypes")
private final ComboitemRenderer _renderer;
private boolean _rendered, _ctrled;
private Renderer() {
_renderer = getRealRenderer();
}
@SuppressWarnings("unchecked")
private void render(Comboitem item, Object value, int index) throws Throwable {
if (!_rendered && (_renderer instanceof RendererCtrl)) {
((RendererCtrl) _renderer).doTry();
_ctrled = true;
}
try {
try {
_renderer.render(item, value, index);
} catch (AbstractMethodError ex) {
final Method m = _renderer.getClass().getMethod("render",
new Class<?>[] { Comboitem.class, Object.class });
m.setAccessible(true);
m.invoke(_renderer, new Object[] { item, value });
}
Object v = item.getAttribute(Attributes.MODEL_RENDERAS);
if (v != null) //a new item is created to replace the existent one
item = (Comboitem) v;
} catch (Throwable ex) {
try {
item.setLabel(Exceptions.getMessage(ex));
} catch (Throwable t) {
log.error("", t);
}
throw ex;
}
if (getSelectableModel().isSelected(value))
setSelectedItem(item);
_rendered = true;
}
private void doCatch(Throwable ex) {
if (_ctrled) {
try {
((RendererCtrl) _renderer).doCatch(ex);
} catch (Throwable t) {
throw UiException.Aide.wrap(t);
}
} else {
throw UiException.Aide.wrap(ex);
}
}
private void doFinally() {
if (_ctrled)
((RendererCtrl) _renderer).doFinally();
}
}
/** Returns whether to automatically drop the list if users is changing
* this text box.
* <p>Default: false.
*/
public boolean isAutodrop() {
return _autodrop;
}
/** Sets whether to automatically drop the list if users is changing
* this text box.
*/
public void setAutodrop(boolean autodrop) {
if (_autodrop != autodrop) {
_autodrop = autodrop;
smartUpdate("autodrop", autodrop);
}
}
/** Returns whether to automatically complete this text box
* by matching the nearest item ({@link Comboitem}.
* It is also known as auto-type-ahead.
*
* <p>Default: true (since 5.0.0).
*
* <p>If true, the nearest item will be searched and the text box is
* updated automatically.
* If false, user has to click the item or use the DOWN or UP keys to
* select it back.
*
* <p>Don't confuse it with the auto-completion feature mentioned by
* other framework. Such kind of auto-completion is supported well
* by listening to the onChanging event.
*/
public boolean isAutocomplete() {
return _autocomplete;
}
/** Sets whether to automatically complete this text box
* by matching the nearest item ({@link Comboitem}.
*/
public void setAutocomplete(boolean autocomplete) {
if (_autocomplete != autocomplete) {
_autocomplete = autocomplete;
smartUpdate("autocomplete", autocomplete);
}
}
/** Returns whether this combobox is open.
*
* <p>Default: false.
* @since 6.0.0
*/
public boolean isOpen() {
return _open;
}
/** Drops down or closes the list of combo items ({@link Comboitem}.
* only works while visible
* @since 3.0.1
*/
public void setOpen(boolean open) {
if (isVisible()) {
if (_open != open) {
_open = open;
smartUpdate("open", open);
}
}
}
/** Drops down the list of combo items ({@link Comboitem}.
* It is the same as setOpen(true).
*
* @since 3.0.1
*/
public void open() {
_open = true;
response("open", new AuInvoke(this, "setOpen", true)); //don't use smartUpdate
}
/** Closes the list of combo items ({@link Comboitem} if it was
* dropped down.
* It is the same as setOpen(false).
*
* @since 3.0.1
*/
public void close() {
_open = false;
response("open", new AuInvoke(this, "setOpen", false)); //don't use smartUpdate
}
/** Returns whether the button (on the right of the textbox) is visible.
* <p>Default: true.
*/
public boolean isButtonVisible() {
return _btnVisible;
}
/** Sets whether the button (on the right of the textbox) is visible.
*/
public void setButtonVisible(boolean visible) {
if (_btnVisible != visible) {
_btnVisible = visible;
smartUpdate("buttonVisible", visible);
}
}
/**
* Returns true if onSelect event is sent as soon as user selects using keyboard navigation.
* <p>Default: true
*
* @since 8.6.1
*/
public boolean isInstantSelect() {
return _instantSelect;
}
/**
* Sets the instantSelect attribute. When the attribute is true, onSelect event
* will be fired as soon as user selects using keyboard navigation.
*
* If the attribute is false, user needs to press Enter key to finish the selection using keyboard navigation.
*
* @since 8.6.1
*/
public void setInstantSelect(boolean instantSelect) {
if (_instantSelect != instantSelect) {
_instantSelect = instantSelect;
smartUpdate("instantSelect", instantSelect);
}
}
/** Returns a 'live' list of all {@link Comboitem}.
* By live we mean you can add or remove them directly with
* the List interface.
*
* <p>Currently, it is the same as {@link #getChildren}. However,
* we might add other kind of children in the future.
*/
public List<Comboitem> getItems() {
return cast(getChildren());
}
/** Returns the number of items.
*/
public int getItemCount() {
return getItems().size();
}
/** Returns the item at the specified index.
*/
public Comboitem getItemAtIndex(int index) {
return getItems().get(index);
}
/** Appends an item.
*/
public Comboitem appendItem(String label) {
final Comboitem item = new Comboitem(label);
item.setParent(this);
return item;
}
/** Removes the child item in the list box at the given index.
* @return the removed item.
*/
public Comboitem removeItemAt(int index) {
final Comboitem item = getItemAtIndex(index);
removeChild(item);
return item;
}
/** Returns the selected item.
* @since 2.4.0
*/
public Comboitem getSelectedItem() {
syncValueToSelection();
return _selItem;
}
/** Deselects the currently selected items and selects the given item.
* <p>Note: if the label of comboitem has the same more than one, the first
* comboitem will be selected at client side, it is a limitation of {@link Combobox}
* and it is different from {@link Listbox}.</p>
* @since 3.0.2
*/
public void setSelectedItem(Comboitem item) {
if (item != null && item.getParent() != this)
throw new UiException("Not a child: " + item);
if (item != _selItem) {
_selItem = item;
if (item != null) {
setValue(item.getLabel());
smartUpdate("selectedItemUuid_", item.getUuid());
} else {
//Bug#2919037: don't call setRawValue(), or the error message will be cleared
if (_value != null && !"".equals(_value)) {
_value = "";
smartUpdate("value", coerceToString(_value));
}
}
_lastCkVal = getValue();
}
}
/** Deselects the currently selected items and selects
* the item with the given index.
* <p>Note: if the label of comboitem has the same more than one, the first
* comboitem will be selected at client side, it is a limitation of {@link Combobox}
* and it is different from {@link Listbox}.</p>
* @since 3.0.2
*/
public void setSelectedIndex(int jsel) {
if (jsel >= getItemCount())
throw new UiException("Out of bound: " + jsel + " while size=" + getItemCount());
if (jsel < -1)
jsel = -1;
setSelectedItem(jsel >= 0 ? getItemAtIndex(jsel) : null);
//Bug#2919037: setSelectedIndex(-1) shall unselect even with constraint
}
/** Returns the index of the selected item, or -1 if not selected.
* @since 3.0.1
*/
public int getSelectedIndex() {
syncValueToSelection();
return _selItem != null ? _selItem.getIndex() : -1;
}
/**
* @return the width of the popup of this component
* @since 8.0.3
*/
public String getPopupWidth() {
return _popupWidth;
}
/**
* Sets the width of the popup of this component.
* If the input is a percentage, the popup width will be calculated by multiplying the width of this component with the percentage.
* (e.g. if the input string is 130%, and the width of this component is 300px, the popup width will be 390px = 300px * 130%)
* Others will be set directly.
* @param popupWidth the width of the popup of this component
* @since 8.0.3
*/
public void setPopupWidth(String popupWidth) {
if (!Objects.equals(popupWidth, _popupWidth)) {
_popupWidth = popupWidth;
smartUpdate("popupWidth", popupWidth);
}
}
//-- super --//
public void setMultiline(boolean multiline) {
if (multiline)
throw new UnsupportedOperationException("Combobox doesn't support multiline");
}
public void setRows(int rows) {
if (rows != 1)
throw new UnsupportedOperationException("Combobox doesn't support multiple rows, " + rows);
}
public Object getExtraCtrl() {
return new ExtraCtrl();
}
/** A utility class to implement {@link #getExtraCtrl}.
* It is used only by component developers.
*
* <p>If a component requires more client controls, it is suggested to
* override {@link #getExtraCtrl} to return an instance that extends from
* this class.
*/
protected class ExtraCtrl extends Textbox.ExtraCtrl implements Blockable {
public boolean shallBlock(AuRequest request) {
// B50-3316103: special case of readonly component: do not block onChange and onSelect
final String cmd = request.getCommand();
if (Events.ON_OPEN.equals(cmd))
return false;
return isDisabled() || (isReadonly() && Events.ON_CHANGING.equals(cmd))
|| !Components.isRealVisible(Combobox.this);
}
}
private void syncSelectionToModel() {
if (_model != null) {
List<Object> selObjs = new ArrayList<Object>();
if (_selItem != null)
selObjs.add(getElementAt(_selItem.getIndex()));
getSelectableModel().setSelection(selObjs);
}
}
/**
* Sets the iconSclass name of this Combobox.
* @param iconSclass String
* @since 8.6.2
*/
public void setIconSclass(String iconSclass) {
if (!Objects.equals(_iconSclass, iconSclass)) {
_iconSclass = iconSclass;
smartUpdate("iconSclass", iconSclass);
}
}
/**
* Returns the iconSclass name of this Combobox.
* @return the iconSclass name
* @since 8.6.2
*/
public String getIconSclass() {
return _iconSclass;
}
// super
public String getZclass() {
return _zclass == null ? "z-combobox" : _zclass;
}
protected void renderProperties(org.zkoss.zk.ui.sys.ContentRenderer renderer) throws java.io.IOException {
super.renderProperties(renderer);
render(renderer, "autodrop", _autodrop);
if (!_autocomplete)
renderer.render("autocomplete", false);
if (!_btnVisible)
renderer.render("buttonVisible", false);
if (_selItem != null)
renderer.render("selectedItemUuid_", _selItem.getUuid());
if (_popupWidth != null)
renderer.render("popupWidth", _popupWidth);
if (_emptySearchMessage != null)
renderer.render("emptySearchMessage", _emptySearchMessage);
if (!_instantSelect)
renderer.render("instantSelect", false);
if (!ICON_SCLASS.equals(_iconSclass))
renderer.render("iconSclass", _iconSclass);
// handle open state here instead of send AuInvoke for Stateless
if (_open)
renderer.render("open", true);
}
/** Processes an AU request.
*
* <p>Default: in addition to what are handled by {@link Textbox#service},
* it also handles onOpen and onSelect.
* @since 5.0.0
*/
public void service(org.zkoss.zk.au.AuRequest request, boolean everError) {
final String cmd = request.getCommand();
if (cmd.equals(Events.ON_OPEN)) {
OpenEvent evt = OpenEvent.getOpenEvent(request);
_open = evt.isOpen();
Events.postEvent(evt);
} else if (cmd.equals(Events.ON_SELECT)) {
final Set<Comboitem> prevSelectedItems = new LinkedHashSet<Comboitem>();
Comboitem prevSeld = (Comboitem) request.getDesktop()
.getComponentByUuidIfAny((String) request.getData().get("prevSeld"));
// ZK-2089: should skip when selected item is null
if (prevSeld != null)
prevSelectedItems.add(prevSeld);
SelectEvent<Comboitem, Object> evt = SelectEvent.getSelectEvent(request,
new SelectEvent.SelectedObjectHandler<Comboitem>() {
public Set<Object> getObjects(Set<Comboitem> items) {
if (items == null || items.isEmpty() || _model == null)
return null;
Set<Object> objs = new LinkedHashSet<Object>();
// ZK-5047: we cannot use index here (if it's ListSubModel case)
if (_model instanceof ListSubModel) {
for (Comboitem i : items) {
objs.add(i.getValue());
}
} else {
for (Comboitem i : items) {
objs.add(_model.getElementAt(i.getIndex()));
}
}
return objs;
}
public Set<Comboitem> getPreviousSelectedItems() {
return prevSelectedItems;
}
// in single selection, getPreviousSelectedItems() is same as getPreviousSelectedItems()
public Set<Comboitem> getUnselectedItems() {
return getPreviousSelectedItems();
}
public Set<Object> getPreviousSelectedObjects() {
Set<Comboitem> items = getPreviousSelectedItems();
if (_model == null || items.size() < 1)
return null;
else {
Set<Object> s = new LinkedHashSet<Object>();
s.add(_model.getElementAt(((Comboitem) items.iterator().next()).getIndex()));
return s;
}
}
// in single selection, getUnselectedObjects() is same as getPreviousSelectedObjects()
public Set<Object> getUnselectedObjects() {
return getPreviousSelectedObjects();
}
});
Comboitem oldSel = _selItem;
Set<Comboitem> selItems = evt.getSelectedItems();
_selItem = selItems != null && !selItems.isEmpty() ? (Comboitem) selItems.iterator().next() : null;
if (getModel() instanceof ListSubModel && _selItem == null && ((List) request.getData().get("items")).size() != 0) {
_selItem = oldSel; // selItem might be missing when new items are created in the same AU, sync back
if (selItems != null) {
selItems.add(oldSel);
}
}
_lastCkVal = getValue(); //onChange is sent before onSelect
syncSelectionToModel();
//ZK-1987: Combobox item selection rely items label string
String val = _lastCkVal;
if (oldSel != null && !oldSel.equals(_selItem) && oldSel.getLabel().equals(val))
Events.postEvent(new InputEvent(Events.ON_CHANGE, this, val, val));
Events.postEvent(evt);
} else if (cmd.equals(Events.ON_CHANGE)) {
super.service(request, everError);
// Bug ZK-1492: synchronize the input value to selection
syncValueToSelection();
} else
super.service(request, everError);
}
//-- Component --//
public void beforeChildAdded(Component newChild, Component refChild) {
if (!(newChild instanceof Comboitem))
throw new UiException("Unsupported child for Combobox: " + newChild);
super.beforeChildAdded(newChild, refChild);
}
/** Childable. */
protected boolean isChildable() {
return true;
}
public void onChildAdded(Component child) {
super.onChildAdded(child);
_syncItemIndicesLater = true;
smartUpdate("repos", true);
}
public void onChildRemoved(Component child) {
super.onChildRemoved(child);
_syncItemIndicesLater = true;
if (child == _selItem) {
// Bug B60-ZK-1202.zul
_selItem = null;
schedSyncValueToSelection();
}
smartUpdate("repos", true);
}
/*package*/ void syncItemIndices() { //called by Comboitem
if (_syncItemIndicesLater) {
_syncItemIndicesLater = false;
int j = 0;
for (final Comboitem item : getItems())
item.setIndexDirectly(j++);
}
}
private void syncValueToSelection() {
final String value = getValue();
if (!Objects.equals(_lastCkVal, value)) {
_lastCkVal = value;
_selItem = null;
for (final Comboitem item : getItems()) {
if (Objects.equals(value, item.getLabel())) {
_selItem = item;
break;
}
}
syncSelectionToModel();
}
}
/*package*/ void schedSyncValueToSelection() {
_lastCkVal = null;
}
/*package*/ Comboitem getSelectedItemDirectly() {
return _selItem;
}
//Cloneable//
@SuppressWarnings("rawtypes")
public Object clone() {
final Combobox clone = (Combobox) super.clone();
clone._selItem = null;
clone.schedSyncValueToSelection();
if (clone._model != null) {
if (clone._model instanceof ComponentCloneListener) {
final ListModel model = (ListModel) ((ComponentCloneListener) clone._model).willClone(clone);
if (model != null)
clone._model = model;
}
clone._dataListener = null;
clone._eventListener = null;
clone.initDataListener();
}
return clone;
}
// Serializable//
private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
schedSyncValueToSelection();
if (_model != null) {
initDataListener();
// Map#Entry cannot be serialized, we have to restore them
if (_model instanceof ListModelMap) {
for (final Comboitem item : getItems()) {
item.setValue(getElementAt(item.getIndex()));
}
}
}
}
public void sessionWillPassivate(Page page) {
super.sessionWillPassivate(page);
willPassivate(_model);
willPassivate(_renderer);
}
public void sessionDidActivate(Page page) {
super.sessionDidActivate(page);
didActivate(_model);
didActivate(_renderer);
}
//--ComponentCtrl--//
private static HashMap<String, PropertyAccess> _properties = new HashMap<String, PropertyAccess>(3);
static {
_properties.put("buttonVisible", new BooleanPropertyAccess() {
public void setValue(Component cmp, Boolean value) {
((Combobox) cmp).setButtonVisible(value);
}
public Boolean getValue(Component cmp) {
return ((Combobox) cmp).isButtonVisible();
}
});
_properties.put("autocomplete", new BooleanPropertyAccess() {
public void setValue(Component cmp, Boolean value) {
((Combobox) cmp).setAutocomplete(value);
}
public Boolean getValue(Component cmp) {
return ((Combobox) cmp).isAutocomplete();
}
});
_properties.put("autodrop", new BooleanPropertyAccess() {
public void setValue(Component cmp, Boolean value) {
((Combobox) cmp).setAutodrop(value);
}
public Boolean getValue(Component cmp) {
return ((Combobox) cmp).isAutodrop();
}
});
_properties.put("instantSelect", new BooleanPropertyAccess() {
public void setValue(Component cmp, Boolean value) {
((Combobox) cmp).setInstantSelect(value);
}
public Boolean getValue(Component cmp) {
return ((Combobox) cmp).isInstantSelect();
}
});
}
public PropertyAccess getPropertyAccess(String prop) {
PropertyAccess pa = _properties.get(prop);
if (pa != null)
return pa;
return super.getPropertyAccess(prop);
}
public void onPageAttached(Page newpage, Page oldpage) {
super.onPageAttached(newpage, oldpage);
if (_model != null) {
postOnInitRender(null);
if (_dataListener != null) {
_model.removeListDataListener(_dataListener);
_model.addListDataListener(_dataListener);
}
}
}
public void onPageDetached(Page page) {
super.onPageDetached(page);
if (_model != null && _dataListener != null)
_model.removeListDataListener(_dataListener);
}
}