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 }