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