org.apache.wicket.extensions.markup.html.form.palette.Palette.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.wicket.extensions.markup.html.form.palette.Palette.java

Source

/*
 * 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));
    }
}