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>&nbsp;</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>&nbsp;</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 &lt;select&gt; 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 &lt;th&gt; reserved for the
116 * title above the selected items &lt;select&gt; (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>&nbsp;</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 (&lt;select&gt; and &lt;button&gt; 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}