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.IPropertySelectionModel;
035 import org.apache.tapestry.form.ValidatableField;
036 import org.apache.tapestry.form.ValidatableFieldSupport;
037 import org.apache.tapestry.valid.IValidationDelegate;
038 import org.apache.tapestry.valid.ValidationConstants;
039 import 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
210 public 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 }