/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.wicket.extensions.markup.html.form.palette;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;
import org.apache.wicket.Component;
import org.apache.wicket.ResourceReference;
import org.apache.wicket.behavior.HeaderContributor;
import org.apache.wicket.extensions.markup.html.form.palette.component.Choices;
import org.apache.wicket.extensions.markup.html.form.palette.component.Recorder;
import org.apache.wicket.extensions.markup.html.form.palette.component.Selection;
import org.apache.wicket.markup.ComponentTag;
import org.apache.wicket.markup.html.WebMarkupContainer;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.form.FormComponent;
import org.apache.wicket.markup.html.form.IChoiceRenderer;
import org.apache.wicket.markup.html.image.Image;
import org.apache.wicket.markup.html.panel.Panel;
import org.apache.wicket.markup.html.resources.StyleSheetReference;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.Model;
/**
* Palette is a component that allows the user to easily select and order
* multiple items by moving them from one select box into another.
*
* <strong>Ajaxifying the palette</strong>: The palette itself cannot be
* ajaxified because it is a panel and therefore does not receive any javascript
* events. Instead ajax behaviors can be attached to the recorder component
* which supports the javascript <code>onchange</code> event. The recorder
* component can be retrieved via a call to {@link #getRecorderComponent()}.
*
* Example:
*
* <pre>
* Form form=new Form(...);
* Palette palette=new Palette(...);
* palette.getRecorderComponent().add(new AjaxFormComponentUpdatingBehavior("onchange") {...});
* </pre>
*
* @author Igor Vaynberg ( ivaynberg )
*
*/
public class Palette extends Panel
{
private static final String SELECTED_HEADER_ID = "selectedHeader";
private static final String AVAILABLE_HEADER_ID = "availableHeader";
private static final long serialVersionUID = 1L;
/** collection containing all available choices */
private IModel choicesModel;
/**
* choice render used to render the choices in both available and selected
* collections
*/
private IChoiceRenderer choiceRenderer;
/** number of rows to show in the select boxes */
private int rows;
/**
* recorder component used to track user's selection. it is updated by
* javascript on changes.
*/
private Recorder recorderComponent;
/**
* component used to represent all available choices. by default this is a
* select box with multiple attribute
*/
private Component choicesComponent;
/**
* component used to represent selected items. by default this is a select
* box with multiple attribute
*/
private Component selectionComponent;
/** reference to the palette's javascript resource */
private static final ResourceReference JAVASCRIPT = new ResourceReference(Palette.class,
"palette.js");
/** reference to the palette's css resource */
private static final ResourceReference CSS = new ResourceReference(Palette.class, "palette.css");
/** reference to default up buttom image */
private static final ResourceReference UP_IMAGE = new ResourceReference(Palette.class, "up.gif");
/** reference to default down button image */
private static final ResourceReference DOWN_IMAGE = new ResourceReference(Palette.class,
"down.gif");
/** reference to default remove button image */
private static final ResourceReference REMOVE_IMAGE = new ResourceReference(Palette.class,
"remove.gif");
/** reference to default add buttom image */
private static final ResourceReference ADD_IMAGE = new ResourceReference(Palette.class,
"add.gif");
/**
* @param id
* component id
* @param choicesModel
* model representing collection of all available choices
* @param choiceRenderer
* render used to render choices
* @param rows
* number of choices to be visible on the screen with out
* scrolling
* @param allowOrder
* allow user to move selections up and down
*/
public Palette(String id, IModel choicesModel, IChoiceRenderer choiceRenderer, int rows,
boolean allowOrder)
{
this(id, null, choicesModel, choiceRenderer, rows, allowOrder);
}
/**
* @param id
* component id
* @param model
* model representing collection of user's selections
* @param choicesModel
* model representing collection of all available choices
* @param choiceRenderer
* render used to render choices
* @param rows
* number of choices to be visible on the screen with out
* scrolling
* @param allowOrder
* allow user to move selections up and down
*/
public Palette(String id, IModel model, IModel choicesModel, IChoiceRenderer choiceRenderer,
int rows, boolean allowOrder)
{
super(id, model);
this.choicesModel = choicesModel;
this.choiceRenderer = choiceRenderer;
this.rows = rows;
recorderComponent = newRecorderComponent();
add(recorderComponent);
choicesComponent = newChoicesComponent();
add(choicesComponent);
selectionComponent = newSelectionComponent();
add(selectionComponent);
add(newAddComponent());
add(newRemoveComponent());
add(newUpComponent().setVisible(allowOrder));
add(newDownComponent().setVisible(allowOrder));
add(newAvailableHeader(AVAILABLE_HEADER_ID));
add(newSelectedHeader(SELECTED_HEADER_ID));
add(HeaderContributor.forJavaScript(JAVASCRIPT));
ResourceReference css = getCSS();
if (css != null)
{
add(HeaderContributor.forCss(css));
}
}
/**
* Returns the resource reference of the default stylesheet. You may return
* null to avoid using any stylesheet.
*
* @return A resource reference
*/
protected ResourceReference getCSS()
{
return CSS;
}
/**
* Can be overriden by clients for custom style sheet
*
* @return the style sheet reference
*/
protected StyleSheetReference getStyleSheet()
{
return new StyleSheetReference("paletteCSS", getClass(), "palette.css");
}
/**
* Return true if the palette is enabled, false otherwise
*
* @return true if the palette is enabled, false otherwise
*/
public final boolean isPaletteEnabled()
{
return isEnabled() && isEnableAllowed();
}
/**
* @return iterator over selected choices
*/
public Iterator getSelectedChoices()
{
return getRecorderComponent().getSelectedChoices();
}
/**
* @return iterator over unselected choices
*/
public Iterator getUnselectedChoices()
{
return getRecorderComponent().getUnselectedChoices();
}
/**
* factory method to create the tracker component
*
* @return tracker component
*/
private Recorder newRecorderComponent()
{
// create component that will keep track of selections
return new Recorder("recorder", this)
{
private static final long serialVersionUID = 1L;
public void updateModel()
{
super.updateModel();
Palette.this.updateModel();
}
};
}
/**
* factory method for the available items header
*
* @param componentId
* component id of the returned header component
*
* @return available items component
*/
protected Component newAvailableHeader(String componentId)
{
return new Label(componentId, getString("palette.available", null, "Available"));
}
/**
* factory method for the selected items header
*
* @param componentId
* component id of the returned header component
*
* @return header component
*/
protected Component newSelectedHeader(String componentId)
{
return new Label(componentId, getString("palette.selected", null, "Selected"));
}
/**
* factory method for the move down component
*
* @return move down component
*/
protected Component newDownComponent()
{
return new PaletteButton("moveDownButton")
{
private static final long serialVersionUID = 1L;
protected void onComponentTag(ComponentTag tag)
{
super.onComponentTag(tag);
tag.getAttributes().put("onclick", Palette.this.getDownOnClickJS());
}
}.add(new Image("image", DOWN_IMAGE));
}
/**
* factory method for the move up component
*
* @return move up component
*/
protected Component newUpComponent()
{
return new PaletteButton("moveUpButton")
{
private static final long serialVersionUID = 1L;
protected void onComponentTag(ComponentTag tag)
{
super.onComponentTag(tag);
tag.getAttributes().put("onclick", Palette.this.getUpOnClickJS());
}
}.add(new Image("image", UP_IMAGE));
}
/**
* factory method for the remove component
*
* @return remove component
*/
protected Component newRemoveComponent()
{
return new PaletteButton("removeButton")
{
private static final long serialVersionUID = 1L;
protected void onComponentTag(ComponentTag tag)
{
super.onComponentTag(tag);
tag.getAttributes().put("onclick", Palette.this.getRemoveOnClickJS());
}
}.add(new Image("image", REMOVE_IMAGE));
}
/**
* factory method for the addcomponent
*
* @return add component
*/
protected Component newAddComponent()
{
return new PaletteButton("addButton")
{
private static final long serialVersionUID = 1L;
protected void onComponentTag(ComponentTag tag)
{
super.onComponentTag(tag);
tag.getAttributes().put("onclick", Palette.this.getAddOnClickJS());
}
}.add(new Image("image", ADD_IMAGE));
}
/**
* factory method for the selected items component
*
* @return selected items component
*/
protected Component newSelectionComponent()
{
return new Selection("selection", this)
{
private static final long serialVersionUID = 1L;
protected Map getAdditionalAttributes(Object choice)
{
return Palette.this.getAdditionalAttributesForSelection(choice);
}
};
}
/**
* @see wicket.extensions.markup.html.form.palette.component#getAdditionalAttributes()
*/
protected Map getAdditionalAttributesForSelection(Object choice)
{
return null;
}
/**
* factory method for the available items component
*
* @return available items component
*/
protected Component newChoicesComponent()
{
return new Choices("choices", this)
{
private static final long serialVersionUID = 1L;
protected Map getAdditionalAttributes(Object choice)
{
return Palette.this.getAdditionalAttributesForChoices(choice);
}
};
}
/**
* @see wicket.extensions.markup.html.form.palette.component#getAdditionalAttributes()
*/
protected Map getAdditionalAttributesForChoices(Object choice)
{
return null;
}
private Component getChoicesComponent()
{
return choicesComponent;
}
private Component getSelectionComponent()
{
return selectionComponent;
}
/**
* Returns recorder component. Recorder component is a form component used
* to track the selection of the palette. It receives <code>onchange</code>
* javascript event whenever a change in selection occurs.
*
* @return recorder component
*/
public final Recorder getRecorderComponent()
{
return recorderComponent;
}
/**
* @return collection representing all available items
*/
public Collection getChoices()
{
return (Collection)choicesModel.getObject();
}
/**
* @return collection representing selected items
*/
public Collection getModelCollection()
{
return (Collection)getModelObject();
}
/**
* @return choice renderer
*/
public IChoiceRenderer getChoiceRenderer()
{
return choiceRenderer;
}
/**
* @return items visible without scrolling
*/
public int getRows()
{
return rows;
}
/**
* The model object is assumed to be a Collection, and it is modified
* in-place. Then {@link Model#setObject(Object)} is called with the same
* instance: it allows the Model to be notified of changes even when
* {@link Model#getObject()} returns a different {@link Collection} at every
* invocation.
*
* @see FormComponent#updateModel()
*/
protected final void updateModel()
{
// prepare model
Collection model = (Collection)getModelObject();
model.clear();
// update model
Iterator it = getRecorderComponent().getSelectedChoices();
while (it.hasNext())
{
final Object selectedChoice = it.next();
model.add(selectedChoice);
}
getModel().setObject(model);
}
/**
* builds javascript handler call
*
* @param funcName
* name of javascript function to call
* @return string representing the call tho the function with palette params
*/
protected String buildJSCall(String funcName)
{
return new StringBuffer(funcName).append("('").append(getChoicesComponent().getMarkupId())
.append("','").append(getSelectionComponent().getMarkupId()).append("','").append(
getRecorderComponent().getMarkupId()).append("');").toString();
}
/**
* @return choices component on focus javascript handler
*/
public String getChoicesOnFocusJS()
{
return buildJSCall("paletteChoicesOnFocus");
}
/**
* @return selection component on focus javascript handler
*/
public String getSelectionOnFocusJS()
{
return buildJSCall("paletteSelectionOnFocus");
}
/**
* @return add action javascript handler
*/
public String getAddOnClickJS()
{
return buildJSCall("paletteAdd");
}
/**
* @return remove action javascript handler
*/
public String getRemoveOnClickJS()
{
return buildJSCall("paletteRemove");
}
/**
* @return move up action javascript handler
*/
public String getUpOnClickJS()
{
return buildJSCall("paletteMoveUp");
}
/**
* @return move down action javascript handler
*/
public String getDownOnClickJS()
{
return buildJSCall("paletteMoveDown");
}
protected void onDetach()
{
// we need to manually detach the choices model since it is not attached
// to a component
// an alternative might be to attach it to one of the subcomponents
choicesModel.detach();
super.onDetach();
}
private class PaletteButton extends WebMarkupContainer
{
private static final long serialVersionUID = 1L;
/**
* Constructor
*
* @param id
*/
public PaletteButton(String id)
{
super(id);
}
protected void onComponentTag(ComponentTag tag)
{
if (!isPaletteEnabled())
{
tag.getAttributes().put("disabled", "disabled");
}
}
}
}