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.FormComponentContributorContext;
035    import org.apache.tapestry.form.IPropertySelectionModel;
036    import org.apache.tapestry.form.ValidatableFieldExtension;
037    import org.apache.tapestry.form.ValidatableFieldSupport;
038    import org.apache.tapestry.form.validator.Required;
039    import org.apache.tapestry.form.validator.Validator;
040    import org.apache.tapestry.html.Body;
041    import org.apache.tapestry.json.JSONLiteral;
042    import org.apache.tapestry.json.JSONObject;
043    import org.apache.tapestry.valid.IValidationDelegate;
044    import org.apache.tapestry.valid.ValidationConstants;
045    import org.apache.tapestry.valid.ValidatorException;
046    
047    /**
048     * A component used to make a number of selections from a list. The general look is a pair of
049     * <select> elements. with a pair of buttons between them. The right element is a list of
050     * values that can be selected. The buttons move values from the right column ("available") to the
051     * left column ("selected").
052     * <p>
053     * This all takes a bit of JavaScript to accomplish (quite a bit), which means a {@link Body}
054     * component must wrap the Palette. If JavaScript is not enabled in the client browser, then the
055     * user will be unable to make (or change) any selections.
056     * <p>
057     * Cross-browser compatibility is not perfect. In some cases, the
058     * {@link org.apache.tapestry.contrib.form.MultiplePropertySelection}component may be a better
059     * choice.
060     * <p>
061     * <table border=1>
062     * <tr>
063     * <td>Parameter</td>
064     * <td>Type</td>
065     * <td>Direction</td>
066     * <td>Required</td>
067     * <td>Default</td>
068     * <td>Description</td>
069     * </tr>
070     * <tr>
071     * <td>selected</td>
072     * <td>{@link List}</td>
073     * <td>in</td>
074     * <td>yes</td>
075     * <td>&nbsp;</td>
076     * <td>A List of selected values. Possible selections are defined by the model; this should be a
077     * subset of the possible values. This may be null when the component is renderred. When the
078     * containing form is submitted, this parameter is updated with a new List of selected objects.
079     * <p>
080     * The order may be set by the user, as well, depending on the sortMode parameter.</td>
081     * </tr>
082     * <tr>
083     * <td>model</td>
084     * <td>{@link IPropertySelectionModel}</td>
085     * <td>in</td>
086     * <td>yes</td>
087     * <td>&nbsp;</td>
088     * <td>Works, as with a {@link org.apache.tapestry.form.PropertySelection}component, to define the
089     * possible values.</td>
090     * </tr>
091     * <tr>
092     * <td>sort</td>
093     * <td>string</td>
094     * <td>in</td>
095     * <td>no</td>
096     * <td>{@link SortMode#NONE}</td>
097     * <td>Controls automatic sorting of the options.</td>
098     * </tr>
099     * <tr>
100     * <td>rows</td>
101     * <td>int</td>
102     * <td>in</td>
103     * <td>no</td>
104     * <td>10</td>
105     * <td>The number of rows that should be visible in the Pallete's &lt;select&gt; elements.</td>
106     * </tr>
107     * <tr>
108     * <td>tableClass</td>
109     * <td>{@link String}</td>
110     * <td>in</td>
111     * <td>no</td>
112     * <td>tapestry-palette</td>
113     * <td>The CSS class for the table which surrounds the other elements of the Palette.</td>
114     * </tr>
115     * <tr>
116     * <td>selectedTitleBlock</td>
117     * <td>{@link Block}</td>
118     * <td>in</td>
119     * <td>no</td>
120     * <td>"Selected"</td>
121     * <td>If specified, allows a {@link Block}to be placed within the &lt;th&gt; reserved for the
122     * title above the selected items &lt;select&gt; (on the right). This allows for images or other
123     * components to be placed there. By default, the simple word <code>Selected</code> is used.</td>
124     * </tr>
125     * <tr>
126     * <td>availableTitleBlock</td>
127     * <td>{@link Block}</td>
128     * <td>in</td>
129     * <td>no</td>
130     * <td>"Available"</td>
131     * <td>As with selectedTitleBlock, but for the left column, of items which are available to be
132     * selected. The default is the word <code>Available</code>.</td>
133     * </tr>
134     * <tr>
135     * <td>selectImage <br>
136     * selectDisabledImage <br>
137     * deselectImage <br>
138     * deselectDisabledImage <br>
139     * upImage <br>
140     * upDisabledImage <br>
141     * downImage <br>
142     * downDisabledImage</td>
143     * <td>{@link IAsset}</td>
144     * <td>in</td>
145     * <td>no</td>
146     * <td>&nbsp;</td>
147     * <td>If any of these are specified then they override the default images provided with the
148     * component. This allows the look and feel to be customized relatively easily.
149     * <p>
150     * The most common reason to replace the images is to deal with backgrounds. The default images are
151     * anti-aliased against a white background. If a colored or patterned background is used, the
152     * default images will have an ugly white fringe. Until all browsers have full support for PNG
153     * (which has a true alpha channel), it is necessary to customize the images to match the
154     * background.</td>
155     * </tr>
156     * </table>
157     * <p>
158     * A Palette requires some CSS entries to render correctly ... especially the middle column, which
159     * contains the two or four buttons for moving selections between the two columns. The width and
160     * alignment of this column must be set using CSS. Additionally, CSS is commonly used to give the
161     * Palette columns a fixed width, and to dress up the titles. Here is an example of some CSS you can
162     * use to format the palette component:
163     * 
164     * <pre>
165     *                             TABLE.tapestry-palette TH
166     *                             {
167     *                               font-size: 9pt;
168     *                               font-weight: bold;
169     *                               color: white;
170     *                               background-color: #330066;
171     *                               text-align: center;
172     *                             }
173     *                            
174     *                             TD.available-cell SELECT
175     *                             {
176     *                               font-weight: normal;
177     *                               background-color: #FFFFFF;
178     *                               width: 200px;
179     *                             }
180     *                             
181     *                             TD.selected-cell SELECT
182     *                             {
183     *                               font-weight: normal;
184     *                               background-color: #FFFFFF;
185     *                               width: 200px;
186     *                             }
187     *                             
188     *                             TABLE.tapestry-palette TD.controls
189     *                             {
190     *                               text-align: center;
191     *                               vertical-align: middle;
192     *                               width: 60px;
193     *                             }
194     * </pre>
195     * 
196     * <p>
197     * As of 4.0, this component can be validated.
198     * </p>
199     * 
200     * @author Howard Lewis Ship
201     */
202    
203    public abstract class Palette extends BaseComponent implements ValidatableFieldExtension
204    {
205        private static final int MAP_SIZE = 7;
206    
207        /**
208         * A set of symbols produced by the Palette script. This is used to provide proper names for
209         * some of the HTML elements (&lt;select&gt; and &lt;button&gt; elements, etc.).
210         */
211        private Map _symbols;
212    
213        /** @since 3.0 * */
214        public abstract void setAvailableColumn(PaletteColumn column);
215    
216        /** @since 3.0 * */
217        public abstract void setSelectedColumn(PaletteColumn column);
218    
219        public abstract void setName(String name);
220    
221        public abstract void setForm(IForm form);
222    
223        /** @since 4.0 */
224        public abstract void setRequiredMessage(String message);
225    
226        protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
227        {
228            // Next few lines of code is similar to AbstractFormComponent (which, alas, extends from
229            // AbstractComponent, not from BaseComponent).
230            IForm form = TapestryUtils.getForm(cycle, this);
231    
232            setForm(form);
233    
234            if (form.wasPrerendered(writer, this))
235                return;
236    
237            IValidationDelegate delegate = form.getDelegate();
238            
239            delegate.setFormComponent(this);
240    
241            form.getElementId(this);
242    
243            if (form.isRewinding())
244            {
245                if (!isDisabled())
246                {
247                    rewindFormComponent(writer, cycle);
248                }
249            }
250            else if (!cycle.isRewinding())
251            {
252                if (!isDisabled())
253                    delegate.registerForFocus(this, ValidationConstants.NORMAL_FIELD);
254    
255                renderFormComponent(writer, cycle);
256    
257                if (delegate.isInError())
258                    delegate.registerForFocus(this, ValidationConstants.ERROR_FIELD);
259            }
260    
261            super.renderComponent(writer, cycle);
262        }
263    
264        protected void renderFormComponent(IMarkupWriter writer, IRequestCycle cycle)
265        {
266            _symbols = new HashMap(MAP_SIZE);
267    
268            runScript(cycle);
269    
270            constructColumns();
271    
272            getValidatableFieldSupport().renderContributions(this, writer, cycle);
273        }
274    
275        protected void rewindFormComponent(IMarkupWriter writer, IRequestCycle cycle)
276        {
277            String[] values = cycle.getParameters(getName());
278    
279            int count = Tapestry.size(values);
280    
281            List selected = new ArrayList(count);
282            IPropertySelectionModel model = getModel();
283    
284            for (int i = 0; i < count; i++)
285            {
286                String value = values[i];
287                Object option = model.translateValue(value);
288    
289                selected.add(option);
290            }
291    
292            setSelected(selected);
293    
294            try
295            {
296                getValidatableFieldSupport().validate(this, writer, cycle, selected);
297            }
298            catch (ValidatorException e)
299            {
300                getForm().getDelegate().record(e);
301            }
302        }
303        
304        /** 
305         * {@inheritDoc}
306         */
307        public void overrideContributions(Validator validator, FormComponentContributorContext context,
308                IMarkupWriter writer, IRequestCycle cycle)
309        {
310            // we know this has to be a Required validator
311            Required required = (Required)validator;
312            
313            JSONObject profile = context.getProfile();
314            
315            if (!profile.has(ValidationConstants.CONSTRAINTS)) {
316                profile.put(ValidationConstants.CONSTRAINTS, new JSONObject());
317            }
318            JSONObject cons = profile.getJSONObject(ValidationConstants.CONSTRAINTS);
319            
320            required.accumulateProperty(cons, getClientId(), 
321                    new JSONLiteral("[tapestry.form.validation.isPalleteSelected]"));
322            
323            required.accumulateProfileProperty(this, profile, 
324                    ValidationConstants.CONSTRAINTS, required.buildMessage(context, this));
325        }
326        
327        /** 
328         * {@inheritDoc}
329         */
330        public boolean overrideValidator(Validator validator, IRequestCycle cycle)
331        {
332            if (Required.class.isAssignableFrom(validator.getClass()))
333                return true;
334            
335            return false;
336        }
337    
338        protected void cleanupAfterRender(IRequestCycle cycle)
339        {
340            _symbols = null;
341    
342            setAvailableColumn(null);
343            setSelectedColumn(null);
344    
345            super.cleanupAfterRender(cycle);
346        }
347    
348        /**
349         * Executes the associated script, which generates all the JavaScript to support this Palette.
350         */
351        private void runScript(IRequestCycle cycle)
352        {
353            PageRenderSupport pageRenderSupport = TapestryUtils.getPageRenderSupport(cycle, this);
354    
355            setImage(pageRenderSupport, cycle, "selectImage", getSelectImage());
356            setImage(pageRenderSupport, cycle, "selectDisabledImage", getSelectDisabledImage());
357            setImage(pageRenderSupport, cycle, "deselectImage", getDeselectImage());
358            setImage(pageRenderSupport, cycle, "deselectDisabledImage", getDeselectDisabledImage());
359    
360            if (isSortUser())
361            {
362                setImage(pageRenderSupport, cycle, "upImage", getUpImage());
363                setImage(pageRenderSupport, cycle, "upDisabledImage", getUpDisabledImage());
364                setImage(pageRenderSupport, cycle, "downImage", getDownImage());
365                setImage(pageRenderSupport, cycle, "downDisabledImage", getDownDisabledImage());
366            }
367    
368            _symbols.put("palette", this);
369    
370            getScript().execute(this, cycle, pageRenderSupport, _symbols);
371        }
372    
373        /**
374         * Extracts its asset URL, sets it up for preloading, and assigns the preload reference as a
375         * script symbol.
376         */
377        private void setImage(PageRenderSupport pageRenderSupport, IRequestCycle cycle,
378                String symbolName, IAsset asset)
379        {
380            String url = asset.buildURL();
381            String reference = pageRenderSupport.getPreloadedImageReference(this, url);
382    
383            _symbols.put(symbolName, reference);
384        }
385    
386        public Map getSymbols()
387        {
388            return _symbols;
389        }
390    
391        /**
392         * Constructs a pair of {@link PaletteColumn}s: the available and selected options.
393         */
394        private void constructColumns()
395        {
396            // Build a Set around the list of selected items.
397    
398            List selected = getSelected();
399    
400            if (selected == null)
401                selected = Collections.EMPTY_LIST;
402    
403            String sortMode = getSort();
404    
405            boolean sortUser = sortMode.equals(SortMode.USER);
406    
407            List selectedOptions = null;
408    
409            if (sortUser)
410            {
411                int count = selected.size();
412                selectedOptions = new ArrayList(count);
413    
414                for (int i = 0; i < count; i++)
415                    selectedOptions.add(null);
416            }
417    
418            PaletteColumn availableColumn = new PaletteColumn((String) _symbols.get("availableName"),
419                    (String)_symbols.get("availableName"), getRows());
420            PaletteColumn selectedColumn = new PaletteColumn(getName(), getClientId(), getRows());
421    
422            // Each value specified in the model will go into either the selected or available
423            // lists.
424    
425            IPropertySelectionModel model = getModel();
426    
427            int count = model.getOptionCount();
428    
429            for (int i = 0; i < count; i++)
430            {
431                Object optionValue = model.getOption(i);
432    
433                PaletteOption o = new PaletteOption(model.getValue(i), model.getLabel(i));
434    
435                int index = selected.indexOf(optionValue);
436                boolean isSelected = index >= 0;
437    
438                if (sortUser && isSelected)
439                {
440                    selectedOptions.set(index, o);
441                    continue;
442                }
443    
444                PaletteColumn c = isSelected ? selectedColumn : availableColumn;
445    
446                c.addOption(o);
447            }
448    
449            if (sortUser)
450            {
451                Iterator i = selectedOptions.iterator();
452                while (i.hasNext())
453                {
454                    PaletteOption o = (PaletteOption) i.next();
455                    selectedColumn.addOption(o);
456                }
457            }
458    
459            if (sortMode.equals(SortMode.VALUE))
460            {
461                availableColumn.sortByValue();
462                selectedColumn.sortByValue();
463            }
464            else if (sortMode.equals(SortMode.LABEL))
465            {
466                availableColumn.sortByLabel();
467                selectedColumn.sortByLabel();
468            }
469    
470            setAvailableColumn(availableColumn);
471            setSelectedColumn(selectedColumn);
472        }
473    
474        public boolean isSortUser()
475        {
476            return getSort().equals(SortMode.USER);
477        }
478    
479        public abstract Block getAvailableTitleBlock();
480    
481        public abstract IAsset getDeselectDisabledImage();
482    
483        public abstract IAsset getDeselectImage();
484    
485        public abstract IAsset getDownDisabledImage();
486    
487        public abstract IAsset getDownImage();
488    
489        public abstract IAsset getSelectDisabledImage();
490    
491        public abstract IPropertySelectionModel getModel();
492    
493        public abstract int getRows();
494    
495        public abstract Block getSelectedTitleBlock();
496    
497        public abstract IAsset getSelectImage();
498    
499        public abstract String getSort();
500    
501        public abstract IAsset getUpDisabledImage();
502    
503        public abstract IAsset getUpImage();
504    
505        /**
506         * Returns false. Palette components are never disabled.
507         * 
508         * @since 2.2
509         */
510        public boolean isDisabled()
511        {
512            return false;
513        }
514    
515        /** @since 2.2 * */
516    
517        public abstract List getSelected();
518    
519        /** @since 2.2 * */
520    
521        public abstract void setSelected(List selected);
522    
523        /**
524         * Injected.
525         * 
526         * @since 4.0
527         */
528        public abstract IScript getScript();
529    
530        /**
531         * Injected.
532         * 
533         * @since 4.0
534         */
535        public abstract ValidatableFieldSupport getValidatableFieldSupport();
536    
537        /**
538         * @see org.apache.tapestry.form.AbstractFormComponent#isRequired()
539         */
540        public boolean isRequired()
541        {
542            return getValidatableFieldSupport().isRequired(this);
543        }
544    }