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