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