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 <select> element's widths are 200px, and it is common to override this to a specific value: 048 * <p/> 049 * <pre> 050 * <style> 051 * DIV.palette SELECT { width: 300px; } 052 * </style> 053 * </pre> 054 * <p/> 055 * You'll want to ensure that both <select> 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}