001 // Copyright 2004, 2005 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 015 package org.apache.tapestry.contrib.palette; 016 017 import org.apache.tapestry.*; 018 import org.apache.tapestry.components.Block; 019 import org.apache.tapestry.form.FormComponentContributorContext; 020 import org.apache.tapestry.form.IPropertySelectionModel; 021 import org.apache.tapestry.form.ValidatableFieldExtension; 022 import org.apache.tapestry.form.ValidatableFieldSupport; 023 import org.apache.tapestry.form.validator.Required; 024 import org.apache.tapestry.form.validator.Validator; 025 import org.apache.tapestry.html.Body; 026 import org.apache.tapestry.json.JSONLiteral; 027 import org.apache.tapestry.json.JSONObject; 028 import org.apache.tapestry.valid.IValidationDelegate; 029 import org.apache.tapestry.valid.ValidationConstants; 030 import org.apache.tapestry.valid.ValidatorException; 031 032 import java.util.*; 033 034 /** 035 * A component used to make a number of selections from a list. The general look is a pair of 036 * <select> elements. with a pair of buttons between them. The right element is a list of 037 * values that can be selected. The buttons move values from the right column ("available") to the 038 * left column ("selected"). 039 * <p> 040 * This all takes a bit of JavaScript to accomplish (quite a bit), which means a {@link Body} 041 * component must wrap the Palette. If JavaScript is not enabled in the client browser, then the 042 * user will be unable to make (or change) any selections. 043 * <p> 044 * Cross-browser compatibility is not perfect. In some cases, the 045 * {@link org.apache.tapestry.contrib.form.MultiplePropertySelection}component may be a better 046 * choice. 047 * <p> 048 * <table border=1> 049 * <tr> 050 * <td>Parameter</td> 051 * <td>Type</td> 052 * <td>Direction</td> 053 * <td>Required</td> 054 * <td>Default</td> 055 * <td>Description</td> 056 * </tr> 057 * <tr> 058 * <td>selected</td> 059 * <td>{@link List}</td> 060 * <td>in</td> 061 * <td>yes</td> 062 * <td> </td> 063 * <td>A List of selected values. Possible selections are defined by the model; this should be a 064 * subset of the possible values. This may be null when the component is renderred. When the 065 * containing form is submitted, this parameter is updated with a new List of selected objects. 066 * <p> 067 * The order may be set by the user, as well, depending on the sortMode parameter.</td> 068 * </tr> 069 * <tr> 070 * <td>model</td> 071 * <td>{@link IPropertySelectionModel}</td> 072 * <td>in</td> 073 * <td>yes</td> 074 * <td> </td> 075 * <td>Works, as with a {@link org.apache.tapestry.form.PropertySelection}component, to define the 076 * possible values.</td> 077 * </tr> 078 * <tr> 079 * <td>sort</td> 080 * <td>string</td> 081 * <td>in</td> 082 * <td>no</td> 083 * <td>{@link SortMode#NONE}</td> 084 * <td>Controls automatic sorting of the options.</td> 085 * </tr> 086 * <tr> 087 * <td>rows</td> 088 * <td>int</td> 089 * <td>in</td> 090 * <td>no</td> 091 * <td>10</td> 092 * <td>The number of rows that should be visible in the Pallete's <select> elements.</td> 093 * </tr> 094 * <tr> 095 * <td>tableClass</td> 096 * <td>{@link String}</td> 097 * <td>in</td> 098 * <td>no</td> 099 * <td>tapestry-palette</td> 100 * <td>The CSS class for the table which surrounds the other elements of the Palette.</td> 101 * </tr> 102 * <tr> 103 * <td>selectedTitleBlock</td> 104 * <td>{@link Block}</td> 105 * <td>in</td> 106 * <td>no</td> 107 * <td>"Selected"</td> 108 * <td>If specified, allows a {@link Block}to be placed within the <th> reserved for the 109 * title above the selected items <select> (on the right). This allows for images or other 110 * components to be placed there. By default, the simple word <code>Selected</code> is used.</td> 111 * </tr> 112 * <tr> 113 * <td>availableTitleBlock</td> 114 * <td>{@link Block}</td> 115 * <td>in</td> 116 * <td>no</td> 117 * <td>"Available"</td> 118 * <td>As with selectedTitleBlock, but for the left column, of items which are available to be 119 * selected. The default is the word <code>Available</code>.</td> 120 * </tr> 121 * <tr> 122 * <td>selectImage <br> 123 * selectDisabledImage <br> 124 * deselectImage <br> 125 * deselectDisabledImage <br> 126 * upImage <br> 127 * upDisabledImage <br> 128 * downImage <br> 129 * downDisabledImage</td> 130 * <td>{@link IAsset}</td> 131 * <td>in</td> 132 * <td>no</td> 133 * <td> </td> 134 * <td>If any of these are specified then they override the default images provided with the 135 * component. This allows the look and feel to be customized relatively easily. 136 * <p> 137 * The most common reason to replace the images is to deal with backgrounds. The default images are 138 * anti-aliased against a white background. If a colored or patterned background is used, the 139 * default images will have an ugly white fringe. Until all browsers have full support for PNG 140 * (which has a true alpha channel), it is necessary to customize the images to match the 141 * background.</td> 142 * </tr> 143 * </table> 144 * <p> 145 * A Palette requires some CSS entries to render correctly ... especially the middle column, which 146 * contains the two or four buttons for moving selections between the two columns. The width and 147 * alignment of this column must be set using CSS. Additionally, CSS is commonly used to give the 148 * Palette columns a fixed width, and to dress up the titles. Here is an example of some CSS you can 149 * use to format the palette component: 150 * 151 * <pre> 152 * TABLE.tapestry-palette TH 153 * { 154 * font-size: 9pt; 155 * font-weight: bold; 156 * color: white; 157 * background-color: #330066; 158 * text-align: center; 159 * } 160 * 161 * TD.available-cell SELECT 162 * { 163 * font-weight: normal; 164 * background-color: #FFFFFF; 165 * width: 200px; 166 * } 167 * 168 * TD.selected-cell SELECT 169 * { 170 * font-weight: normal; 171 * background-color: #FFFFFF; 172 * width: 200px; 173 * } 174 * 175 * TABLE.tapestry-palette TD.controls 176 * { 177 * text-align: center; 178 * vertical-align: middle; 179 * width: 60px; 180 * } 181 * </pre> 182 * 183 * <p> 184 * As of 4.0, this component can be validated. 185 * </p> 186 * 187 * @author Howard Lewis Ship 188 */ 189 190 public abstract class Palette extends BaseComponent implements ValidatableFieldExtension 191 { 192 private static final int MAP_SIZE = 7; 193 194 /** 195 * A set of symbols produced by the Palette script. This is used to provide proper names for 196 * some of the HTML elements (<select> and <button> elements, etc.). 197 */ 198 private Map _symbols; 199 200 /** @since 3.0 * */ 201 public abstract void setAvailableColumn(PaletteColumn column); 202 203 /** @since 3.0 * */ 204 public abstract void setSelectedColumn(PaletteColumn column); 205 206 public abstract void setName(String name); 207 208 public abstract void setForm(IForm form); 209 210 /** @since 4.0 */ 211 public abstract void setRequiredMessage(String message); 212 213 protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle) 214 { 215 // Next few lines of code is similar to AbstractFormComponent (which, alas, extends from 216 // AbstractComponent, not from BaseComponent). 217 IForm form = TapestryUtils.getForm(cycle, this); 218 219 setForm(form); 220 221 if (form.wasPrerendered(writer, this)) 222 return; 223 224 IValidationDelegate delegate = form.getDelegate(); 225 226 delegate.setFormComponent(this); 227 228 form.getElementId(this); 229 230 if (form.isRewinding()) 231 { 232 if (!isDisabled()) 233 { 234 rewindFormComponent(writer, cycle); 235 } 236 } 237 else if (!cycle.isRewinding()) 238 { 239 if (!isDisabled()) 240 delegate.registerForFocus(this, ValidationConstants.NORMAL_FIELD); 241 242 renderFormComponent(writer, cycle); 243 244 if (delegate.isInError()) 245 delegate.registerForFocus(this, ValidationConstants.ERROR_FIELD); 246 } 247 248 super.renderComponent(writer, cycle); 249 } 250 251 protected void renderFormComponent(IMarkupWriter writer, IRequestCycle cycle) 252 { 253 _symbols = new HashMap(MAP_SIZE); 254 255 getForm().getDelegate().writePrefix(writer, cycle, this, null); 256 257 runScript(cycle); 258 259 constructColumns(); 260 261 getValidatableFieldSupport().renderContributions(this, writer, cycle); 262 263 getForm().getDelegate().writeSuffix(writer, cycle, this, null); 264 } 265 266 protected void rewindFormComponent(IMarkupWriter writer, IRequestCycle cycle) 267 { 268 String[] values = cycle.getParameters(getName()); 269 270 int count = Tapestry.size(values); 271 272 List selected = new ArrayList(count); 273 IPropertySelectionModel model = getModel(); 274 275 for (int i = 0; i < count; i++) 276 { 277 String value = values[i]; 278 Object option = model.translateValue(value); 279 280 selected.add(option); 281 } 282 283 setSelected(selected); 284 285 try 286 { 287 getValidatableFieldSupport().validate(this, writer, cycle, selected); 288 } 289 catch (ValidatorException e) 290 { 291 getForm().getDelegate().record(e); 292 } 293 } 294 295 /** 296 * {@inheritDoc} 297 */ 298 public void overrideContributions(Validator validator, FormComponentContributorContext context, 299 IMarkupWriter writer, IRequestCycle cycle) 300 { 301 // we know this has to be a Required validator 302 Required required = (Required)validator; 303 304 JSONObject profile = context.getProfile(); 305 306 if (!profile.has(ValidationConstants.CONSTRAINTS)) { 307 profile.put(ValidationConstants.CONSTRAINTS, new JSONObject()); 308 } 309 JSONObject cons = profile.getJSONObject(ValidationConstants.CONSTRAINTS); 310 311 required.accumulateProperty(cons, getClientId(), 312 new JSONLiteral("[tapestry.form.validation.isPalleteSelected]")); 313 314 required.accumulateProfileProperty(this, profile, 315 ValidationConstants.CONSTRAINTS, required.buildMessage(context, this)); 316 } 317 318 /** 319 * {@inheritDoc} 320 */ 321 public boolean overrideValidator(Validator validator, IRequestCycle cycle) 322 { 323 if (Required.class.isAssignableFrom(validator.getClass())) 324 return true; 325 326 return false; 327 } 328 329 protected void cleanupAfterRender(IRequestCycle cycle) 330 { 331 _symbols = null; 332 333 setAvailableColumn(null); 334 setSelectedColumn(null); 335 336 super.cleanupAfterRender(cycle); 337 } 338 339 /** 340 * Executes the associated script, which generates all the JavaScript to support this Palette. 341 */ 342 private void runScript(IRequestCycle cycle) 343 { 344 PageRenderSupport pageRenderSupport = TapestryUtils.getPageRenderSupport(cycle, this); 345 346 setImage(pageRenderSupport, cycle, "selectImage", getSelectImage()); 347 setImage(pageRenderSupport, cycle, "selectDisabledImage", getSelectDisabledImage()); 348 setImage(pageRenderSupport, cycle, "deselectImage", getDeselectImage()); 349 setImage(pageRenderSupport, cycle, "deselectDisabledImage", getDeselectDisabledImage()); 350 351 if (isSortUser()) 352 { 353 setImage(pageRenderSupport, cycle, "upImage", getUpImage()); 354 setImage(pageRenderSupport, cycle, "upDisabledImage", getUpDisabledImage()); 355 setImage(pageRenderSupport, cycle, "downImage", getDownImage()); 356 setImage(pageRenderSupport, cycle, "downDisabledImage", getDownDisabledImage()); 357 } 358 359 _symbols.put("palette", this); 360 361 getScript().execute(this, cycle, pageRenderSupport, _symbols); 362 } 363 364 /** 365 * Extracts its asset URL, sets it up for preloading, and assigns the preload reference as a 366 * script symbol. 367 */ 368 private void setImage(PageRenderSupport pageRenderSupport, IRequestCycle cycle, 369 String symbolName, IAsset asset) 370 { 371 String url = asset.buildURL(); 372 String reference = pageRenderSupport.getPreloadedImageReference(this, url); 373 374 _symbols.put(symbolName, reference); 375 } 376 377 public Map getSymbols() 378 { 379 return _symbols; 380 } 381 382 /** 383 * Constructs a pair of {@link PaletteColumn}s: the available and selected options. 384 */ 385 private void constructColumns() 386 { 387 // Build a Set around the list of selected items. 388 389 List selected = getSelected(); 390 391 if (selected == null) 392 selected = Collections.EMPTY_LIST; 393 394 String sortMode = getSort(); 395 396 boolean sortUser = sortMode.equals(SortMode.USER); 397 398 List selectedOptions = null; 399 400 if (sortUser) 401 { 402 int count = selected.size(); 403 selectedOptions = new ArrayList(count); 404 405 for (int i = 0; i < count; i++) 406 selectedOptions.add(null); 407 } 408 409 PaletteColumn availableColumn = new PaletteColumn((String) _symbols.get("availableName"), 410 (String)_symbols.get("availableName"), getRows()); 411 PaletteColumn selectedColumn = new PaletteColumn(getName(), getClientId(), getRows()); 412 413 // Each value specified in the model will go into either the selected or available 414 // lists. 415 416 IPropertySelectionModel model = getModel(); 417 418 int count = model.getOptionCount(); 419 420 for (int i = 0; i < count; i++) 421 { 422 Object optionValue = model.getOption(i); 423 424 PaletteOption o = new PaletteOption(model.getValue(i), model.getLabel(i)); 425 426 int index = selected.indexOf(optionValue); 427 boolean isSelected = index >= 0; 428 429 if (sortUser && isSelected) 430 { 431 selectedOptions.set(index, o); 432 continue; 433 } 434 435 PaletteColumn c = isSelected ? selectedColumn : availableColumn; 436 437 c.addOption(o); 438 } 439 440 if (sortUser) 441 { 442 Iterator i = selectedOptions.iterator(); 443 while (i.hasNext()) 444 { 445 PaletteOption o = (PaletteOption) i.next(); 446 selectedColumn.addOption(o); 447 } 448 } 449 450 if (sortMode.equals(SortMode.VALUE)) 451 { 452 availableColumn.sortByValue(); 453 selectedColumn.sortByValue(); 454 } 455 else if (sortMode.equals(SortMode.LABEL)) 456 { 457 availableColumn.sortByLabel(); 458 selectedColumn.sortByLabel(); 459 } 460 461 setAvailableColumn(availableColumn); 462 setSelectedColumn(selectedColumn); 463 } 464 465 public boolean isSortUser() 466 { 467 return getSort().equals(SortMode.USER); 468 } 469 470 public abstract Block getAvailableTitleBlock(); 471 472 public abstract IAsset getDeselectDisabledImage(); 473 474 public abstract IAsset getDeselectImage(); 475 476 public abstract IAsset getDownDisabledImage(); 477 478 public abstract IAsset getDownImage(); 479 480 public abstract IAsset getSelectDisabledImage(); 481 482 public abstract IPropertySelectionModel getModel(); 483 484 public abstract int getRows(); 485 486 public abstract Block getSelectedTitleBlock(); 487 488 public abstract IAsset getSelectImage(); 489 490 public abstract String getSort(); 491 492 public abstract IAsset getUpDisabledImage(); 493 494 public abstract IAsset getUpImage(); 495 496 /** 497 * Returns false. Palette components are never disabled. 498 * 499 * @since 2.2 500 */ 501 public boolean isDisabled() 502 { 503 return false; 504 } 505 506 /** @since 2.2 * */ 507 508 public abstract List getSelected(); 509 510 /** @since 2.2 * */ 511 512 public abstract void setSelected(List selected); 513 514 /** 515 * Injected. 516 * 517 * @since 4.0 518 */ 519 public abstract IScript getScript(); 520 521 /** 522 * Injected. 523 * 524 * @since 4.0 525 */ 526 public abstract ValidatableFieldSupport getValidatableFieldSupport(); 527 528 /** 529 * @see org.apache.tapestry.form.AbstractFormComponent#isRequired() 530 */ 531 public boolean isRequired() 532 { 533 return getValidatableFieldSupport().isRequired(this); 534 } 535 }