Classes in this File | Line Coverage | Branch Coverage | Complexity | ||||||||
Palette |
|
| 1.6285714285714286;1.629 |
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 | 1 | 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 | 1 | Required required = (Required)validator; |
303 | ||
304 | 1 | JSONObject profile = context.getProfile(); |
305 | ||
306 | 1 | if (!profile.has(ValidationConstants.CONSTRAINTS)) { |
307 | 1 | profile.put(ValidationConstants.CONSTRAINTS, new JSONObject()); |
308 | } |
|
309 | 1 | JSONObject cons = profile.getJSONObject(ValidationConstants.CONSTRAINTS); |
310 | ||
311 | 1 | required.accumulateProperty(cons, getClientId(), |
312 | new JSONLiteral("[tapestry.form.validation.isPalleteSelected]")); |
|
313 | ||
314 | 1 | required.accumulateProfileProperty(this, profile, |
315 | ValidationConstants.CONSTRAINTS, required.buildMessage(context, this)); |
|
316 | 1 | } |
317 | ||
318 | /** |
|
319 | * {@inheritDoc} |
|
320 | */ |
|
321 | public boolean overrideValidator(Validator validator, IRequestCycle cycle) |
|
322 | { |
|
323 | 2 | if (Required.class.isAssignableFrom(validator.getClass())) |
324 | 1 | 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 | } |