/*
* 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.List;
import java.util.Map;
import org.apache.wicket.Component;
import org.apache.wicket.ajax.form.AjaxFormComponentUpdatingBehavior;
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.extensions.markup.html.form.palette.theme.DefaultTheme;
import org.apache.wicket.markup.ComponentTag;
import org.apache.wicket.markup.head.IHeaderResponse;
import org.apache.wicket.markup.head.JavaScriptHeaderItem;
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.FormComponentPanel;
import org.apache.wicket.markup.html.form.IChoiceRenderer;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.model.ResourceModel;
import org.apache.wicket.request.resource.ResourceReference;
import org.apache.wicket.resource.JQueryPluginResourceReference;
/**
* Palette is a component that allows the user to easily select and order multiple items by moving
* them from one select box into another.
* <p>
* When creating a Palette object make sure your IChoiceRenderer returns a specific ID, not the
* index.
* <p>
* <strong>Ajaxifying the palette</strong>: If you want to update a Palette with an
* {@link AjaxFormComponentUpdatingBehavior}, you have to attach it to the contained
* {@link Recorder} by overriding {@link #newRecorderComponent()} and calling
* {@link #processInput()}:
*
* <pre>{@code
* Palette palette=new Palette(...) {
* protected Recorder newRecorderComponent()
* {
* Recorder recorder=super.newRecorderComponent();
* recorder.add(new AjaxFormComponentUpdatingBehavior("change") {
* protected void onUpdate(AjaxRequestTarget target) {
* processInput(); // let Palette process input too
*
* ...
* }
* });
* return recorder;
* }
* }
* }</pre>
*
* You can add a {@link DefaultTheme} to style this component in a left to right fashion.
*
* @author Igor Vaynberg ( ivaynberg )
* @param <T>
* Type of model object
*
*/
public class Palette<T> extends FormComponentPanel<Collection<T>>
{
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 final IModel<? extends Collection<? extends T>> choicesModel;
/**
* choice render used to render the choices in both available and selected collections
*/
private final IChoiceRenderer<? super T> choiceRenderer;
/** number of rows to show in the select boxes */
private final int rows;
/** if reordering of selected items is allowed in */
private final boolean allowOrder;
/** if add all and remove all are allowed */
private final boolean allowMoveAll;
/**
* recorder component used to track user's selection. it is updated by javascript on changes.
*/
private Recorder<T> 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 JQueryPluginResourceReference(
Palette.class, "palette.js");
/**
* @param id
* Component id
* @param choicesModel
* Model representing collection of all available choices
* @param choiceRenderer
* Render used to render choices. This must use unique IDs for the objects, not the
* index.
* @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(final String id, final IModel<? extends Collection<T>> choicesModel,
final IChoiceRenderer<? super T> choiceRenderer, final int rows, final 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. This must use unique IDs for the objects, not the
* index.
* @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(final String id, final IModel<? extends Collection<T>> model,
final IModel<? extends Collection<? extends T>> choicesModel,
final IChoiceRenderer<? super T> choiceRenderer, final int rows, final boolean allowOrder)
{
this(id, model, choicesModel, choiceRenderer, rows, allowOrder, false);
}
/**
* Constructor.
*
* @param id
* Component id
* @param choicesModel
* Model representing collection of all available choices
* @param choiceRenderer
* Render used to render choices. This must use unique IDs for the objects, not the
* index.
* @param rows
* Number of choices to be visible on the screen with out scrolling
* @param allowOrder
* Allow user to move selections up and down
* @param allowMoveAll
* Allow user to add or remove all items at once
*/
public Palette(final String id, final IModel<? extends Collection<T>> model,
final IModel<? extends Collection<? extends T>> choicesModel,
final IChoiceRenderer<? super T> choiceRenderer, final int rows, final boolean allowOrder,
boolean allowMoveAll)
{
super(id, (IModel<Collection<T>>)model);
this.choicesModel = choicesModel;
this.choiceRenderer = choiceRenderer;
this.rows = rows;
this.allowOrder = allowOrder;
this.allowMoveAll = allowMoveAll;
}
@Override
protected void onBeforeRender()
{
if (get("recorder") == null)
{
initFactories();
}
super.onBeforeRender();
}
/**
* One-time init method for components that are created via overridable factories. This method
* is here because we do not want to call overridable methods form palette's constructor.
*/
private void initFactories()
{
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(newAddAllComponent().setVisible(allowMoveAll));
add(newRemoveAllComponent().setVisible(allowMoveAll));
add(newAvailableHeader(AVAILABLE_HEADER_ID));
add(newSelectedHeader(SELECTED_HEADER_ID));
}
/**
* Return true if the palette is enabled, false otherwise
*
* @return true if the palette is enabled, false otherwise
*/
public final boolean isPaletteEnabled()
{
return isEnabledInHierarchy();
}
/**
* @return iterator over selected choices
*/
public Iterator<T> getSelectedChoices()
{
return getRecorderComponent().getSelectedList().iterator();
}
/**
* @return iterator over unselected choices
*/
public Iterator<T> getUnselectedChoices()
{
return getRecorderComponent().getUnselectedList().iterator();
}
/**
* factory method to create the tracker component
*
* @return tracker component
*/
protected Recorder<T> newRecorderComponent()
{
// create component that will keep track of selections
return new Recorder<>("recorder", this);
}
/**
* factory method for the available items header
*
* @param componentId
* component id of the returned header component
*
* @return available items component
*/
protected Component newAvailableHeader(final String componentId)
{
return new Label(componentId, new ResourceModel("palette.available", "Available"));
}
/**
* factory method for the selected items header
*
* @param componentId
* component id of the returned header component
*
* @return header component
*/
protected Component newSelectedHeader(final String componentId)
{
return new Label(componentId, new ResourceModel("palette.selected", "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;
@Override
protected void onComponentTag(final ComponentTag tag)
{
super.onComponentTag(tag);
tag.getAttributes().put("onclick", Palette.this.getDownOnClickJS());
}
};
}
/**
* factory method for the move up component
*
* @return move up component
*/
protected Component newUpComponent()
{
return new PaletteButton("moveUpButton")
{
private static final long serialVersionUID = 1L;
@Override
protected void onComponentTag(final ComponentTag tag)
{
super.onComponentTag(tag);
tag.getAttributes().put("onclick", Palette.this.getUpOnClickJS());
}
};
}
/**
* factory method for the remove component
*
* @return remove component
*/
protected Component newRemoveComponent()
{
return new PaletteButton("removeButton")
{
private static final long serialVersionUID = 1L;
@Override
protected void onComponentTag(final ComponentTag tag)
{
super.onComponentTag(tag);
tag.getAttributes().put("onclick", Palette.this.getRemoveOnClickJS());
}
};
}
/**
* factory method for the addcomponent
*
* @return add component
*/
protected Component newAddComponent()
{
return new PaletteButton("addButton")
{
private static final long serialVersionUID = 1L;
@Override
protected void onComponentTag(final ComponentTag tag)
{
super.onComponentTag(tag);
tag.getAttributes().put("onclick", Palette.this.getAddOnClickJS());
}
};
}
/**
* factory method for the selected items component
*
* @return selected items component
*/
protected Component newSelectionComponent()
{
return new Selection<T>("selection", this)
{
private static final long serialVersionUID = 1L;
@Override
protected Map<String, String> getAdditionalAttributes(final Object choice)
{
return Palette.this.getAdditionalAttributesForSelection(choice);
}
@Override
protected boolean localizeDisplayValues()
{
return Palette.this.localizeDisplayValues();
}
};
}
/**
* factory method for the addAll component
*
* @return addAll component
*/
protected Component newAddAllComponent()
{
return new PaletteButton("addAllButton")
{
private static final long serialVersionUID = 1L;
protected void onComponentTag(ComponentTag tag)
{
super.onComponentTag(tag);
tag.getAttributes().put("onclick", Palette.this.getAddAllOnClickJS());
}
};
}
/**
* factory method for the removeAll component
*
* @return removeAll component
*/
protected Component newRemoveAllComponent()
{
return new PaletteButton("removeAllButton")
{
private static final long serialVersionUID = 1L;
protected void onComponentTag(ComponentTag tag)
{
super.onComponentTag(tag);
tag.getAttributes().put("onclick", Palette.this.getRemoveAllOnClickJS());
}
};
}
/**
* @param choice
* @return null
* @see org.apache.wicket.extensions.markup.html.form.palette.component.Selection#getAdditionalAttributes(Object)
*/
protected Map<String, String> getAdditionalAttributesForSelection(final Object choice)
{
return null;
}
/**
* factory method for the available items component
*
* @return available items component
*/
protected Component newChoicesComponent()
{
return new Choices<T>("choices", this)
{
private static final long serialVersionUID = 1L;
@Override
protected Map<String, String> getAdditionalAttributes(final Object choice)
{
return Palette.this.getAdditionalAttributesForChoices(choice);
}
@Override
protected boolean localizeDisplayValues()
{
return Palette.this.localizeDisplayValues();
}
};
}
/**
* Override this method if you do <strong>not</strong> want to localize the display values of
* the generated options. By default true is returned.
*
* @return true If you want to localize the display values, default == true
*/
protected boolean localizeDisplayValues()
{
return true;
}
/**
* @param choice
* @return null
* @see org.apache.wicket.extensions.markup.html.form.palette.component.Selection#getAdditionalAttributes(Object)
*/
protected Map<String, String> getAdditionalAttributesForChoices(final Object choice)
{
return null;
}
protected Component getChoicesComponent()
{
return choicesComponent;
}
protected 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<T> getRecorderComponent()
{
return recorderComponent;
}
/**
* @return collection representing all available items
*/
public Collection<? extends T> getChoices()
{
return choicesModel.getObject();
}
/**
* @return collection representing selected items
*/
@SuppressWarnings("unchecked")
public Collection<T> getModelCollection()
{
return (Collection<T>)getDefaultModelObject();
}
/**
* @return choice renderer
*/
public IChoiceRenderer<? super T> getChoiceRenderer()
{
return choiceRenderer;
}
/**
* @return items visible without scrolling
*/
public int getRows()
{
return rows;
}
@Override
public void convertInput()
{
List<T> selectedList = getRecorderComponent().getSelectedList();
if (selectedList.isEmpty())
{
setConvertedInput(null);
}
else
{
setConvertedInput(selectedList);
}
}
/**
* 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()
*/
@Override
public final void updateModel()
{
FormComponent.updateCollectionModel(this);
}
/**
* 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(final String funcName)
{
return new StringBuilder(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("Wicket.Palette.choicesOnFocus");
}
/**
* @return selection component on focus javascript handler
*/
public String getSelectionOnFocusJS()
{
return buildJSCall("Wicket.Palette.selectionOnFocus");
}
/**
* @return add action javascript handler
*/
public String getAddOnClickJS()
{
return buildJSCall("Wicket.Palette.add");
}
/**
* @return remove action javascript handler
*/
public String getRemoveOnClickJS()
{
return buildJSCall("Wicket.Palette.remove");
}
/**
* @return move up action javascript handler
*/
public String getUpOnClickJS()
{
return buildJSCall("Wicket.Palette.moveUp");
}
/**
* @return move down action javascript handler
*/
public String getDownOnClickJS()
{
return buildJSCall("Wicket.Palette.moveDown");
}
/**
* @return addAll action javascript handler
*/
public String getAddAllOnClickJS()
{
return buildJSCall("Wicket.Palette.addAll");
}
/**
* @return removeAll action javascript handler
*/
public String getRemoveAllOnClickJS()
{
return buildJSCall("Wicket.Palette.removeAll");
}
@Override
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();
choiceRenderer.detach();
super.onDetach();
}
private class PaletteButton extends WebMarkupContainer
{
private static final long serialVersionUID = 1L;
/**
* Constructor
*
* @param id
*/
public PaletteButton(final String id)
{
super(id);
}
@Override
protected void onComponentTag(final ComponentTag tag)
{
super.onComponentTag(tag);
if (!isPaletteEnabled())
{
tag.getAttributes().put("disabled", "disabled");
}
}
}
/**
* Renders header contributions
*
* @param response
*/
@Override
public void renderHead(final IHeaderResponse response)
{
response.render(JavaScriptHeaderItem.forReference(JAVASCRIPT));
}
}