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>&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    
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 (&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    }