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 }