Classes in this File | Line Coverage | Branch Coverage | Complexity | ||||
Palette |
|
| 1.6857142857142857;1.686 |
1 | // Copyright 2004, 2005 The Apache Software Foundation | |
2 | // | |
3 | // Licensed under the Apache License, Version 2.0 (the "License"); | |
4 | // you may not use this file except in compliance with the License. | |
5 | // You may obtain a copy of the License at | |
6 | // | |
7 | // http://www.apache.org/licenses/LICENSE-2.0 | |
8 | // | |
9 | // Unless required by applicable law or agreed to in writing, software | |
10 | // distributed under the License is distributed on an "AS IS" BASIS, | |
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
12 | // See the License for the specific language governing permissions and | |
13 | // limitations under the License. | |
14 | ||
15 | package org.apache.tapestry.contrib.palette; | |
16 | ||
17 | import org.apache.tapestry.*; | |
18 | import org.apache.tapestry.components.Block; | |
19 | import org.apache.tapestry.form.FormComponentContributorContext; | |
20 | import org.apache.tapestry.form.IPropertySelectionModel; | |
21 | import org.apache.tapestry.form.ValidatableFieldExtension; | |
22 | import org.apache.tapestry.form.ValidatableFieldSupport; | |
23 | import org.apache.tapestry.form.validator.Required; | |
24 | import org.apache.tapestry.form.validator.Validator; | |
25 | import org.apache.tapestry.html.Body; | |
26 | import org.apache.tapestry.json.JSONLiteral; | |
27 | import org.apache.tapestry.json.JSONObject; | |
28 | import org.apache.tapestry.valid.IValidationDelegate; | |
29 | import org.apache.tapestry.valid.ValidationConstants; | |
30 | import org.apache.tapestry.valid.ValidatorException; | |
31 | ||
32 | import java.util.*; | |
33 | ||
34 | /** | |
35 | * A component used to make a number of selections from a list. The general look is a pair of | |
36 | * <select> elements. with a pair of buttons between them. The right element is a list of | |
37 | * values that can be selected. The buttons move values from the right column ("available") to the | |
38 | * left column ("selected"). | |
39 | * <p> | |
40 | * This all takes a bit of JavaScript to accomplish (quite a bit), which means a {@link Body} | |
41 | * component must wrap the Palette. If JavaScript is not enabled in the client browser, then the | |
42 | * user will be unable to make (or change) any selections. | |
43 | * <p> | |
44 | * Cross-browser compatibility is not perfect. In some cases, the | |
45 | * {@link org.apache.tapestry.contrib.form.MultiplePropertySelection}component may be a better | |
46 | * choice. | |
47 | * <p> | |
48 | * <table border=1> | |
49 | * <tr> | |
50 | * <td>Parameter</td> | |
51 | * <td>Type</td> | |
52 | * <td>Direction</td> | |
53 | * <td>Required</td> | |
54 | * <td>Default</td> | |
55 | * <td>Description</td> | |
56 | * </tr> | |
57 | * <tr> | |
58 | * <td>selected</td> | |
59 | * <td>{@link List}</td> | |
60 | * <td>in</td> | |
61 | * <td>yes</td> | |
62 | * <td> </td> | |
63 | * <td>A List of selected values. Possible selections are defined by the model; this should be a | |
64 | * subset of the possible values. This may be null when the component is renderred. When the | |
65 | * containing form is submitted, this parameter is updated with a new List of selected objects. | |
66 | * <p> | |
67 | * The order may be set by the user, as well, depending on the sortMode parameter.</td> | |
68 | * </tr> | |
69 | * <tr> | |
70 | * <td>model</td> | |
71 | * <td>{@link IPropertySelectionModel}</td> | |
72 | * <td>in</td> | |
73 | * <td>yes</td> | |
74 | * <td> </td> | |
75 | * <td>Works, as with a {@link org.apache.tapestry.form.PropertySelection}component, to define the | |
76 | * possible values.</td> | |
77 | * </tr> | |
78 | * <tr> | |
79 | * <td>sort</td> | |
80 | * <td>string</td> | |
81 | * <td>in</td> | |
82 | * <td>no</td> | |
83 | * <td>{@link SortMode#NONE}</td> | |
84 | * <td>Controls automatic sorting of the options.</td> | |
85 | * </tr> | |
86 | * <tr> | |
87 | * <td>rows</td> | |
88 | * <td>int</td> | |
89 | * <td>in</td> | |
90 | * <td>no</td> | |
91 | * <td>10</td> | |
92 | * <td>The number of rows that should be visible in the Pallete's <select> elements.</td> | |
93 | * </tr> | |
94 | * <tr> | |
95 | * <td>tableClass</td> | |
96 | * <td>{@link String}</td> | |
97 | * <td>in</td> | |
98 | * <td>no</td> | |
99 | * <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 | 0 | 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 | 0 | IForm form = TapestryUtils.getForm(cycle, this); |
218 | ||
219 | 0 | setForm(form); |
220 | ||
221 | 0 | if (form.wasPrerendered(writer, this)) |
222 | 0 | return; |
223 | ||
224 | 0 | IValidationDelegate delegate = form.getDelegate(); |
225 | ||
226 | 0 | delegate.setFormComponent(this); |
227 | ||
228 | 0 | form.getElementId(this); |
229 | ||
230 | 0 | if (form.isRewinding()) |
231 | { | |
232 | 0 | if (!isDisabled()) |
233 | { | |
234 | 0 | rewindFormComponent(writer, cycle); |
235 | } | |
236 | } | |
237 | 0 | else if (!cycle.isRewinding()) |
238 | { | |
239 | 0 | if (!isDisabled()) |
240 | 0 | delegate.registerForFocus(this, ValidationConstants.NORMAL_FIELD); |
241 | ||
242 | 0 | renderFormComponent(writer, cycle); |
243 | ||
244 | 0 | if (delegate.isInError()) |
245 | 0 | delegate.registerForFocus(this, ValidationConstants.ERROR_FIELD); |
246 | } | |
247 | ||
248 | 0 | super.renderComponent(writer, cycle); |
249 | 0 | } |
250 | ||
251 | protected void renderFormComponent(IMarkupWriter writer, IRequestCycle cycle) | |
252 | { | |
253 | 0 | _symbols = new HashMap(MAP_SIZE); |
254 | ||
255 | 0 | getForm().getDelegate().writePrefix(writer, cycle, this, null); |
256 | ||
257 | 0 | runScript(cycle); |
258 | ||
259 | 0 | constructColumns(); |
260 | ||
261 | 0 | getValidatableFieldSupport().renderContributions(this, writer, cycle); |
262 | ||
263 | 0 | getForm().getDelegate().writeSuffix(writer, cycle, this, null); |
264 | 0 | } |
265 | ||
266 | protected void rewindFormComponent(IMarkupWriter writer, IRequestCycle cycle) | |
267 | { | |
268 | 0 | String[] values = cycle.getParameters(getName()); |
269 | ||
270 | 0 | int count = Tapestry.size(values); |
271 | ||
272 | 0 | List selected = new ArrayList(count); |
273 | 0 | IPropertySelectionModel model = getModel(); |
274 | ||
275 | 0 | for (int i = 0; i < count; i++) |
276 | { | |
277 | 0 | String value = values[i]; |
278 | 0 | Object option = model.translateValue(value); |
279 | ||
280 | 0 | selected.add(option); |
281 | } | |
282 | ||
283 | 0 | setSelected(selected); |
284 | ||
285 | try | |
286 | { | |
287 | 0 | getValidatableFieldSupport().validate(this, writer, cycle, selected); |
288 | } | |
289 | 0 | catch (ValidatorException e) |
290 | { | |
291 | 0 | getForm().getDelegate().record(e); |
292 | 0 | } |
293 | 0 | } |
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 | 0 | Required required = (Required)validator; |
303 | ||
304 | 0 | JSONObject profile = context.getProfile(); |
305 | ||
306 | 0 | if (!profile.has(ValidationConstants.CONSTRAINTS)) { |
307 | 0 | profile.put(ValidationConstants.CONSTRAINTS, new JSONObject()); |
308 | } | |
309 | 0 | JSONObject cons = profile.getJSONObject(ValidationConstants.CONSTRAINTS); |
310 | ||
311 | 0 | required.accumulateProperty(cons, getClientId(), |
312 | new JSONLiteral("[tapestry.form.validation.isPalleteSelected]")); | |
313 | ||
314 | 0 | required.accumulateProfileProperty(this, profile, |
315 | ValidationConstants.CONSTRAINTS, required.buildMessage(context, this)); | |
316 | 0 | } |
317 | ||
318 | /** | |
319 | * {@inheritDoc} | |
320 | */ | |
321 | public boolean overrideValidator(Validator validator, IRequestCycle cycle) | |
322 | { | |
323 | 0 | if (Required.class.isAssignableFrom(validator.getClass())) |
324 | 0 | return true; |
325 | ||
326 | 0 | return false; |
327 | } | |
328 | ||
329 | protected void cleanupAfterRender(IRequestCycle cycle) | |
330 | { | |
331 | 0 | _symbols = null; |
332 | ||
333 | 0 | setAvailableColumn(null); |
334 | 0 | setSelectedColumn(null); |
335 | ||
336 | 0 | super.cleanupAfterRender(cycle); |
337 | 0 | } |
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 | 0 | PageRenderSupport pageRenderSupport = TapestryUtils.getPageRenderSupport(cycle, this); |
345 | ||
346 | 0 | setImage(pageRenderSupport, cycle, "selectImage", getSelectImage()); |
347 | 0 | setImage(pageRenderSupport, cycle, "selectDisabledImage", getSelectDisabledImage()); |
348 | 0 | setImage(pageRenderSupport, cycle, "deselectImage", getDeselectImage()); |
349 | 0 | setImage(pageRenderSupport, cycle, "deselectDisabledImage", getDeselectDisabledImage()); |
350 | ||
351 | 0 | if (isSortUser()) |
352 | { | |
353 | 0 | setImage(pageRenderSupport, cycle, "upImage", getUpImage()); |
354 | 0 | setImage(pageRenderSupport, cycle, "upDisabledImage", getUpDisabledImage()); |
355 | 0 | setImage(pageRenderSupport, cycle, "downImage", getDownImage()); |
356 | 0 | setImage(pageRenderSupport, cycle, "downDisabledImage", getDownDisabledImage()); |
357 | } | |
358 | ||
359 | 0 | _symbols.put("palette", this); |
360 | ||
361 | 0 | getScript().execute(this, cycle, pageRenderSupport, _symbols); |
362 | 0 | } |
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 | 0 | String url = asset.buildURL(); |
372 | 0 | String reference = pageRenderSupport.getPreloadedImageReference(this, url); |
373 | ||
374 | 0 | _symbols.put(symbolName, reference); |
375 | 0 | } |
376 | ||
377 | public Map getSymbols() | |
378 | { | |
379 | 0 | 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 | 0 | List selected = getSelected(); |
390 | ||
391 | 0 | if (selected == null) |
392 | 0 | selected = Collections.EMPTY_LIST; |
393 | ||
394 | 0 | String sortMode = getSort(); |
395 | ||
396 | 0 | boolean sortUser = sortMode.equals(SortMode.USER); |
397 | ||
398 | 0 | List selectedOptions = null; |
399 | ||
400 | 0 | if (sortUser) |
401 | { | |
402 | 0 | int count = selected.size(); |
403 | 0 | selectedOptions = new ArrayList(count); |
404 | ||
405 | 0 | for (int i = 0; i < count; i++) |
406 | 0 | selectedOptions.add(null); |
407 | } | |
408 | ||
409 | 0 | PaletteColumn availableColumn = new PaletteColumn((String) _symbols.get("availableName"), |
410 | (String)_symbols.get("availableName"), getRows()); | |
411 | 0 | 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 | 0 | IPropertySelectionModel model = getModel(); |
417 | ||
418 | 0 | int count = model.getOptionCount(); |
419 | ||
420 | 0 | for (int i = 0; i < count; i++) |
421 | { | |
422 | 0 | Object optionValue = model.getOption(i); |
423 | ||
424 | 0 | PaletteOption o = new PaletteOption(model.getValue(i), model.getLabel(i)); |
425 | ||
426 | 0 | int index = selected.indexOf(optionValue); |
427 | 0 | boolean isSelected = index >= 0; |
428 | ||
429 | 0 | if (sortUser && isSelected) |
430 | { | |
431 | 0 | selectedOptions.set(index, o); |
432 | 0 | continue; |
433 | } | |
434 | ||
435 | 0 | PaletteColumn c = isSelected ? selectedColumn : availableColumn; |
436 | ||
437 | 0 | c.addOption(o); |
438 | } | |
439 | ||
440 | 0 | if (sortUser) |
441 | { | |
442 | 0 | Iterator i = selectedOptions.iterator(); |
443 | 0 | while (i.hasNext()) |
444 | { | |
445 | 0 | PaletteOption o = (PaletteOption) i.next(); |
446 | 0 | selectedColumn.addOption(o); |
447 | 0 | } |
448 | } | |
449 | ||
450 | 0 | if (sortMode.equals(SortMode.VALUE)) |
451 | { | |
452 | 0 | availableColumn.sortByValue(); |
453 | 0 | selectedColumn.sortByValue(); |
454 | } | |
455 | 0 | else if (sortMode.equals(SortMode.LABEL)) |
456 | { | |
457 | 0 | availableColumn.sortByLabel(); |
458 | 0 | selectedColumn.sortByLabel(); |
459 | } | |
460 | ||
461 | 0 | setAvailableColumn(availableColumn); |
462 | 0 | setSelectedColumn(selectedColumn); |
463 | 0 | } |
464 | ||
465 | public boolean isSortUser() | |
466 | { | |
467 | 0 | 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 | 0 | 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 | 0 | return getValidatableFieldSupport().isRequired(this); |
534 | } | |
535 | } |