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