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