001: // Copyright 2007 The Apache Software Foundation
002: //
003: // Licensed under the Apache License, Version 2.0 (the "License");
004: // you may not use this file except in compliance with the License.
005: // You may obtain a copy of the License at
006: //
007: // http://www.apache.org/licenses/LICENSE-2.0
008: //
009: // Unless required by applicable law or agreed to in writing, software
010: // distributed under the License is distributed on an "AS IS" BASIS,
011: // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012: // See the License for the specific language governing permissions and
013: // limitations under the License.
014:
015: package org.apache.tapestry.corelib.components;
016:
017: import static org.apache.tapestry.ioc.internal.util.CollectionFactory.newList;
018: import static org.apache.tapestry.ioc.internal.util.CollectionFactory.newMap;
019: import static org.apache.tapestry.ioc.internal.util.CollectionFactory.newSet;
020:
021: import java.util.Collections;
022: import java.util.List;
023: import java.util.Map;
024: import java.util.Set;
025:
026: import org.apache.tapestry.Asset;
027: import org.apache.tapestry.Binding;
028: import org.apache.tapestry.MarkupWriter;
029: import org.apache.tapestry.OptionGroupModel;
030: import org.apache.tapestry.OptionModel;
031: import org.apache.tapestry.PageRenderSupport;
032: import org.apache.tapestry.Renderable;
033: import org.apache.tapestry.SelectModel;
034: import org.apache.tapestry.SelectModelVisitor;
035: import org.apache.tapestry.ValueEncoder;
036: import org.apache.tapestry.annotations.Environmental;
037: import org.apache.tapestry.annotations.Inject;
038: import org.apache.tapestry.annotations.Parameter;
039: import org.apache.tapestry.annotations.Path;
040: import org.apache.tapestry.corelib.base.AbstractField;
041: import org.apache.tapestry.internal.util.SelectModelRenderer;
042: import org.apache.tapestry.ioc.internal.util.InternalUtils;
043: import org.apache.tapestry.services.FormSupport;
044: import org.apache.tapestry.services.Request;
045:
046: /**
047: * Multiple selection component. Generates a UI consisting of two <select> elements configured
048: * for multiple selection; the one on the left is the list of "available" elements, the one on the
049: * right is "selected". Elements can be moved between the lists by clicking a button, or double
050: * clicking an option (and eventually, via drag and drop).
051: * <p>
052: * The items in the available list are kept ordered as per {@link SelectModel} order. When items are
053: * moved from the selected list to the available list, they items are inserted back into their
054: * proper positions.
055: * <p>
056: * The Palette may operate in normal or re-orderable mode, controlled by the reorder parameter.
057: * <p>
058: * In normal mode, the items in the selected list are kept in the same "natural" order as the items
059: * in the available list.
060: * <p>
061: * In re-order mode, items moved to the selected list are simply added to the bottom of the list. In addition,
062: * two extra buttons appear to move items up and down within the selected list.
063: * <p>
064: * Much of the look and feel is driven by CSS, the default Tapestry CSS is used to set up the
065: * columns, etc. By default, the <select> element's widths are driven by the length of the
066: * longest <option>, and it is common to override this to a fixed value:
067: *
068: * <pre>
069: * <style>
070: * DIV.t-palette SELECT { width: 300px; }
071: * </style>
072: * </pre>
073: *
074: * <p>
075: * This ensures that the two columns are the same width, and that the column widths don't change
076: * as items move back and forth.
077: *
078: * <p>
079: * Option groups within the {@link SelectModel} will be rendered, but are not supported by the many
080: * browsers, and are not fully handled on the client side.
081: */
082: public class Palette extends AbstractField {
083: // These all started as anonymous inner classes, and were refactored out to here.
084: // I was chasing down one of those perplexing bytecode errors.
085:
086: private final class AvailableRenderer implements Renderable {
087: public void render(MarkupWriter writer) {
088: writer.element("select", "id", getClientId() + ":avail",
089: "multiple", "multiple", "size", getSize(), "name",
090: getElementName() + ":avail");
091:
092: writeDisabled(writer, isDisabled());
093:
094: for (Runnable r : _availableOptions)
095: r.run();
096:
097: writer.end();
098: }
099: }
100:
101: private final class OptionGroupEnd implements Runnable {
102: private final OptionGroupModel _model;
103:
104: private OptionGroupEnd(OptionGroupModel model) {
105: _model = model;
106: }
107:
108: public void run() {
109: _renderer.endOptionGroup(_model);
110: }
111: }
112:
113: private final class OptionGroupStart implements Runnable {
114: private final OptionGroupModel _model;
115:
116: private OptionGroupStart(OptionGroupModel model) {
117: _model = model;
118: }
119:
120: public void run() {
121: _renderer.beginOptionGroup(_model);
122: }
123: }
124:
125: private final class RenderOption implements Runnable {
126: private final OptionModel _model;
127:
128: private RenderOption(OptionModel model) {
129: _model = model;
130: }
131:
132: public void run() {
133: _renderer.option(_model);
134: }
135: }
136:
137: private final class SelectedRenderer implements Renderable {
138: public void render(MarkupWriter writer) {
139: writer.element("select", "id", getClientId(), "multiple",
140: "multiple", "size", getSize(), "name",
141: getElementName());
142:
143: writeDisabled(writer, isDisabled());
144:
145: for (Object value : getSelected()) {
146: OptionModel model = _valueToOptionModel.get(value);
147:
148: _renderer.option(model);
149: }
150:
151: writer.end();
152: }
153: }
154:
155: /** List of Runnable commands to render the available options. */
156: private List<Runnable> _availableOptions;
157:
158: /**
159: * The image to use for the deselect button (the default is a left pointing arrow).
160: */
161: @Parameter(value="asset:deselect.png")
162: private Asset _deselect;
163:
164: /**
165: * Encoder used to translate between server-side objects and client-side strings.
166: */
167: @Parameter(required=true)
168: private ValueEncoder<Object> _encoder;
169:
170: /**
171: * Model used to define the values and labels used when rendering.
172: */
173: @Parameter(required=true)
174: private SelectModel _model;
175:
176: /**
177: * The image to use for the move down button (the default is a downward pointing arrow).
178: */
179: @Parameter(value="asset:move_down.png")
180: private Asset _moveDown;
181:
182: /**
183: * The image to use for the move up button (the default is an upward pointing arrow).
184: */
185: @Parameter(value="asset:move_up.png")
186: private Asset _moveUp;
187:
188: @Inject
189: @Path("palette.js")
190: private Asset _paletteLibrary;
191:
192: /** Used to include scripting code in the rendered page. */
193: @Environmental
194: private PageRenderSupport _renderSupport;
195:
196: /** Needed to access query parameters when processing form submission. */
197: @Inject
198: private Request _request;
199:
200: private SelectModelRenderer _renderer;
201:
202: /**
203: * The image to use for the select button (the default is a right pointing arrow).
204: */
205: @Parameter(value="asset:select.png")
206: private Asset _select;
207:
208: /**
209: * The list of selected values from the {@link SelectModel}. This will be updated when the form
210: * is submitted. If the value for the parameter is null, a new list will be created, otherwise
211: * the existing list will be cleared. If unbound, defaults to a property of the container
212: * matching this component's id.
213: */
214: @Parameter(required=true)
215: private List<Object> _selected;
216:
217: /**
218: * If true, then additional buttons are provided on the client-side to allow for re-ordering of
219: * the values.
220: */
221: @Parameter("false")
222: private boolean _reorder;
223:
224: /**
225: * Used during rendering to identify the options corresponding to selected values (from the
226: * selected parameter), in the order they should be displayed on the page.
227: */
228: private List<OptionModel> _selectedOptions;
229:
230: private Map<Object, OptionModel> _valueToOptionModel;
231:
232: /**
233: * Number of rows to display.
234: */
235: @Parameter(value="10")
236: private int _size;
237:
238: /**
239: * Defaults the selected parameter to a container property whose name matches this component's
240: * id.
241: */
242: final Binding defaultSelected() {
243: return createDefaultParameterBinding("selected");
244: }
245:
246: public Renderable getAvailableRenderer() {
247: return new AvailableRenderer();
248: }
249:
250: public Asset getDeselect() {
251: return _deselect;
252: }
253:
254: public Asset getMoveDown() {
255: return _moveDown;
256: }
257:
258: public Asset getMoveUp() {
259: return _moveUp;
260: }
261:
262: public Asset getSelect() {
263: return _select;
264: }
265:
266: public Renderable getSelectedRenderer() {
267: return new SelectedRenderer();
268: }
269:
270: @Override
271: protected void processSubmission(FormSupport formSupport,
272: String elementName) {
273: String values = _request.getParameter(elementName + ":values");
274:
275: // Use a couple of local variables to cut down on access via bindings
276:
277: List<Object> selected = _selected;
278:
279: if (selected == null)
280: selected = newList();
281: else
282: selected.clear();
283:
284: ValueEncoder encoder = _encoder;
285:
286: if (InternalUtils.isNonBlank(values)) {
287: for (String value : values.split(";")) {
288: Object objectValue = encoder.toValue(value);
289:
290: selected.add(objectValue);
291: }
292: }
293:
294: _selected = selected;
295: }
296:
297: private void writeDisabled(MarkupWriter writer, boolean disabled) {
298: if (disabled)
299: writer.attributes("disabled", "disabled");
300: }
301:
302: void beginRender(MarkupWriter writer) {
303: String sep = "";
304: StringBuilder selectedValues = new StringBuilder();
305:
306: for (OptionModel selected : _selectedOptions) {
307:
308: Object value = selected.getValue();
309: String clientValue = _encoder.toClient(value);
310:
311: selectedValues.append(sep);
312: selectedValues.append(clientValue);
313:
314: sep = ";";
315: }
316:
317: StringBuilder naturalOrder = new StringBuilder();
318: sep = "";
319: for (String value : _naturalOrder) {
320: naturalOrder.append(sep);
321: naturalOrder.append(value);
322: sep = ";";
323: }
324:
325: String clientId = getClientId();
326:
327: _renderSupport.addScriptLink(_paletteLibrary);
328:
329: _renderSupport.addScript(
330: "new Tapestry.Palette('%s', %s, '%s');", clientId,
331: _reorder, naturalOrder);
332:
333: writer.element("input", "type", "hidden", "id", clientId
334: + ":values", "name", getElementName() + ":values",
335: "value", selectedValues);
336: writer.end();
337: }
338:
339: /** Prevent the body from rendering. */
340: boolean beforeRenderBody() {
341: return false;
342: }
343:
344: /** The natural order of elements, in terms of their client ids. */
345: private List<String> _naturalOrder;
346:
347: @SuppressWarnings("unchecked")
348: void setupRender(MarkupWriter writer) {
349: _valueToOptionModel = newMap();
350: _availableOptions = newList();
351: _selectedOptions = newList();
352: _naturalOrder = newList();
353: _renderer = new SelectModelRenderer(writer, _encoder);
354:
355: final Set selectedSet = newSet(getSelected());
356:
357: SelectModelVisitor visitor = new SelectModelVisitor() {
358: public void beginOptionGroup(OptionGroupModel groupModel) {
359: _availableOptions.add(new OptionGroupStart(groupModel));
360: }
361:
362: public void endOptionGroup(OptionGroupModel groupModel) {
363: _availableOptions.add(new OptionGroupEnd(groupModel));
364: }
365:
366: public void option(OptionModel optionModel) {
367: Object value = optionModel.getValue();
368:
369: boolean isSelected = selectedSet.contains(value);
370:
371: String clientValue = toClient(value);
372:
373: _naturalOrder.add(clientValue);
374:
375: if (isSelected) {
376: _selectedOptions.add(optionModel);
377: _valueToOptionModel.put(value, optionModel);
378: return;
379: }
380:
381: _availableOptions.add(new RenderOption(optionModel));
382: }
383:
384: };
385:
386: _model.visit(visitor);
387: }
388:
389: // Avoids a strange Javassist bytecode error, c'est lavie!
390: int getSize() {
391: return _size;
392: }
393:
394: String toClient(Object value) {
395: return _encoder.toClient(value);
396: }
397:
398: List<Object> getSelected() {
399: if (_selected == null)
400: return Collections.emptyList();
401:
402: return _selected;
403: }
404:
405: public boolean getReorder() {
406: return _reorder;
407: }
408: }
|