001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.wicket.extensions.markup.html.form.palette;
018
019import java.util.Collection;
020import java.util.Iterator;
021import java.util.List;
022import java.util.Map;
023
024import org.apache.wicket.Component;
025import org.apache.wicket.ajax.form.AjaxFormComponentUpdatingBehavior;
026import org.apache.wicket.extensions.markup.html.form.palette.component.Choices;
027import org.apache.wicket.extensions.markup.html.form.palette.component.Recorder;
028import org.apache.wicket.extensions.markup.html.form.palette.component.Selection;
029import org.apache.wicket.extensions.markup.html.form.palette.theme.DefaultTheme;
030import org.apache.wicket.markup.ComponentTag;
031import org.apache.wicket.markup.head.IHeaderResponse;
032import org.apache.wicket.markup.head.JavaScriptHeaderItem;
033import org.apache.wicket.markup.head.OnEventHeaderItem;
034import org.apache.wicket.markup.html.WebMarkupContainer;
035import org.apache.wicket.markup.html.basic.Label;
036import org.apache.wicket.markup.html.form.FormComponent;
037import org.apache.wicket.markup.html.form.FormComponentPanel;
038import org.apache.wicket.markup.html.form.IChoiceRenderer;
039import org.apache.wicket.model.IModel;
040import org.apache.wicket.model.Model;
041import org.apache.wicket.model.ResourceModel;
042import org.apache.wicket.request.resource.ResourceReference;
043import org.apache.wicket.resource.JQueryPluginResourceReference;
044
045
046/**
047 * Palette is a component that allows the user to easily select and order multiple items by moving
048 * them from one select box into another.
049 * <p>
050 * When creating a Palette object make sure your IChoiceRenderer returns a specific ID, not the
051 * index.
052 * <p>
053 * <strong>Ajaxifying the palette</strong>: If you want to update a Palette with an
054 * {@link AjaxFormComponentUpdatingBehavior}, you have to attach it to the contained
055 * {@link Recorder} by overriding {@link #newRecorderComponent()} and calling
056 * {@link #processInput()}:
057 * 
058 * <pre>{@code
059 *  Palette palette=new Palette(...) {
060 *    protected Recorder newRecorderComponent()
061 *    {
062 *      Recorder recorder=super.newRecorderComponent();     
063 *      recorder.add(new AjaxFormComponentUpdatingBehavior("change") {
064 *        protected void onUpdate(AjaxRequestTarget target) {
065 *          processInput(); // let Palette process input too
066 *
067 *          ...
068 *        }
069 *      });
070 *      return recorder;
071 *    }
072 *  }
073 * }</pre>
074 * 
075 * You can add a {@link DefaultTheme} to style this component in a left to right fashion.
076 * 
077 * @author Igor Vaynberg ( ivaynberg )
078 * @param <T>
079 *            Type of model object
080 * 
081 */
082public class Palette<T> extends FormComponentPanel<Collection<T>>
083{
084        private static final String SELECTED_HEADER_ID = "selectedHeader";
085
086        private static final String AVAILABLE_HEADER_ID = "availableHeader";
087
088        private static final long serialVersionUID = 1L;
089
090        /** collection containing all available choices */
091        private final IModel<? extends Collection<? extends T>> choicesModel;
092
093        /**
094         * choice render used to render the choices in both available and selected collections
095         */
096        private final IChoiceRenderer<? super T> choiceRenderer;
097
098        /** number of rows to show in the select boxes */
099        private final int rows;
100
101        /** if reordering of selected items is allowed in */
102        private final boolean allowOrder;
103
104        /** if add all and remove all are allowed */
105        private final boolean allowMoveAll;
106
107        /**
108         * recorder component used to track user's selection. it is updated by javascript on changes.
109         */
110        private Recorder<T> recorderComponent;
111
112        /**
113         * component used to represent all available choices. by default this is a select box with
114         * multiple attribute
115         */
116        private Component choicesComponent;
117
118        /**
119         * component used to represent selected items. by default this is a select box with multiple
120         * attribute
121         */
122        private Component selectionComponent;
123
124        /** reference to the palette's javascript resource */
125        private static final ResourceReference JAVASCRIPT = new JQueryPluginResourceReference(
126                Palette.class, "palette.js");
127
128        /**
129         * @param id
130         *            Component id
131         * @param choicesModel
132         *            Model representing collection of all available choices
133         * @param choiceRenderer
134         *            Render used to render choices. This must use unique IDs for the objects, not the
135         *            index.
136         * @param rows
137         *            Number of choices to be visible on the screen with out scrolling
138         * @param allowOrder
139         *            Allow user to move selections up and down
140         */
141        public Palette(final String id, final IModel<? extends Collection<T>> choicesModel,
142                final IChoiceRenderer<? super T> choiceRenderer, final int rows, final boolean allowOrder)
143        {
144                this(id, null, choicesModel, choiceRenderer, rows, allowOrder);
145        }
146
147        /**
148         * @param id
149         *            Component id
150         * @param model
151         *            Model representing collection of user's selections
152         * @param choicesModel
153         *            Model representing collection of all available choices
154         * @param choiceRenderer
155         *            Render used to render choices. This must use unique IDs for the objects, not the
156         *            index.
157         * @param rows
158         *            Number of choices to be visible on the screen with out scrolling
159         * @param allowOrder
160         *            Allow user to move selections up and down
161         */
162        public Palette(final String id, final IModel<? extends Collection<T>> model,
163                final IModel<? extends Collection<? extends T>> choicesModel,
164                final IChoiceRenderer<? super T> choiceRenderer, final int rows, final boolean allowOrder)
165        {
166                this(id, model, choicesModel, choiceRenderer, rows, allowOrder, false);
167        }
168
169        /**
170         * Constructor.
171         * 
172         * @param id
173         *            Component id
174         * @param choicesModel
175         *            Model representing collection of all available choices
176         * @param choiceRenderer
177         *            Render used to render choices. This must use unique IDs for the objects, not the
178         *            index.
179         * @param rows
180         *            Number of choices to be visible on the screen with out scrolling
181         * @param allowOrder
182         *            Allow user to move selections up and down
183         * @param allowMoveAll
184         *            Allow user to add or remove all items at once
185         */
186        public Palette(final String id, final IModel<? extends Collection<T>> model,
187                final IModel<? extends Collection<? extends T>> choicesModel,
188                final IChoiceRenderer<? super T> choiceRenderer, final int rows, final boolean allowOrder,
189                boolean allowMoveAll)
190        {
191                super(id, (IModel<Collection<T>>)model);
192
193                this.choicesModel = choicesModel;
194                this.choiceRenderer = choiceRenderer;
195                this.rows = rows;
196                this.allowOrder = allowOrder;
197                this.allowMoveAll = allowMoveAll;
198        }
199
200        @Override
201        protected void onBeforeRender()
202        {
203                if (get("recorder") == null)
204                {
205                        initFactories();
206                }
207                super.onBeforeRender();
208        }
209
210
211        /**
212         * One-time init method for components that are created via overridable factories. This method
213         * is here because we do not want to call overridable methods form palette's constructor.
214         */
215        private void initFactories()
216        {
217                recorderComponent = newRecorderComponent();
218                add(recorderComponent);
219
220                choicesComponent = newChoicesComponent();
221                add(choicesComponent);
222
223                selectionComponent = newSelectionComponent();
224                add(selectionComponent);
225
226
227                add(newAddComponent());
228                add(newRemoveComponent());
229                add(newUpComponent().setVisible(allowOrder));
230                add(newDownComponent().setVisible(allowOrder));
231                add(newAddAllComponent().setVisible(allowMoveAll));
232                add(newRemoveAllComponent().setVisible(allowMoveAll));
233
234                add(newAvailableHeader(AVAILABLE_HEADER_ID));
235                add(newSelectedHeader(SELECTED_HEADER_ID));
236        }
237
238        /**
239         * Return true if the palette is enabled, false otherwise
240         * 
241         * @return true if the palette is enabled, false otherwise
242         */
243        public final boolean isPaletteEnabled()
244        {
245                return isEnabledInHierarchy();
246        }
247
248
249        /**
250         * @return iterator over selected choices
251         */
252        public Iterator<T> getSelectedChoices()
253        {
254                return getRecorderComponent().getSelectedList().iterator();
255        }
256
257        /**
258         * @return iterator over unselected choices
259         */
260        public Iterator<T> getUnselectedChoices()
261        {
262                return getRecorderComponent().getUnselectedList().iterator();
263        }
264
265
266        /**
267         * factory method to create the tracker component
268         * 
269         * @return tracker component
270         */
271        protected Recorder<T> newRecorderComponent()
272        {
273                // create component that will keep track of selections
274                return new Recorder<>("recorder", this);
275        }
276
277        /**
278         * factory method for the available items header
279         * 
280         * @param componentId
281         *            component id of the returned header component
282         * 
283         * @return available items component
284         */
285        protected Component newAvailableHeader(final String componentId)
286        {
287                return new Label(componentId, new ResourceModel("palette.available", "Available"));
288        }
289
290        /**
291         * factory method for the selected items header
292         * 
293         * @param componentId
294         *            component id of the returned header component
295         * 
296         * @return header component
297         */
298        protected Component newSelectedHeader(final String componentId)
299        {
300                return new Label(componentId, new ResourceModel("palette.selected", "Selected"));
301        }
302
303
304        /**
305         * factory method for the move down component
306         * 
307         * @return move down component
308         */
309        protected Component newDownComponent()
310        {
311                return new PaletteButton("moveDownButton")
312                {
313                        private static final long serialVersionUID = 1L;
314
315                        @Override
316                        public void renderHead(IHeaderResponse response)
317                        {
318                                super.renderHead(response);
319                                response.render(
320                                        OnEventHeaderItem.forComponent(this, "click", Palette.this.getDownOnClickJS()));
321                        }
322                };
323        }
324
325        /**
326         * factory method for the move up component
327         * 
328         * @return move up component
329         */
330        protected Component newUpComponent()
331        {
332                return new PaletteButton("moveUpButton")
333                {
334                        private static final long serialVersionUID = 1L;
335
336                        @Override
337                        public void renderHead(IHeaderResponse response)
338                        {
339                                super.renderHead(response);
340                                response.render(
341                                        OnEventHeaderItem.forComponent(this, "click", Palette.this.getUpOnClickJS()));
342                        }
343                };
344        }
345
346        /**
347         * factory method for the remove component
348         * 
349         * @return remove component
350         */
351        protected Component newRemoveComponent()
352        {
353                return new PaletteButton("removeButton")
354                {
355                        private static final long serialVersionUID = 1L;
356
357                        @Override
358                        public void renderHead(IHeaderResponse response)
359                        {
360                                super.renderHead(response);
361                                response.render(OnEventHeaderItem.forComponent(this, "click",
362                                        Palette.this.getRemoveOnClickJS()));
363                        }
364                };
365        }
366
367        /**
368         * factory method for the addcomponent
369         * 
370         * @return add component
371         */
372        protected Component newAddComponent()
373        {
374                return new PaletteButton("addButton")
375                {
376                        private static final long serialVersionUID = 1L;
377
378                        @Override
379                        public void renderHead(IHeaderResponse response)
380                        {
381                                super.renderHead(response);
382                                response.render(
383                                        OnEventHeaderItem.forComponent(this, "click", Palette.this.getAddOnClickJS()));
384                        }
385                };
386        }
387
388        /**
389         * factory method for the selected items component
390         * 
391         * @return selected items component
392         */
393        protected Component newSelectionComponent()
394        {
395                return new Selection<T>("selection", this)
396                {
397                        private static final long serialVersionUID = 1L;
398
399                        @Override
400                        protected Map<String, String> getAdditionalAttributes(final Object choice)
401                        {
402                                return Palette.this.getAdditionalAttributesForSelection(choice);
403                        }
404
405                        @Override
406                        protected boolean localizeDisplayValues()
407                        {
408                                return Palette.this.localizeDisplayValues();
409                        }
410                };
411        }
412
413        /**
414         * factory method for the addAll component
415         * 
416         * @return addAll component
417         */
418        protected Component newAddAllComponent()
419        {
420                return new PaletteButton("addAllButton")
421                {
422                        private static final long serialVersionUID = 1L;
423
424                        @Override
425                        public void renderHead(IHeaderResponse response)
426                        {
427                                super.renderHead(response);
428                                response.render(OnEventHeaderItem.forComponent(this, "click",
429                                        Palette.this.getAddAllOnClickJS()));
430                        }
431                };
432        }
433
434
435        /**
436         * factory method for the removeAll component
437         * 
438         * @return removeAll component
439         */
440        protected Component newRemoveAllComponent()
441        {
442                return new PaletteButton("removeAllButton")
443                {
444                        private static final long serialVersionUID = 1L;
445
446                        @Override
447                        public void renderHead(IHeaderResponse response)
448                        {
449                                super.renderHead(response);
450                                response.render(OnEventHeaderItem.forComponent(this, "click",
451                                        Palette.this.getRemoveAllOnClickJS()));
452                        }
453                };
454        }
455
456        /**
457         * @param choice
458         * @return null
459         * @see org.apache.wicket.extensions.markup.html.form.palette.component.Selection#getAdditionalAttributes(Object)
460         */
461        protected Map<String, String> getAdditionalAttributesForSelection(final Object choice)
462        {
463                return null;
464        }
465
466        /**
467         * factory method for the available items component
468         * 
469         * @return available items component
470         */
471        protected Component newChoicesComponent()
472        {
473                return new Choices<T>("choices", this)
474                {
475                        private static final long serialVersionUID = 1L;
476
477                        @Override
478                        protected Map<String, String> getAdditionalAttributes(final Object choice)
479                        {
480                                return Palette.this.getAdditionalAttributesForChoices(choice);
481                        }
482
483                        @Override
484                        protected boolean localizeDisplayValues()
485                        {
486                                return Palette.this.localizeDisplayValues();
487                        }
488                };
489        }
490
491        /**
492         * Override this method if you do <strong>not</strong> want to localize the display values of
493         * the generated options. By default true is returned.
494         * 
495         * @return true If you want to localize the display values, default == true
496         */
497        protected boolean localizeDisplayValues()
498        {
499                return true;
500        }
501
502        /**
503         * @param choice
504         * @return null
505         * @see org.apache.wicket.extensions.markup.html.form.palette.component.Selection#getAdditionalAttributes(Object)
506         */
507        protected Map<String, String> getAdditionalAttributesForChoices(final Object choice)
508        {
509                return null;
510        }
511
512        protected Component getChoicesComponent()
513        {
514                return choicesComponent;
515        }
516
517        protected Component getSelectionComponent()
518        {
519                return selectionComponent;
520        }
521
522        /**
523         * Returns recorder component. Recorder component is a form component used to track the
524         * selection of the palette. It receives <code>onchange</code> javascript event whenever a
525         * change in selection occurs.
526         * 
527         * @return recorder component
528         */
529        public final Recorder<T> getRecorderComponent()
530        {
531                return recorderComponent;
532        }
533
534        /**
535         * @return collection representing all available items
536         */
537        public Collection<? extends T> getChoices()
538        {
539                return choicesModel.getObject();
540        }
541
542        /**
543         * @return collection representing selected items
544         */
545        @SuppressWarnings("unchecked")
546        public Collection<T> getModelCollection()
547        {
548                return (Collection<T>)getDefaultModelObject();
549        }
550
551        /**
552         * @return choice renderer
553         */
554        public IChoiceRenderer<? super T> getChoiceRenderer()
555        {
556                return choiceRenderer;
557        }
558
559
560        /**
561         * @return items visible without scrolling
562         */
563        public int getRows()
564        {
565                return rows;
566        }
567
568        @Override
569        public void convertInput()
570        {
571                List<T> selectedList = getRecorderComponent().getSelectedList();
572                if (selectedList.isEmpty())
573                {
574                        setConvertedInput(null);
575                }
576                else
577                {
578                        setConvertedInput(selectedList);
579                }
580        }
581
582        /**
583         * The model object is assumed to be a Collection, and it is modified in-place. Then
584         * {@link Model#setObject(Object)} is called with the same instance: it allows the Model to be
585         * notified of changes even when {@link Model#getObject()} returns a different
586         * {@link Collection} at every invocation.
587         * 
588         * @see FormComponent#updateModel()
589         */
590        @Override
591        public final void updateModel()
592        {
593                FormComponent.updateCollectionModel(this);
594        }
595
596        /**
597         * builds javascript handler call
598         * 
599         * @param funcName
600         *            name of javascript function to call
601         * @return string representing the call tho the function with palette params
602         */
603        protected String buildJSCall(final String funcName)
604        {
605                return new StringBuilder(funcName).append("('").append(getChoicesComponent().getMarkupId())
606                        .append("','").append(getSelectionComponent().getMarkupId()).append("','")
607                        .append(getRecorderComponent().getMarkupId()).append("');").toString();
608        }
609
610
611        /**
612         * @return choices component on focus javascript handler
613         */
614        public String getChoicesOnFocusJS()
615        {
616                return buildJSCall("Wicket.Palette.choicesOnFocus");
617        }
618
619        /**
620         * @return selection component on focus javascript handler
621         */
622        public String getSelectionOnFocusJS()
623        {
624                return buildJSCall("Wicket.Palette.selectionOnFocus");
625        }
626
627        /**
628         * @return add action javascript handler
629         */
630        public String getAddOnClickJS()
631        {
632                return buildJSCall("Wicket.Palette.add");
633        }
634
635        /**
636         * @return remove action javascript handler
637         */
638        public String getRemoveOnClickJS()
639        {
640                return buildJSCall("Wicket.Palette.remove");
641        }
642
643        /**
644         * @return move up action javascript handler
645         */
646        public String getUpOnClickJS()
647        {
648                return buildJSCall("Wicket.Palette.moveUp");
649        }
650
651        /**
652         * @return move down action javascript handler
653         */
654        public String getDownOnClickJS()
655        {
656                return buildJSCall("Wicket.Palette.moveDown");
657        }
658
659        /**
660         * @return addAll action javascript handler
661         */
662        public String getAddAllOnClickJS()
663        {
664                return buildJSCall("Wicket.Palette.addAll");
665        }
666
667        /**
668         * @return removeAll action javascript handler
669         */
670        public String getRemoveAllOnClickJS()
671        {
672                return buildJSCall("Wicket.Palette.removeAll");
673        }
674
675        @Override
676        protected void onDetach()
677        {
678                // we need to manually detach the choices model since it is not attached
679                // to a component
680                // an alternative might be to attach it to one of the subcomponents
681                choicesModel.detach();
682
683                choiceRenderer.detach();
684
685                super.onDetach();
686        }
687
688        private class PaletteButton extends WebMarkupContainer
689        {
690
691                private static final long serialVersionUID = 1L;
692
693                /**
694                 * Constructor
695                 * 
696                 * @param id
697                 */
698                public PaletteButton(final String id)
699                {
700                        super(id);
701                }
702
703
704                @Override
705                protected void onComponentTag(final ComponentTag tag)
706                {
707                        super.onComponentTag(tag);
708
709                        if (!isPaletteEnabled())
710                        {
711                                tag.getAttributes().put("disabled", "disabled");
712                        }
713                }
714        }
715
716        /**
717         * Renders header contributions
718         * 
719         * @param response
720         */
721        @Override
722        public void renderHead(final IHeaderResponse response)
723        {
724                response.render(JavaScriptHeaderItem.forReference(JAVASCRIPT));
725        }
726}