001 /*
002 * Apache License
003 * Version 2.0, January 2004
004 * http://www.apache.org/licenses/
005 *
006 * Copyright 2008-2010 by chenillekit.org
007 *
008 * Licensed under the Apache License, Version 2.0 (the "License");
009 * you may not use this file except in compliance with the License.
010 * You may obtain a copy of the License at
011 *
012 * http://www.apache.org/licenses/LICENSE-2.0
013 */
014
015 package org.chenillekit.tapestry.core.components.prototype_ui;
016
017 import org.apache.commons.lang.StringUtils;
018 import org.apache.tapestry5.BindingConstants;
019 import org.apache.tapestry5.ComponentEventCallback;
020 import org.apache.tapestry5.ComponentResources;
021 import org.apache.tapestry5.FieldTranslator;
022 import org.apache.tapestry5.MarkupWriter;
023 import org.apache.tapestry5.ValidationException;
024 import org.apache.tapestry5.ValidationTracker;
025 import org.apache.tapestry5.annotations.Environmental;
026 import org.apache.tapestry5.annotations.Import;
027 import org.apache.tapestry5.annotations.Parameter;
028 import org.apache.tapestry5.corelib.base.AbstractField;
029 import org.apache.tapestry5.internal.util.Holder;
030 import org.apache.tapestry5.ioc.annotations.Inject;
031 import org.apache.tapestry5.ioc.services.PropertyAccess;
032 import org.apache.tapestry5.ioc.services.TypeCoercer;
033 import org.apache.tapestry5.json.JSONArray;
034 import org.apache.tapestry5.json.JSONObject;
035 import org.apache.tapestry5.services.Request;
036 import org.apache.tapestry5.services.ResponseRenderer;
037 import org.apache.tapestry5.services.javascript.JavaScriptSupport;
038
039 import java.util.Collections;
040 import java.util.List;
041
042 import static org.apache.tapestry5.ioc.internal.util.CollectionFactory.newList;
043
044 /**
045 * This AutoComplete component based on <a href="http://www.prototype-ui.com/">Prototype-UI's</a>
046 * <a href="http://blog.xilinus.com/2008/2/22/new-component-auto_complete-in-prototype-ui">autocomplete</a> widget.
047 *
048 * @version $Id: AutoComplete.java 674 2010-07-29 12:47:25Z homburgs $
049 */
050 @Import(library = {"../../Chenillekit.js", "prototype-ui.js", "AutoComplete.js"},
051 stylesheet = {"themes/auto_complete/default.css", "themes/shadow/drop_shadow.css",
052 "themes/shadow/auto_complete.css"})
053 public class AutoComplete extends AbstractField
054 {
055 static final String EVENT_NAME = "autocomplete";
056
057 private static final String PARAM_NAME = "search";
058
059 /**
060 * The value to read or update.
061 */
062 @Parameter(required = true, allowNull = false)
063 private List<Object> selected;
064
065 /**
066 * The object which will perform translation between server-side and client-side representations. If not specified,
067 * a value will usually be generated based on the type of the value parameter.
068 */
069 @Parameter(required = true, allowNull = false, defaultPrefix = BindingConstants.TRANSLATE)
070 private FieldTranslator<Object> translate;
071
072 /**
073 * this parameter contains the name of the object property, that should display to user in the item list and the
074 * box of selected items.
075 */
076 @Parameter(required = true, allowNull = false, defaultPrefix = BindingConstants.LITERAL, name = "label")
077 private String labelPropertyName;
078
079 @Inject
080 private Request request;
081
082 @Inject
083 private ResponseRenderer responseRenderer;
084
085 @Inject
086 private TypeCoercer coercer;
087
088 @Inject
089 private ComponentResources resources;
090
091 @Environmental
092 private JavaScriptSupport javascriptSupport;
093
094 @Inject
095 private PropertyAccess propertyAccess;
096
097 @Environmental
098 private ValidationTracker tracker;
099
100 void beginRender(MarkupWriter writer)
101 {
102 writer.element("input",
103 "type", "hidden",
104 "name", getControlName(),
105 "id", getClientId() + "-internal");
106 writer.end();
107
108 writer.element("input",
109 "type", "text",
110 "id", getClientId());
111
112 }
113
114 void afterRender(MarkupWriter writer)
115 {
116 writer.end();
117
118 JSONObject config = new JSONObject();
119 config.put("url", resources.createEventLink(EVENT_NAME).toAbsoluteURI());
120 config.put("preSelected", generateResponseMarkup(selected));
121
122 configure(config);
123
124 javascriptSupport.addScript("new Ck.AutoComplete('%s', %s);", getClientId(), config);
125 }
126
127 /**
128 * Invoked to allow subclasses to further configure the parameters passed to this component's javascript
129 * options. Subclasses may override this method to configure additional features of this component.
130 * <p/>
131 * This implementation does nothing. For more information about window options look at
132 * this <a href="http://prototype-window.xilinus.com/documentation.html#initialize">page</a>.
133 *
134 * @param options windows option object
135 */
136 protected void configure(JSONObject options)
137 {
138
139 }
140
141 JSONArray onAutocomplete()
142 {
143 String input = request.getParameter(PARAM_NAME);
144
145 final Holder<List> matchesHolder = Holder.create();
146
147 // Default it to an empty list.
148
149 matchesHolder.put(Collections.emptyList());
150
151 ComponentEventCallback callback = new ComponentEventCallback()
152 {
153 public boolean handleResult(Object result)
154 {
155 List matches = coercer.coerce(result, List.class);
156
157 matchesHolder.put(matches);
158
159 return true;
160 }
161 };
162
163 resources.triggerEvent("providecompletions", new Object[]{input}, callback);
164
165 return generateResponseMarkup(matchesHolder.get());
166 }
167
168 /**
169 * Method implemented by subclasses to actually do the work of processing the submission of the form. The element's
170 * elementName property will already have been set. This method is only invoked if the field is <strong>not {@link
171 * #isDisabled() disabled}</strong>.
172 *
173 * @param elementName the name of the element (used to find the correct parameter in the request)
174 */
175 protected void processSubmission(String elementName)
176 {
177 String parameterValue = request.getParameter(elementName);
178 String[] values = parameterValue.split(",");
179
180 // Use a couple of local variables to cut down on access via bindings
181 List<Object> selected = this.selected;
182
183 if (selected == null) selected = newList();
184 else selected.clear();
185
186 int count = values.length;
187 try
188 {
189 for (int i = 0; i < count; i++)
190 {
191 String value = StringUtils.trim(values[i]);
192
193 Object objectValue = translate.parse(value);
194
195 if (objectValue != null)
196 selected.add(objectValue);
197 }
198 }
199 catch (ValidationException ex)
200 {
201 tracker.recordError(this, ex.getMessage());
202 }
203
204 this.selected = selected;
205 }
206
207 /**
208 * Generates the markup response that will be returned to the client; this should be an <ul> element with
209 * nested <li> elements. Subclasses may override this to produce more involved markup (including images and
210 * CSS class attributes).
211 *
212 * @param matches list of matching objects, each should be converted to a string
213 */
214 protected JSONArray generateResponseMarkup(List matches)
215 {
216 JSONArray jsonObject = new JSONArray();
217 for (Object o : matches)
218 {
219 Object value = translate.toClient(o);
220 Object label = propertyAccess.get(o, labelPropertyName);
221 JSONObject item = new JSONObject();
222 item.put("text", label);
223 item.put("value", value);
224 jsonObject.put(item);
225 }
226
227 return jsonObject;
228 }
229 }