-
Notifications
You must be signed in to change notification settings - Fork 389
/
Palette.java
717 lines (629 loc) · 18.2 KB
/
Palette.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
/*
* 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));
}
}