001: /*
002: * $Id: Palette.java 462288 2006-09-17 02:50:03 -0700 (Sun, 17 Sep 2006)
003: * ehillenius $ $Revision: 464007 $ $Date: 2006-09-17 02:50:03 -0700 (Sun, 17
004: * Sep 2006) $
005: *
006: * ==============================================================================
007: * Licensed under the Apache License, Version 2.0 (the "License"); you may not
008: * use this file except in compliance with the License. You may obtain a copy of
009: * the License at
010: *
011: * http://www.apache.org/licenses/LICENSE-2.0
012: *
013: * Unless required by applicable law or agreed to in writing, software
014: * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
015: * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
016: * License for the specific language governing permissions and limitations under
017: * the License.
018: */
019: package wicket.extensions.markup.html.form.palette;
020:
021: import java.util.Collection;
022: import java.util.Iterator;
023:
024: import wicket.AttributeModifier;
025: import wicket.Component;
026: import wicket.ResourceReference;
027: import wicket.extensions.markup.html.form.palette.component.Choices;
028: import wicket.extensions.markup.html.form.palette.component.Recorder;
029: import wicket.extensions.markup.html.form.palette.component.Selection;
030: import wicket.markup.ComponentTag;
031: import wicket.markup.html.WebMarkupContainer;
032: import wicket.markup.html.basic.Label;
033: import wicket.markup.html.form.IChoiceRenderer;
034: import wicket.markup.html.image.Image;
035: import wicket.markup.html.panel.Panel;
036: import wicket.model.AbstractModel;
037: import wicket.model.IModel;
038: import wicket.model.Model;
039:
040: /**
041: * Palette is a component that allows the user to easily select and order
042: * multiple items by moving them from one select box into another.
043: *
044: * <strong>Ajaxifying the palette</strong>: The palette itself cannot be
045: * ajaxified because it is a panel and therefore does not receive any javascript
046: * events. Instead ajax behaviors can be attached to the recorder component
047: * which supports the javascript <code>onchange</code> event. The recorder
048: * component can be retrieved via a call to {@link #getRecorderComponent()}.
049: *
050: * Example:
051: *
052: * <pre>
053: * Form form=new Form(...);
054: * Palette palette=new Palette(...);
055: * palette.getRecorderComponent().add(new AjaxFormComponentUpdatingBehavior("onchange") {...});
056: * </pre>
057: *
058: * @author Igor Vaynberg ( ivaynberg )
059: *
060: */
061: public class Palette extends Panel {
062: private static final String SELECTED_HEADER_ID = "selectedHeader";
063:
064: private static final String AVAILABLE_HEADER_ID = "availableHeader";
065:
066: private static final long serialVersionUID = 1L;
067:
068: /** collection containing all available choices */
069: private IModel choicesModel;
070:
071: /**
072: * choice render used to render the choices in both available and selected
073: * collections
074: */
075: private IChoiceRenderer choiceRenderer;
076:
077: /** number of rows to show in the select boxes */
078: private int rows;
079:
080: /**
081: * recorder component used to track user's selection. it is updated by
082: * javascript on changes.
083: */
084: private Recorder recorderComponent;
085:
086: /**
087: * component used to represent all available choices. by default this is a
088: * select box with multiple attribute
089: */
090: private Component choicesComponent;
091:
092: /**
093: * component used to represent selected items. by default this is a select
094: * box with multiple attribute
095: */
096: private Component selectionComponent;
097:
098: /** reference to the palette's javascript resource */
099: private static final ResourceReference javascript = new ResourceReference(
100: Palette.class, "palette.js");
101:
102: /** reference to default up buttom image */
103: private static final ResourceReference upImage = new ResourceReference(
104: Palette.class, "up.gif");
105:
106: /** reference to default down button image */
107: private static final ResourceReference downImage = new ResourceReference(
108: Palette.class, "down.gif");
109:
110: /** reference to default remove button image */
111: private static final ResourceReference removeImage = new ResourceReference(
112: Palette.class, "remove.gif");
113:
114: /** reference to default add buttom image */
115: private static final ResourceReference addImage = new ResourceReference(
116: Palette.class, "add.gif");
117:
118: /**
119: * @param id
120: * component id
121: * @param choicesModel
122: * model representing collection of all available choices
123: * @param choiceRenderer
124: * render used to render choices
125: * @param rows
126: * number of choices to be visible on the screen with out
127: * scrolling
128: * @param allowOrder
129: * allow user to move selections up and down
130: */
131: public Palette(String id, IModel choicesModel,
132: IChoiceRenderer choiceRenderer, int rows, boolean allowOrder) {
133: this (id, null, choicesModel, choiceRenderer, rows, allowOrder);
134: }
135:
136: /**
137: * @param id
138: * component id
139: * @param model
140: * model representing collection of user's selections
141: * @param choicesModel
142: * model representing collection of all available choices
143: * @param choiceRenderer
144: * render used to render choices
145: * @param rows
146: * number of choices to be visible on the screen with out
147: * scrolling
148: * @param allowOrder
149: * allow user to move selections up and down
150: */
151: public Palette(String id, IModel model, IModel choicesModel,
152: IChoiceRenderer choiceRenderer, int rows, boolean allowOrder) {
153: super (id, model);
154:
155: this .choicesModel = choicesModel;
156: this .choiceRenderer = choiceRenderer;
157: this .rows = rows;
158: recorderComponent = newRecorderComponent();
159: add(recorderComponent);
160:
161: choicesComponent = newChoicesComponent();
162: add(choicesComponent);
163:
164: selectionComponent = newSelectionComponent();
165: add(selectionComponent);
166:
167: add(newAddComponent());
168: add(newRemoveComponent());
169: add(newUpComponent().setVisible(allowOrder));
170: add(newDownComponent().setVisible(allowOrder));
171:
172: add(newAvailableHeader(AVAILABLE_HEADER_ID));
173: add(newSelectedHeader(SELECTED_HEADER_ID));
174:
175: addJavascript();
176: }
177:
178: /**
179: * adds the component used to represent the link the the javascript file to
180: * the header
181: */
182: private void addJavascript() {
183: IModel srcReplacement = new Model() {
184: private static final long serialVersionUID = 1L;
185:
186: public Object getObject(Component component) {
187: return urlFor(javascript);
188: };
189: };
190: WebMarkupContainer javascript = new WebMarkupContainer(
191: "javascript");
192: javascript.add(new AttributeModifier("src", true,
193: srcReplacement));
194: add(javascript);
195: }
196:
197: /**
198: * Return true if the palette is enabled, false otherwise
199: *
200: * @return true if the palette is enabled, false otherwise
201: */
202: public final boolean isPaletteEnabled() {
203: return isEnabled() && isEnableAllowed();
204: }
205:
206: /**
207: * @return iterator over selected choices
208: */
209: public Iterator getSelectedChoices() {
210: return getRecorderComponent().getSelectedChoices();
211: }
212:
213: /**
214: * @return iterator over unselected choices
215: */
216: public Iterator getUnselectedChoices() {
217: return getRecorderComponent().getUnselectedChoices();
218: }
219:
220: /**
221: * factory method to create the tracker component
222: *
223: * @return tracker component
224: */
225: private Recorder newRecorderComponent() {
226: // create component that will keep track of selections
227: return new Recorder("recorder", this ) {
228: private static final long serialVersionUID = 1L;
229:
230: public void updateModel() {
231: super .updateModel();
232: Palette.this .updateModel();
233: }
234: };
235: }
236:
237: /**
238: * factory method for the available items header
239: *
240: * @param componentId
241: * component id of the returned header component
242: *
243: * @return available items component
244: */
245: protected Component newAvailableHeader(String componentId) {
246: return new Label(componentId, "Available");
247: }
248:
249: /**
250: * factory method for the selected items header
251: *
252: * @param componentId
253: * component id of the returned header component
254: *
255: * @return header component
256: */
257: protected Component newSelectedHeader(String componentId) {
258: return new Label(componentId, "Selected");
259: }
260:
261: /**
262: * factory method for the move down component
263: *
264: * @return move down component
265: */
266: protected Component newDownComponent() {
267: return new PaletteButton("moveDownButton") {
268: private static final long serialVersionUID = 1L;
269:
270: protected void onComponentTag(ComponentTag tag) {
271: super .onComponentTag(tag);
272: tag.getAttributes().put("onclick",
273: Palette.this .getDownOnClickJS());
274: }
275: }.add(new Image("image", downImage));
276: }
277:
278: /**
279: * factory method for the move up component
280: *
281: * @return move up component
282: */
283: protected Component newUpComponent() {
284: return new PaletteButton("moveUpButton") {
285: private static final long serialVersionUID = 1L;
286:
287: protected void onComponentTag(ComponentTag tag) {
288: super .onComponentTag(tag);
289: tag.getAttributes().put("onclick",
290: Palette.this .getUpOnClickJS());
291: }
292: }.add(new Image("image", upImage));
293: }
294:
295: /**
296: * factory method for the remove component
297: *
298: * @return remove component
299: */
300: protected Component newRemoveComponent() {
301: return new PaletteButton("removeButton") {
302: private static final long serialVersionUID = 1L;
303:
304: protected void onComponentTag(ComponentTag tag) {
305: super .onComponentTag(tag);
306: tag.getAttributes().put("onclick",
307: Palette.this .getRemoveOnClickJS());
308: }
309: }.add(new Image("image", removeImage));
310: }
311:
312: /**
313: * factory method for the addcomponent
314: *
315: * @return add component
316: */
317: protected Component newAddComponent() {
318: return new PaletteButton("addButton") {
319: private static final long serialVersionUID = 1L;
320:
321: protected void onComponentTag(ComponentTag tag) {
322: super .onComponentTag(tag);
323: tag.getAttributes().put("onclick",
324: Palette.this .getAddOnClickJS());
325: }
326: }.add(new Image("image", addImage));
327: }
328:
329: /**
330: * factory method for the selected items component
331: *
332: * @return selected items component
333: */
334: protected Component newSelectionComponent() {
335: return new Selection("selection", this );
336: }
337:
338: /**
339: * factory method for the available items component
340: *
341: * @return available items component
342: */
343: protected Component newChoicesComponent() {
344: return new Choices("choices", this );
345: }
346:
347: private Component getChoicesComponent() {
348: return choicesComponent;
349: }
350:
351: private Component getSelectionComponent() {
352: return selectionComponent;
353: }
354:
355: /**
356: * Returns recorder component. Recorder component is a form component used
357: * to track the selection of the palette. It receives <code>onchange</code>
358: * javascript event whenever a change in selection occurs.
359: *
360: * @return recorder component
361: */
362: public final Recorder getRecorderComponent() {
363: return recorderComponent;
364: }
365:
366: /**
367: * @return collection representing all available items
368: */
369: public Collection getChoices() {
370: return (Collection) choicesModel.getObject(this );
371: }
372:
373: /**
374: * @return collection representing selected items
375: */
376: public Collection getModelCollection() {
377: return (Collection) getModelObject();
378: }
379:
380: /**
381: * @return choice renderer
382: */
383: public IChoiceRenderer getChoiceRenderer() {
384: return choiceRenderer;
385: }
386:
387: /**
388: * @return items visible without scrolling
389: */
390: public int getRows() {
391: return rows;
392: }
393:
394: /**
395: * update the model upon form processing
396: */
397: protected final void updateModel() {
398: // prepare model
399: Collection model = (Collection) getModelObject();
400: model.clear();
401:
402: // update model
403: Iterator it = getRecorderComponent().getSelectedChoices();
404:
405: while (it.hasNext()) {
406: final Object selectedChoice = it.next();
407: model.add(selectedChoice);
408: }
409: }
410:
411: /**
412: * builds javascript handler call
413: *
414: * @param funcName
415: * name of javascript function to call
416: * @return string representing the call tho the function with palette params
417: */
418: protected String buildJSCall(String funcName) {
419: return new StringBuffer(funcName).append("('").append(
420: getChoicesComponent().getMarkupId()).append("','")
421: .append(getSelectionComponent().getMarkupId()).append(
422: "','").append(
423: getRecorderComponent().getMarkupId()).append(
424: "');").toString();
425: }
426:
427: /**
428: * @return choices component on focus javascript handler
429: */
430: public String getChoicesOnFocusJS() {
431: return buildJSCall("paletteChoicesOnFocus");
432: }
433:
434: /**
435: * @return selection component on focus javascript handler
436: */
437: public String getSelectionOnFocusJS() {
438: return buildJSCall("paletteSelectionOnFocus");
439: }
440:
441: /**
442: * @return add action javascript handler
443: */
444: public String getAddOnClickJS() {
445: return buildJSCall("paletteAdd");
446: }
447:
448: /**
449: * @return remove action javascript handler
450: */
451: public String getRemoveOnClickJS() {
452: return buildJSCall("paletteRemove");
453: }
454:
455: /**
456: * @return move up action javascript handler
457: */
458: public String getUpOnClickJS() {
459: return buildJSCall("paletteMoveUp");
460: }
461:
462: /**
463: * @return move down action javascript handler
464: */
465: public String getDownOnClickJS() {
466: return buildJSCall("paletteMoveDown");
467: }
468:
469: protected void internalOnDetach() {
470: super .internalOnDetach();
471: // we need to manually detach the choices model since it is not attached
472: // to a component
473: // an alternative might be to attach it to one of the subcomponents
474: choicesModel.detach();
475: }
476:
477: private class PaletteButton extends WebMarkupContainer {
478:
479: private static final long serialVersionUID = 1L;
480:
481: /**
482: * Constructor
483: *
484: * @param id
485: */
486: public PaletteButton(String id) {
487: super (id);
488: }
489:
490: protected void onComponentTag(ComponentTag tag) {
491: if (!isPaletteEnabled()) {
492: tag.getAttributes().put("disabled", "disabled");
493: }
494: }
495: }
496:
497: /**
498: * Model that allows other components to benefit of the compound model that
499: * AjaxEditableLabel inherits.
500: */
501: private final class PassThroughModel extends AbstractModel {
502: private static final long serialVersionUID = 1L;
503:
504: /**
505: * @see wicket.model.IModel#getObject(wicket.Component)
506: */
507: public Object getObject(Component component) {
508: return getModel().getObject(Palette.this );
509: }
510:
511: /**
512: * @see wicket.model.IModel#setObject(wicket.Component,
513: * java.lang.Object)
514: */
515: public void setObject(Component component, Object object) {
516: getModel().setObject(Palette.this, object);
517: }
518: }
519:
520: }
|