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}