| // Copyright 2004, 2005 The Apache Software Foundation |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package org.apache.tapestry.contrib.palette; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| |
| import org.apache.tapestry.BaseComponent; |
| import org.apache.tapestry.IAsset; |
| import org.apache.tapestry.IForm; |
| import org.apache.tapestry.IMarkupWriter; |
| import org.apache.tapestry.IRequestCycle; |
| import org.apache.tapestry.IScript; |
| import org.apache.tapestry.PageRenderSupport; |
| import org.apache.tapestry.Tapestry; |
| import org.apache.tapestry.TapestryUtils; |
| import org.apache.tapestry.components.Block; |
| import org.apache.tapestry.form.FormComponentContributorContext; |
| import org.apache.tapestry.form.IPropertySelectionModel; |
| import org.apache.tapestry.form.ValidatableFieldExtension; |
| import org.apache.tapestry.form.ValidatableFieldSupport; |
| import org.apache.tapestry.form.validator.Required; |
| import org.apache.tapestry.form.validator.Validator; |
| import org.apache.tapestry.html.Body; |
| import org.apache.tapestry.json.JSONLiteral; |
| import org.apache.tapestry.json.JSONObject; |
| import org.apache.tapestry.valid.IValidationDelegate; |
| import org.apache.tapestry.valid.ValidationConstants; |
| import org.apache.tapestry.valid.ValidatorException; |
| |
| /** |
| * A component used to make a number of selections from a list. The general look is a pair of |
| * <select> elements. with a pair of buttons between them. The right element is a list of |
| * values that can be selected. The buttons move values from the right column ("available") to the |
| * left column ("selected"). |
| * <p> |
| * This all takes a bit of JavaScript to accomplish (quite a bit), which means a {@link Body} |
| * component must wrap the Palette. If JavaScript is not enabled in the client browser, then the |
| * user will be unable to make (or change) any selections. |
| * <p> |
| * Cross-browser compatibility is not perfect. In some cases, the |
| * {@link org.apache.tapestry.contrib.form.MultiplePropertySelection}component may be a better |
| * choice. |
| * <p> |
| * <table border=1> |
| * <tr> |
| * <td>Parameter</td> |
| * <td>Type</td> |
| * <td>Direction</td> |
| * <td>Required</td> |
| * <td>Default</td> |
| * <td>Description</td> |
| * </tr> |
| * <tr> |
| * <td>selected</td> |
| * <td>{@link List}</td> |
| * <td>in</td> |
| * <td>yes</td> |
| * <td> </td> |
| * <td>A List of selected values. Possible selections are defined by the model; this should be a |
| * subset of the possible values. This may be null when the component is renderred. When the |
| * containing form is submitted, this parameter is updated with a new List of selected objects. |
| * <p> |
| * The order may be set by the user, as well, depending on the sortMode parameter.</td> |
| * </tr> |
| * <tr> |
| * <td>model</td> |
| * <td>{@link IPropertySelectionModel}</td> |
| * <td>in</td> |
| * <td>yes</td> |
| * <td> </td> |
| * <td>Works, as with a {@link org.apache.tapestry.form.PropertySelection}component, to define the |
| * possible values.</td> |
| * </tr> |
| * <tr> |
| * <td>sort</td> |
| * <td>string</td> |
| * <td>in</td> |
| * <td>no</td> |
| * <td>{@link SortMode#NONE}</td> |
| * <td>Controls automatic sorting of the options.</td> |
| * </tr> |
| * <tr> |
| * <td>rows</td> |
| * <td>int</td> |
| * <td>in</td> |
| * <td>no</td> |
| * <td>10</td> |
| * <td>The number of rows that should be visible in the Pallete's <select> elements.</td> |
| * </tr> |
| * <tr> |
| * <td>tableClass</td> |
| * <td>{@link String}</td> |
| * <td>in</td> |
| * <td>no</td> |
| * <td>tapestry-palette</td> |
| * <td>The CSS class for the table which surrounds the other elements of the Palette.</td> |
| * </tr> |
| * <tr> |
| * <td>selectedTitleBlock</td> |
| * <td>{@link Block}</td> |
| * <td>in</td> |
| * <td>no</td> |
| * <td>"Selected"</td> |
| * <td>If specified, allows a {@link Block}to be placed within the <th> reserved for the |
| * title above the selected items <select> (on the right). This allows for images or other |
| * components to be placed there. By default, the simple word <code>Selected</code> is used.</td> |
| * </tr> |
| * <tr> |
| * <td>availableTitleBlock</td> |
| * <td>{@link Block}</td> |
| * <td>in</td> |
| * <td>no</td> |
| * <td>"Available"</td> |
| * <td>As with selectedTitleBlock, but for the left column, of items which are available to be |
| * selected. The default is the word <code>Available</code>.</td> |
| * </tr> |
| * <tr> |
| * <td>selectImage <br> |
| * selectDisabledImage <br> |
| * deselectImage <br> |
| * deselectDisabledImage <br> |
| * upImage <br> |
| * upDisabledImage <br> |
| * downImage <br> |
| * downDisabledImage</td> |
| * <td>{@link IAsset}</td> |
| * <td>in</td> |
| * <td>no</td> |
| * <td> </td> |
| * <td>If any of these are specified then they override the default images provided with the |
| * component. This allows the look and feel to be customized relatively easily. |
| * <p> |
| * The most common reason to replace the images is to deal with backgrounds. The default images are |
| * anti-aliased against a white background. If a colored or patterned background is used, the |
| * default images will have an ugly white fringe. Until all browsers have full support for PNG |
| * (which has a true alpha channel), it is necessary to customize the images to match the |
| * background.</td> |
| * </tr> |
| * </table> |
| * <p> |
| * A Palette requires some CSS entries to render correctly ... especially the middle column, which |
| * contains the two or four buttons for moving selections between the two columns. The width and |
| * alignment of this column must be set using CSS. Additionally, CSS is commonly used to give the |
| * Palette columns a fixed width, and to dress up the titles. Here is an example of some CSS you can |
| * use to format the palette component: |
| * |
| * <pre> |
| * TABLE.tapestry-palette TH |
| * { |
| * font-size: 9pt; |
| * font-weight: bold; |
| * color: white; |
| * background-color: #330066; |
| * text-align: center; |
| * } |
| * |
| * TD.available-cell SELECT |
| * { |
| * font-weight: normal; |
| * background-color: #FFFFFF; |
| * width: 200px; |
| * } |
| * |
| * TD.selected-cell SELECT |
| * { |
| * font-weight: normal; |
| * background-color: #FFFFFF; |
| * width: 200px; |
| * } |
| * |
| * TABLE.tapestry-palette TD.controls |
| * { |
| * text-align: center; |
| * vertical-align: middle; |
| * width: 60px; |
| * } |
| * </pre> |
| * |
| * <p> |
| * As of 4.0, this component can be validated. |
| * </p> |
| * |
| * @author Howard Lewis Ship |
| */ |
| |
| public abstract class Palette extends BaseComponent implements ValidatableFieldExtension |
| { |
| private static final int MAP_SIZE = 7; |
| |
| /** |
| * A set of symbols produced by the Palette script. This is used to provide proper names for |
| * some of the HTML elements (<select> and <button> elements, etc.). |
| */ |
| private Map _symbols; |
| |
| /** @since 3.0 * */ |
| public abstract void setAvailableColumn(PaletteColumn column); |
| |
| /** @since 3.0 * */ |
| public abstract void setSelectedColumn(PaletteColumn column); |
| |
| public abstract void setName(String name); |
| |
| public abstract void setForm(IForm form); |
| |
| /** @since 4.0 */ |
| public abstract void setRequiredMessage(String message); |
| |
| protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle) |
| { |
| // Next few lines of code is similar to AbstractFormComponent (which, alas, extends from |
| // AbstractComponent, not from BaseComponent). |
| IForm form = TapestryUtils.getForm(cycle, this); |
| |
| setForm(form); |
| |
| if (form.wasPrerendered(writer, this)) |
| return; |
| |
| IValidationDelegate delegate = form.getDelegate(); |
| |
| delegate.setFormComponent(this); |
| |
| form.getElementId(this); |
| |
| if (form.isRewinding()) |
| { |
| if (!isDisabled()) |
| { |
| rewindFormComponent(writer, cycle); |
| } |
| } |
| else if (!cycle.isRewinding()) |
| { |
| if (!isDisabled()) |
| delegate.registerForFocus(this, ValidationConstants.NORMAL_FIELD); |
| |
| renderFormComponent(writer, cycle); |
| |
| if (delegate.isInError()) |
| delegate.registerForFocus(this, ValidationConstants.ERROR_FIELD); |
| } |
| |
| super.renderComponent(writer, cycle); |
| } |
| |
| protected void renderFormComponent(IMarkupWriter writer, IRequestCycle cycle) |
| { |
| _symbols = new HashMap(MAP_SIZE); |
| |
| runScript(cycle); |
| |
| constructColumns(); |
| |
| getValidatableFieldSupport().renderContributions(this, writer, cycle); |
| } |
| |
| protected void rewindFormComponent(IMarkupWriter writer, IRequestCycle cycle) |
| { |
| String[] values = cycle.getParameters(getName()); |
| |
| int count = Tapestry.size(values); |
| |
| List selected = new ArrayList(count); |
| IPropertySelectionModel model = getModel(); |
| |
| for (int i = 0; i < count; i++) |
| { |
| String value = values[i]; |
| Object option = model.translateValue(value); |
| |
| selected.add(option); |
| } |
| |
| setSelected(selected); |
| |
| try |
| { |
| getValidatableFieldSupport().validate(this, writer, cycle, selected); |
| } |
| catch (ValidatorException e) |
| { |
| getForm().getDelegate().record(e); |
| } |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| public void overrideContributions(Validator validator, FormComponentContributorContext context, |
| IMarkupWriter writer, IRequestCycle cycle) |
| { |
| // we know this has to be a Required validator |
| Required required = (Required)validator; |
| |
| JSONObject profile = context.getProfile(); |
| |
| if (!profile.has(ValidationConstants.CONSTRAINTS)) { |
| profile.put(ValidationConstants.CONSTRAINTS, new JSONObject()); |
| } |
| JSONObject cons = profile.getJSONObject(ValidationConstants.CONSTRAINTS); |
| |
| required.accumulateProperty(cons, getClientId(), |
| new JSONLiteral("[tapestry.form.validation.isPalleteSelected]")); |
| |
| required.accumulateProfileProperty(this, profile, |
| ValidationConstants.CONSTRAINTS, required.buildMessage(context, this)); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| public boolean overrideValidator(Validator validator, IRequestCycle cycle) |
| { |
| if (Required.class.isAssignableFrom(validator.getClass())) |
| return true; |
| |
| return false; |
| } |
| |
| protected void cleanupAfterRender(IRequestCycle cycle) |
| { |
| _symbols = null; |
| |
| setAvailableColumn(null); |
| setSelectedColumn(null); |
| |
| super.cleanupAfterRender(cycle); |
| } |
| |
| /** |
| * Executes the associated script, which generates all the JavaScript to support this Palette. |
| */ |
| private void runScript(IRequestCycle cycle) |
| { |
| PageRenderSupport pageRenderSupport = TapestryUtils.getPageRenderSupport(cycle, this); |
| |
| setImage(pageRenderSupport, cycle, "selectImage", getSelectImage()); |
| setImage(pageRenderSupport, cycle, "selectDisabledImage", getSelectDisabledImage()); |
| setImage(pageRenderSupport, cycle, "deselectImage", getDeselectImage()); |
| setImage(pageRenderSupport, cycle, "deselectDisabledImage", getDeselectDisabledImage()); |
| |
| if (isSortUser()) |
| { |
| setImage(pageRenderSupport, cycle, "upImage", getUpImage()); |
| setImage(pageRenderSupport, cycle, "upDisabledImage", getUpDisabledImage()); |
| setImage(pageRenderSupport, cycle, "downImage", getDownImage()); |
| setImage(pageRenderSupport, cycle, "downDisabledImage", getDownDisabledImage()); |
| } |
| |
| _symbols.put("palette", this); |
| |
| getScript().execute(this, cycle, pageRenderSupport, _symbols); |
| } |
| |
| /** |
| * Extracts its asset URL, sets it up for preloading, and assigns the preload reference as a |
| * script symbol. |
| */ |
| private void setImage(PageRenderSupport pageRenderSupport, IRequestCycle cycle, |
| String symbolName, IAsset asset) |
| { |
| String url = asset.buildURL(); |
| String reference = pageRenderSupport.getPreloadedImageReference(this, url); |
| |
| _symbols.put(symbolName, reference); |
| } |
| |
| public Map getSymbols() |
| { |
| return _symbols; |
| } |
| |
| /** |
| * Constructs a pair of {@link PaletteColumn}s: the available and selected options. |
| */ |
| private void constructColumns() |
| { |
| // Build a Set around the list of selected items. |
| |
| List selected = getSelected(); |
| |
| if (selected == null) |
| selected = Collections.EMPTY_LIST; |
| |
| String sortMode = getSort(); |
| |
| boolean sortUser = sortMode.equals(SortMode.USER); |
| |
| List selectedOptions = null; |
| |
| if (sortUser) |
| { |
| int count = selected.size(); |
| selectedOptions = new ArrayList(count); |
| |
| for (int i = 0; i < count; i++) |
| selectedOptions.add(null); |
| } |
| |
| PaletteColumn availableColumn = new PaletteColumn((String) _symbols.get("availableName"), |
| (String)_symbols.get("availableName"), getRows()); |
| PaletteColumn selectedColumn = new PaletteColumn(getName(), getClientId(), getRows()); |
| |
| // Each value specified in the model will go into either the selected or available |
| // lists. |
| |
| IPropertySelectionModel model = getModel(); |
| |
| int count = model.getOptionCount(); |
| |
| for (int i = 0; i < count; i++) |
| { |
| Object optionValue = model.getOption(i); |
| |
| PaletteOption o = new PaletteOption(model.getValue(i), model.getLabel(i)); |
| |
| int index = selected.indexOf(optionValue); |
| boolean isSelected = index >= 0; |
| |
| if (sortUser && isSelected) |
| { |
| selectedOptions.set(index, o); |
| continue; |
| } |
| |
| PaletteColumn c = isSelected ? selectedColumn : availableColumn; |
| |
| c.addOption(o); |
| } |
| |
| if (sortUser) |
| { |
| Iterator i = selectedOptions.iterator(); |
| while (i.hasNext()) |
| { |
| PaletteOption o = (PaletteOption) i.next(); |
| selectedColumn.addOption(o); |
| } |
| } |
| |
| if (sortMode.equals(SortMode.VALUE)) |
| { |
| availableColumn.sortByValue(); |
| selectedColumn.sortByValue(); |
| } |
| else if (sortMode.equals(SortMode.LABEL)) |
| { |
| availableColumn.sortByLabel(); |
| selectedColumn.sortByLabel(); |
| } |
| |
| setAvailableColumn(availableColumn); |
| setSelectedColumn(selectedColumn); |
| } |
| |
| public boolean isSortUser() |
| { |
| return getSort().equals(SortMode.USER); |
| } |
| |
| public abstract Block getAvailableTitleBlock(); |
| |
| public abstract IAsset getDeselectDisabledImage(); |
| |
| public abstract IAsset getDeselectImage(); |
| |
| public abstract IAsset getDownDisabledImage(); |
| |
| public abstract IAsset getDownImage(); |
| |
| public abstract IAsset getSelectDisabledImage(); |
| |
| public abstract IPropertySelectionModel getModel(); |
| |
| public abstract int getRows(); |
| |
| public abstract Block getSelectedTitleBlock(); |
| |
| public abstract IAsset getSelectImage(); |
| |
| public abstract String getSort(); |
| |
| public abstract IAsset getUpDisabledImage(); |
| |
| public abstract IAsset getUpImage(); |
| |
| /** |
| * Returns false. Palette components are never disabled. |
| * |
| * @since 2.2 |
| */ |
| public boolean isDisabled() |
| { |
| return false; |
| } |
| |
| /** @since 2.2 * */ |
| |
| public abstract List getSelected(); |
| |
| /** @since 2.2 * */ |
| |
| public abstract void setSelected(List selected); |
| |
| /** |
| * Injected. |
| * |
| * @since 4.0 |
| */ |
| public abstract IScript getScript(); |
| |
| /** |
| * Injected. |
| * |
| * @since 4.0 |
| */ |
| public abstract ValidatableFieldSupport getValidatableFieldSupport(); |
| |
| /** |
| * @see org.apache.tapestry.form.AbstractFormComponent#isRequired() |
| */ |
| public boolean isRequired() |
| { |
| return getValidatableFieldSupport().isRequired(this); |
| } |
| } |