/* Selectbox.java Purpose: Description: History: Fri Sep 30 10:53:25 TST 2011, Created by jumperchen Copyright (C) 2011 Potix Corporation. All Rights Reserved. {{IS_RIGHT This program is distributed under LGPL Version 3.0 in the hope that it will be useful, but WITHOUT ANY WARRANTY. }}IS_RIGHT */ package org.zkoss.zul; import java.util.LinkedHashSet; import java.util.Set; import org.zkoss.lang.Classes; import org.zkoss.lang.Objects; import org.zkoss.xel.VariableResolver; import org.zkoss.zk.au.AuRequest; import org.zkoss.zk.ui.Component; import org.zkoss.zk.ui.Components; import org.zkoss.zk.ui.HtmlBasedComponent; import org.zkoss.zk.ui.Page; import org.zkoss.zk.ui.UiException; import org.zkoss.zk.ui.event.Events; import org.zkoss.zk.ui.event.SelectEvent; import org.zkoss.zk.ui.ext.Blockable; 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.ext.Selectable; /** * A light weight dropdown list. * <p> * Default {@link #getZclass}: z-selectbox. It does not create child widgets for * each data, so the memory usage is much lower at the server. However, the * implementation is based on HTML SELECT and OPTION tags, so the functionality * is not as rich as {@link Listbox}. * * @author jumperchen * @since 6.0.0 */ @SuppressWarnings("serial") public class Selectbox extends HtmlBasedComponent { private String _name; private boolean _disabled; private int _jsel = -1; private transient ListModel<?> _model; private transient ListDataListener _dataListener; private transient ItemRenderer<?> _renderer; private static final String ATTR_ON_INIT_RENDER_POSTED = "org.zkoss.zul.onInitLaterPosted"; private transient boolean _childable; private transient String[] _tmpdatas; static { addClientEvent(Selectbox.class, Events.ON_SELECT, CE_DUPLICATE_IGNORE | CE_IMPORTANT); addClientEvent(Selectbox.class, Events.ON_FOCUS, CE_DUPLICATE_IGNORE); addClientEvent(Selectbox.class, Events.ON_BLUR, CE_DUPLICATE_IGNORE); } public String getZclass() { return _zclass == null ? "z-selectbox" : _zclass; } /** * Returns the index of the selected item (-1 if no one is selected). */ public int getSelectedIndex() { return _jsel; } /** * Selects the item with the given index. */ public void setSelectedIndex(int jsel) { if (jsel < -1) jsel = -1; if (jsel != _jsel) { _jsel = jsel; smartUpdate("selectedIndex", jsel); } } /** * Returns the renderer to render each item, or null if the default renderer * is used. */ @SuppressWarnings({ "unchecked", "rawtypes" }) public <T> ItemRenderer<T> getItemRenderer() { return (ItemRenderer) _renderer; } /** * Sets the renderer which is used to render each item if {@link #getModel} * is not null. * * <p> * Note: changing a render will not cause the selectbox to re-render. If you * want it to re-render, you could assign the same model again (i.e., * setModel(null) and than setModel(oldModel)), 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 */ public void setItemRenderer(ItemRenderer<?> renderer) { if (_renderer != renderer) { _renderer = renderer; postOnInitRender(); // Bug ZK-2607 invalidate(); } } /** * Sets the renderer by use of a class name. It creates an instance * automatically. */ @SuppressWarnings("rawtypes") public void setItemRenderer(String clsnm) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InstantiationException, java.lang.reflect.InvocationTargetException { if (clsnm != null) setItemRenderer((ItemRenderer) Classes.newInstanceByThread(clsnm)); } /** * Returns whether it is disabled. * <p> * Default: false. */ public boolean isDisabled() { return _disabled; } protected boolean isChildable() { return _childable; } /** * Sets whether it is disabled. */ public void setDisabled(boolean disabled) { if (_disabled != disabled) { _disabled = disabled; smartUpdate("disabled", _disabled); } } /** * Returns the name of this component. * <p> * Default: null. * <p> * The name is used only to work with "legacy" Web application that handles * user's request by servlets. It works only with HTTP/HTML-based browsers. * It doesn't work with other kind of clients. * <p> * Don't use this method if your application is purely based on ZK's * event-driven model. */ public String getName() { return _name; } /** * Sets the name of this component. * <p> * The name is used only to work with "legacy" Web application that handles * user's request by servlets. It works only with HTTP/HTML-based browsers. * It doesn't work with other kind of clients. * <p> * Don't use this method if your application is purely based on ZK's * event-driven model. * * @param name * the name of this component. */ public void setName(String name) { if (name != null && name.length() == 0) name = null; if (!Objects.equals(_name, name)) { _name = name; smartUpdate("name", name); } } private void initDataListener() { if (_dataListener == null) _dataListener = new ListDataListener() { public void onChange(ListDataEvent event) { switch (event.getType()) { case ListDataEvent.SELECTION_CHANGED: doSelectionChanged(); return; //nothing changed so need to rerender case ListDataEvent.MULTIPLE_CHANGED: return; //nothing to do } postOnInitRender(); } }; _model.addListDataListener(_dataListener); } private void doSelectionChanged() { final Selectable<Object> smodel = getSelectableModel(); if (smodel.isSelectionEmpty()) { if (_jsel >= 0) setSelectedIndex(-1); return; } if (_jsel >= 0 && smodel.isSelected(_model.getElementAt(_jsel))) return; //nothing changed for (int i = 0, sz = _model.getSize(); i < sz; i++) { if (smodel.isSelected(_model.getElementAt(i))) { setSelectedIndex(i); return; //done } } setSelectedIndex(-1); //just in case } @SuppressWarnings("unchecked") private Selectable<Object> getSelectableModel() { return (Selectable<Object>) _model; } /** * Sets the list model associated with this selectbox. 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 */ 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); } _model = model; _jsel = -1; //Bug ZK-1418: clear select index since model is changed. initDataListener(); postOnInitRender(); } } else if (_model != null) { _model.removeListDataListener(_dataListener); _model = null; invalidate(); } } public void onInitRender() { removeAttribute(ATTR_ON_INIT_RENDER_POSTED); onInitRenderNow(); invalidate(); } public void onInitRenderNow() { if (_model != null) { _tmpdatas = new String[_model.getSize()]; final boolean old = _childable; try { _childable = true; final ItemRenderer<Object> renderer = getRealRenderer(); final Selectable<Object> smodel = getSelectableModel(); _jsel = -1; for (int i = 0, sz = _model.getSize(); i < sz; i++) { final Object value = _model.getElementAt(i); if (_jsel < 0 && smodel.isSelected(value)) _jsel = i; _tmpdatas[i] = renderer.render(this, value, i); } } catch (Exception e) { throw UiException.Aide.wrap(e); } finally { //clear possible children created in renderer _childable = old; getChildren().clear(); } } } private void postOnInitRender() { if (getAttribute(ATTR_ON_INIT_RENDER_POSTED) == null) { setAttribute(ATTR_ON_INIT_RENDER_POSTED, Boolean.TRUE); Events.postEvent("onInitRender", this, null); } } /** * Returns the model associated with this selectbox, or null if this * selectbox is not associated with any list data model. */ @SuppressWarnings({ "unchecked", "rawtypes" }) public <T> ListModel<T> getModel() { return (ListModel) _model; } @SuppressWarnings({ "unchecked", "rawtypes" }) public <T> ItemRenderer<T> getRealRenderer() { final ItemRenderer renderer = getItemRenderer(); return renderer != null ? renderer : _defRend; } private static final ItemRenderer<Object> _defRend = new ItemRenderer<Object>() { public String render(final Component owner, final Object data, final int index) { final Selectbox self = (Selectbox) owner; final Template tm = self.getTemplate("model"); if (tm == null) return Objects.toString(data); else { final Component[] items = ShadowElementsCtrl .filterOutShadows(tm.create(owner, null, 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 ((Selectbox) owner).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); if (!(items[0] instanceof Label)) throw new UiException("The model template can only support Label component, not " + items[0]); items[0].detach(); //remove the label from owner return ((Label) items[0]).getValue(); } } }; // -- ComponentCtrl --// public void invalidate() { // post onInitRender to rerender content if not done it before prepareDatas(); super.invalidate(); } // ZK-948 need render data when change parent or attach to page public void setParent(Component parent) { super.setParent(parent); if (parent != null) { prepareDatas(); } } // ZK-948 public void onPageAttached(Page newpage, Page oldpage) { super.onPageAttached(newpage, oldpage); prepareDatas(); } private void prepareDatas() { if (_tmpdatas == null && _model != null && _model.getSize() > 0) { // post onInitRender to rerender content postOnInitRender(); } } protected void renderProperties(org.zkoss.zk.ui.sys.ContentRenderer renderer) throws java.io.IOException { super.renderProperties(renderer); render(renderer, "name", _name); render(renderer, "disabled", isDisabled()); renderer.render("selectedIndex", _jsel); if (_tmpdatas != null) { render(renderer, "items", _tmpdatas); // B65-ZK-2379 _tmpdatas = null; //purge the data } } @SuppressWarnings("unchecked") public void service(org.zkoss.zk.au.AuRequest request, boolean everError) { final String cmd = request.getCommand(); if (cmd.equals(Events.ON_SELECT)) { // ZK-2148: should check if model exists final Object prevSelected = _jsel >= 0 && _model != null ? _model.getElementAt(_jsel) : null; _jsel = ((Integer) request.getData().get("")).intValue(); final Integer index = ((Integer) request.getData().get("")); final Set<Object> selObjs = new LinkedHashSet<Object>(); // ZK-2148: should check if model exists if (_model != null) { if (index >= 0) selObjs.add(_model.getElementAt(index)); getSelectableModel().setSelection(selObjs); } if (prevSelected != null) { final Set<Object> prevSeldObjs = new LinkedHashSet<Object>(1); prevSeldObjs.add(prevSelected); Events.postEvent(new SelectEvent(Events.ON_SELECT, this, null, null, null, selObjs, prevSeldObjs, prevSeldObjs, null, index, 0)); } else { Events.postEvent( new SelectEvent(Events.ON_SELECT, this, null, null, null, selObjs, null, null, null, index, 0)); } } else // ZK-1053 super.service(request, everError); } // Cloneable// @SuppressWarnings("rawtypes") public Object clone() { final Selectbox clone = (Selectbox) super.clone(); 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.postOnInitRender(); // we use the same data model but we have to create a new listener clone._dataListener = null; clone.initDataListener(); } return clone; } // -- Serializable --// // NOTE: they must be declared as private private synchronized void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException { s.defaultWriteObject(); willSerialize(_model); s.writeObject( _model instanceof java.io.Serializable || _model instanceof java.io.Externalizable ? _model : null); willSerialize(_renderer); s.writeObject(_renderer instanceof java.io.Serializable || _renderer instanceof java.io.Externalizable ? _renderer : null); } @SuppressWarnings("rawtypes") private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { s.defaultReadObject(); _model = (ListModel) s.readObject(); didDeserialize(_model); _renderer = (ItemRenderer) s.readObject(); didDeserialize(_renderer); if (_model != null) { initDataListener(); } } public void sessionWillPassivate(Page page) { super.sessionWillPassivate(page); willPassivate(_model); willPassivate(_renderer); } public void sessionDidActivate(Page page) { super.sessionDidActivate(page); didActivate(_model); didActivate(_renderer); if (_model != null) postOnInitRender(); } 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 HtmlBasedComponent.ExtraCtrl implements Blockable { public boolean shallBlock(AuRequest request) { return isDisabled() || !Components.isRealVisible(Selectbox.this); } } }