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