001 // Copyright 2007, 2008, 2009, 2010, 2011 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.tapestry5.corelib.mixins;
016
017 import java.util.Collections;
018 import java.util.List;
019
020 import org.apache.tapestry5.Asset;
021 import org.apache.tapestry5.BindingConstants;
022 import org.apache.tapestry5.CSSClassConstants;
023 import org.apache.tapestry5.ComponentEventCallback;
024 import org.apache.tapestry5.ComponentResources;
025 import org.apache.tapestry5.ContentType;
026 import org.apache.tapestry5.EventConstants;
027 import org.apache.tapestry5.Field;
028 import org.apache.tapestry5.Link;
029 import org.apache.tapestry5.MarkupWriter;
030 import org.apache.tapestry5.annotations.Environmental;
031 import org.apache.tapestry5.annotations.Events;
032 import org.apache.tapestry5.annotations.Import;
033 import org.apache.tapestry5.annotations.InjectContainer;
034 import org.apache.tapestry5.annotations.Parameter;
035 import org.apache.tapestry5.annotations.Path;
036 import org.apache.tapestry5.annotations.RequestParameter;
037 import org.apache.tapestry5.internal.util.Holder;
038 import org.apache.tapestry5.ioc.annotations.Inject;
039 import org.apache.tapestry5.ioc.services.TypeCoercer;
040 import org.apache.tapestry5.json.JSONObject;
041 import org.apache.tapestry5.services.MarkupWriterFactory;
042 import org.apache.tapestry5.services.ResponseRenderer;
043 import org.apache.tapestry5.services.javascript.JavaScriptSupport;
044 import org.apache.tapestry5.util.TextStreamResponse;
045
046 /**
047 * A mixin for a text field that allows for autocompletion of text fields. This is based on Prototype's autocompleter
048 * control.
049 * <p/>
050 * The mixin renders an (initially invisible) progress indicator after the field (it will also be after the error icon
051 * most fields render). The progress indicator is made visible during the request to the server. The mixin then renders
052 * a <div> that will be filled in on the client side with dynamically obtained selections.
053 * <p/>
054 * Multiple selection on the client is enabled by binding the tokens parameter (however, the mixin doesn't help split
055 * multiple selections up on the server, that is still your code's responsibility).
056 * <p/>
057 * The container is responsible for providing an event handler for event "providecompletions". The context will be the
058 * partial input string sent from the client. The return value should be an array or list of completions, in
059 * presentation order. I.e.
060 * <p/>
061 *
062 * <pre>
063 * String[] onProvideCompletionsFromMyField(String input)
064 * {
065 * return . . .;
066 * }
067 * </pre>
068 *
069 * @tapestrydoc
070 */
071 @Import(library =
072 { "${tapestry.scriptaculous}/controls.js", "autocomplete.js" })
073 @Events(EventConstants.PROVIDE_COMPLETIONS)
074 public class Autocomplete
075 {
076 static final String EVENT_NAME = "autocomplete";
077
078 private static final String PARAM_NAME = "t:input";
079
080 /**
081 * The field component to which this mixin is attached.
082 */
083 @InjectContainer
084 private Field field;
085
086 @Inject
087 private ComponentResources resources;
088
089 @Environmental
090 private JavaScriptSupport jsSupport;
091
092 @Inject
093 private TypeCoercer coercer;
094
095 @Inject
096 private MarkupWriterFactory factory;
097
098 @Inject
099 @Path("${tapestry.spacer-image}")
100 private Asset spacerImage;
101
102 /**
103 * Overwrites the default minimum characters to trigger a server round trip (the default is 1).
104 */
105 @Parameter(defaultPrefix = BindingConstants.LITERAL)
106 private int minChars;
107
108 @Inject
109 private ResponseRenderer responseRenderer;
110
111 /**
112 * Overrides the default check frequency for determining whether to send a server request. The default is .4
113 * seconds.
114 */
115 @Parameter(defaultPrefix = BindingConstants.LITERAL)
116 private double frequency;
117
118 /**
119 * If given, then the autocompleter will support multiple input values, seperated by any of the individual
120 * characters in the string.
121 */
122 @Parameter(defaultPrefix = BindingConstants.LITERAL)
123 private String tokens;
124
125 /**
126 * Mixin afterRender phrase occurs after the component itself. This is where we write the <div> element and
127 * the JavaScript.
128 *
129 * @param writer
130 */
131 void afterRender(MarkupWriter writer)
132 {
133 String id = field.getClientId();
134
135 String menuId = id + ":menu";
136 String loaderId = id + ":loader";
137
138 // The spacer image is used as a placeholder, allowing CSS to determine what image
139 // is actually displayed.
140
141 writer.element("img",
142
143 "src", spacerImage.toClientURL(),
144
145 "class", "t-autoloader-icon " + CSSClassConstants.INVISIBLE,
146
147 "alt", "",
148
149 "id", loaderId);
150 writer.end();
151
152 writer.element("div",
153
154 "id", menuId,
155
156 "class", "t-autocomplete-menu");
157 writer.end();
158
159 Link link = resources.createEventLink(EVENT_NAME);
160
161 JSONObject config = new JSONObject();
162 config.put("paramName", PARAM_NAME);
163 config.put("indicator", loaderId);
164
165 if (resources.isBound("minChars"))
166 config.put("minChars", minChars);
167
168 if (resources.isBound("frequency"))
169 config.put("frequency", frequency);
170
171 if (resources.isBound("tokens"))
172 {
173 for (int i = 0; i < tokens.length(); i++)
174 {
175 config.accumulate("tokens", tokens.substring(i, i + 1));
176 }
177 }
178
179 // Let subclasses do more.
180 configure(config);
181
182 JSONObject spec = new JSONObject("elementId", id, "menuId", menuId, "url", link.toURI()).put("config",
183 config);
184
185 jsSupport.addInitializerCall("autocompleter", spec);
186 }
187
188 Object onAutocomplete(@RequestParameter(PARAM_NAME)
189 String input)
190 {
191 final Holder<List> matchesHolder = Holder.create();
192
193 // Default it to an empty list.
194
195 matchesHolder.put(Collections.emptyList());
196
197 ComponentEventCallback callback = new ComponentEventCallback()
198 {
199 public boolean handleResult(Object result)
200 {
201 List matches = coercer.coerce(result, List.class);
202
203 matchesHolder.put(matches);
204
205 return true;
206 }
207 };
208
209 resources.triggerEvent(EventConstants.PROVIDE_COMPLETIONS, new Object[]
210 { input }, callback);
211
212 ContentType contentType = responseRenderer.findContentType(this);
213
214 MarkupWriter writer = factory.newPartialMarkupWriter(contentType);
215
216 generateResponseMarkup(writer, matchesHolder.get());
217
218 return new TextStreamResponse(contentType.toString(), writer.toString());
219 }
220
221 /**
222 * Invoked to allow subclasses to further configure the parameters passed to the JavaScript Ajax.Autocompleter
223 * options. The values minChars, frequency and tokens my be pre-configured. Subclasses may override this method to
224 * configure additional features of the Ajax.Autocompleter.
225 * <p/>
226 * <p/>
227 * This implementation does nothing.
228 *
229 * @param config
230 * parameters object
231 */
232 protected void configure(JSONObject config)
233 {
234 }
235
236 /**
237 * Generates the markup response that will be returned to the client; this should be an <ul> element with
238 * nested <li> elements. Subclasses may override this to produce more involved markup (including images and
239 * CSS class attributes).
240 *
241 * @param writer
242 * to write the list to
243 * @param matches
244 * list of matching objects, each should be converted to a string
245 */
246 protected void generateResponseMarkup(MarkupWriter writer, List matches)
247 {
248 writer.element("ul");
249
250 for (Object o : matches)
251 {
252 writer.element("li");
253 writer.write(o.toString());
254 writer.end();
255 }
256
257 writer.end(); // ul
258 }
259 }