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 <select> 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 * <p/>
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 * <p/>
036 * The Palette may operate in normal or re-orderable mode, controlled by the reorder parameter.
037 * <p/>
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 * <p/>
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 * <p/>
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 * <p/>
047 * <pre>
048 * &lt;style&gt;
049 *   DIV.palette SELECT { width: 300px; }
050 * &lt;/style&gt;
051 * </pre>
052 * <p/>
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 * <p/>
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 * <p/>
059 * For an alternative component that can be used for similar purposes, see
060 * {@link Checklist}.
061 * <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,
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     * <p/>
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
178    public 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}