blob: b609e81e280fbc62ba2f31baf2d6106cb8fb321a [file] [log] [blame]
// 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>&nbsp;</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>&nbsp;</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 &lt;select&gt; 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 &lt;th&gt; reserved for the
* title above the selected items &lt;select&gt; (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>&nbsp;</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 (&lt;select&gt; and &lt;button&gt; 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);
}
}