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