    001// Licensed under the Apache License, Version 2.0 (the "License");
    002// you may not use this file except in compliance with the License.
    003// You may obtain a copy of the License at
    004//
    005// http://www.apache.org/licenses/LICENSE-2.0
    006//
    007// Unless required by applicable law or agreed to in writing, software
    008// distributed under the License is distributed on an "AS IS" BASIS,
    009// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    010// See the License for the specific language governing permissions and
    011// limitations under the License.
    012
    013package org.apache.tapestry5.corelib.components;
    014
    015import org.apache.tapestry5.*;
    016import org.apache.tapestry5.annotations.Import;
    017import org.apache.tapestry5.annotations.Parameter;
    018import org.apache.tapestry5.annotations.Property;
    019import org.apache.tapestry5.corelib.base.AbstractField;
    020import org.apache.tapestry5.internal.util.SelectModelRenderer;
    021import org.apache.tapestry5.ioc.annotations.Inject;
    022import org.apache.tapestry5.ioc.annotations.Symbol;
    023import org.apache.tapestry5.json.JSONArray;
    024import org.apache.tapestry5.services.compatibility.DeprecationWarning;
    025
    026import java.util.Collection;
    027
    028/**
    029 * Multiple selection component. Generates a UI consisting of two &lt;select&gt; elements configured for multiple
    030 * selection; the one on the left is the list of "available" elements, the one on the right is "selected". Elements can
    031 * be moved between the lists by clicking a button, or double clicking an option (and eventually, via drag and drop).
    032 *
    033 * The items in the available list are kept ordered as per {@link SelectModel} order. When items are moved from the
    034 * selected list to the available list, they items are inserted back into their proper positions.
    035 *
    036 * The Palette may operate in normal or re-orderable mode, controlled by the reorder parameter.
    037 *
    038 * In normal mode, the items in the selected list are kept in the same "natural" order as the items in the available
    039 * list.
    040 *
    041 * In re-order mode, items moved to the selected list are simply added to the bottom of the list. In addition, two extra
    042 * buttons appear to move items up and down within the selected list.
    043 *
    044 * Much of the look and feel is driven by CSS, the default Tapestry CSS is used to set up the columns, etc. By default,
    045 * the &lt;select&gt; element's widths are 200px, and it is common to override this to a specific value:
    046 *
    047 * <pre>
    048 * &lt;style&gt;
    049 *   DIV.palette SELECT { width: 300px; }
    050 * &lt;/style&gt;
    051 * </pre>
    052 *
    053 * You'll want to ensure that both &lt;select&gt; in each column is the same width, otherwise the display will update
    054 * poorly as options are moved from one column to the other.
    055 *
    056 * Option groups within the {@link SelectModel} will be rendered, but are not supported by many browsers, and are not
    057 * fully handled on the client side.
    058 *
    059 * For an alternative component that can be used for similar purposes, see
    060 * {@link Checklist}.
    061 * Starting in 5.4, the selected parameter may be any kind of collection, but is typically a List if the Palette is configured for re-ordering,
    062 * and a Set if order does not matter (though it is common to use a List in the latter case as well). Also, starting in 5.4,
    063 * the Palette is compatible with the {@link org.apache.tapestry5.validator.Required} validator (on both client and server-side), and
    064 * triggers new events that allows the application to veto a proposed changed to the selection (see the {@code t5/core/events} module).
    065 *
    066 * @tapestrydoc
    067 * @see Form
    068 * @see Select
    069 */
    070@Import(stylesheet = "Palette.css")
    071public class Palette extends AbstractField
    072{
    073    /**
    074     * The image to use for the deselect button (the default is a left pointing arrow).
    075     */
    076    @Parameter
    077    private Asset deselect;
    078
    079    /**
    080     * A ValueEncoder used to convert server-side objects (provided from the
    081     * "source" parameter) into unique client-side strings (typically IDs) and
    082     * back. Note: this component does NOT support ValueEncoders configured to
    083     * be provided automatically by Tapestry.
    084     */
    085    @Parameter(required = true, allowNull = false)
    086    private ValueEncoder<Object> encoder;
    087
    088    /**
    089     * Model used to define the values and labels used when rendering.
    090     */
    091    @Parameter(required = true, allowNull = false)
    092    private SelectModel model;
    093
    094    /**
    095     * Allows the title text for the available column (on the left) to be modified. As this is a Block, it can contain
    096     * conditionals and components. The default is the text "Available".
    097     */
    098    @Property(write = false)
    099    @Parameter(required = true, allowNull = false, value = "message:core-palette-available-label", defaultPrefix = BindingConstants.LITERAL)
    100    private Block availableLabel;
    101
    102    /**
    103     * Allows the title text for the selected column (on the right) to be modified. As this is a Block, it can contain
    104     * conditionals and components. The default is the text "Available".
    105     */
    106    @Property(write = false)
    107    @Parameter(required = true, allowNull = false, value = "message:core-palette-selected-label", defaultPrefix = BindingConstants.LITERAL)
    108    private Block selectedLabel;
    109
    110    /**
    111     * The image to use for the move down button (the default is a downward pointing arrow).
    112     */
    113    @Parameter
    114    private Asset moveDown;
    115
    116    /**
    117     * The image to use for the move up button (the default is an upward pointing arrow).
    118     */
    119    @Parameter
    120    private Asset moveUp;
    121
    122    /**
    123     * The image to use for the select button (the default is a right pointing arrow).
    124     */
    125    @Parameter
    126    private Asset select;
    127
    128    /**
    129     * The list of selected values from the {@link org.apache.tapestry5.SelectModel}. This will be updated when the form
    130     * is submitted. If the value for the parameter is null, a new list will be created, otherwise the existing list
    131     * will be cleared. If unbound, defaults to a property of the container matching this component's id.
    132     *
    133     * Prior to Tapestry 5.4, this allowed null, and a list would be created when the form was submitted. Starting
    134     * with 5.4, the selected list may not be null, and it need not be a list (it may be, for example, a set).
    135     */
    136    @Parameter(required = true, autoconnect = true, allowNull = false)
    137    private Collection<Object> selected;
    138
    139    /**
    140     * If true, then additional buttons are provided on the client-side to allow for re-ordering of the values.
    141     * This is only useful when the selected parameter is bound to a {@code List}, rather than a {@code Set} or other
    142     * unordered collection.
    143     */
    144    @Parameter("false")
    145    @Property(write = false)
    146    private boolean reorder;
    147
    148
    149    /**
    150     * Number of rows to display.
    151     */
    152    @Property(write = false)
    153    @Parameter(value = BindingConstants.SYMBOL + ":" + ComponentParameterConstants.PALETTE_ROWS_SIZE)
    154    private int size;
    155
    156    /**
    157     * The object that will perform input validation. The validate binding prefix is generally used to provide
    158     * this object in a declarative fashion.
    159     *
    160     * @since 5.2.0
    161     */
    162    @Parameter(defaultPrefix = BindingConstants.VALIDATE)
    163    @SuppressWarnings("unchecked")
    164    private FieldValidator<Object> validate;
    165
    166    @Inject
    167    @Symbol(SymbolConstants.COMPACT_JSON)
    168    private boolean compactJSON;
    169
    170    @Inject
    171    private DeprecationWarning deprecationWarning;
    172
    173    void pageLoaded() {
    174        deprecationWarning.ignoredComponentParameters(resources, "select", "moveUp", "moveDown", "deselect");
    175    }
    176
    177    @Property(write = false)
    178    private final Renderable mainRenderer = new Renderable()
    179    {
    180        public void render(MarkupWriter writer)
    181        {
    182            SelectModelRenderer visitor = new SelectModelRenderer(writer, encoder, false);
    183
    184            model.visit(visitor);
    185        }
    186    };
    187
    188    public String getInitialJSON()
    189    {
    190        JSONArray array = new JSONArray();
    191
    192        for (Object o : selected)
    193        {
    194            String value = encoder.toClient(o);
    195            array.put(value);
    196        }
    197
    198        return array.toString(compactJSON);
    199    }
    200
    201
    202    @Override
    203    protected void processSubmission(String controlName)
    204    {
    205        String parameterValue = request.getParameter(controlName);
    206
    207        JSONArray values = new JSONArray(parameterValue);
    208
    209        // Use a couple of local variables to cut down on access via bindings
    210
    211        Collection<Object> selected = this.selected;
    212
    213        selected.clear();
    214
    215        ValueEncoder encoder = this.encoder;
    216
    217        // TODO: Validation error if the model does not contain a value.
    218
    219        int count = values.length();
    220        for (int i = 0; i < count; i++)
    221        {
    222            String value = values.getString(i);
    223
    224            Object objectValue = encoder.toValue(value);
    225
    226            selected.add(objectValue);
    227        }
    228
    229        putPropertyNameIntoBeanValidationContext("selected");
    230
    231        try
    232        {
    233            fieldValidationSupport.validate(selected, resources, validate);
    234
    235            this.selected = selected;
    236        } catch (final ValidationException e)
    237        {
    238            validationTracker.recordError(this, e.getMessage());
    239        }
    240
    241        removePropertyNameFromBeanValidationContext();
    242    }
    243
    244    void beginRender()
    245    {
    246        String clientId = getClientId();
    247
    248        // The client side just need to know the id of the selected (right column) select;
    249        // it can take it from there.
    250        javaScriptSupport.require("t5/core/palette").with(clientId);
    251    }
    252
    253    /**
    254     * Prevent the body from rendering.
    255     */
    256    boolean beforeRenderBody()
    257    {
    258        return false;
    259    }
    260
    261    /**
    262     * Computes a default value for the "validate" parameter using
    263     * {@link org.apache.tapestry5.services.FieldValidatorDefaultSource}.
    264     */
    265    Binding defaultValidate()
    266    {
    267        return this.defaultProvider.defaultValidatorBinding("selected", this.resources);
    268    }
    269
    270    String toClient(Object value)
    271    {
    272        return encoder.toClient(value);
    273    }
    274
    275
    276    @Override
    277    public boolean isRequired()
    278    {
    279        return validate.isRequired();
    280    }
    281
    282    public String getDisabledValue()
    283    {
    284        return disabled ? "disabled" : null;
    285    }
    286
    287    void onBeginRenderFromSelected(MarkupWriter writer)
    288    {
    289        validate.render(writer);
    290    }
    291}