001 package org.apache.tapestry.scriptaculous; 002 003 import java.text.ParseException; 004 import java.util.Arrays; 005 import java.util.HashMap; 006 import java.util.Iterator; 007 import java.util.List; 008 import java.util.Map; 009 010 import org.apache.hivemind.ApplicationRuntimeException; 011 import org.apache.hivemind.util.Defense; 012 import org.apache.tapestry.IActionListener; 013 import org.apache.tapestry.IDirect; 014 import org.apache.tapestry.IForm; 015 import org.apache.tapestry.IMarkupWriter; 016 import org.apache.tapestry.IRequestCycle; 017 import org.apache.tapestry.IScript; 018 import org.apache.tapestry.PageRenderSupport; 019 import org.apache.tapestry.TapestryUtils; 020 import org.apache.tapestry.coerce.ValueConverter; 021 import org.apache.tapestry.engine.DirectServiceParameter; 022 import org.apache.tapestry.engine.IEngineService; 023 import org.apache.tapestry.engine.ILink; 024 import org.apache.tapestry.form.AbstractFormComponent; 025 import org.apache.tapestry.form.TranslatedField; 026 import org.apache.tapestry.form.TranslatedFieldSupport; 027 import org.apache.tapestry.form.ValidatableFieldSupport; 028 import org.apache.tapestry.json.JSONObject; 029 import org.apache.tapestry.link.DirectLink; 030 import org.apache.tapestry.listener.ListenerInvoker; 031 import org.apache.tapestry.services.ResponseBuilder; 032 import org.apache.tapestry.util.SizeRestrictingIterator; 033 import org.apache.tapestry.valid.ValidatorException; 034 035 /** 036 * Implementation of the <a href="http://wiki.script.aculo.us/scriptaculous/show/Ajax.Autocompleter">Ajax.Autocompleter</a> in 037 * the form of a {@link org.apache.tapestry.form.TextField} like component with the additional ability to dynamically suggest 038 * values via XHR requests. 039 * 040 * <p> 041 * This component will use the html element tag name defined in your html template to include it to determine whether or not 042 * to render a TextArea or TextField style input element. For example, specifying a component definition such as: 043 * </p> 044 * 045 * <pre><input jwcid="@Suggest" value="literal:A default value" /></pre> 046 * 047 * <p> 048 * would render something looking like: 049 * </p> 050 * 051 * <pre><input type="text" name="suggest" id="suggest" autocomplete="off" value="literal:A default value" /></pre> 052 * 053 * <p>while a defintion of</p> 054 * 055 * <pre><textarea jwcid="@Suggest" value="literal:A default value" /></pre> 056 * 057 * <p>would render something like:</p> 058 * 059 * <pre> 060 * <textarea name="suggest" id="suggest" >A default value<textarea/> 061 * </pre> 062 * 063 */ 064 public abstract class Suggest extends AbstractFormComponent implements TranslatedField, IDirect { 065 066 /** 067 * Injected service used to invoke whatever listeners people have setup to handle 068 * changing value from this field. 069 * 070 * @return The invoker. 071 */ 072 public abstract ListenerInvoker getListenerInvoker(); 073 074 /** 075 * Injected response builder for doing specific XHR things. 076 * 077 * @return ResponseBuilder for this request. 078 */ 079 public abstract ResponseBuilder getResponse(); 080 081 /** 082 * Associated javascript template. 083 * 084 * @return The script template. 085 */ 086 public abstract IScript getScript(); 087 088 /** 089 * Used to convert form input values. 090 * 091 * @return The value converter to use. 092 */ 093 public abstract ValueConverter getValueConverter(); 094 095 /** 096 * Injected. 097 * 098 * @return Service used to validate input. 099 */ 100 public abstract ValidatableFieldSupport getValidatableFieldSupport(); 101 102 /** 103 * Injected. 104 * 105 * @return Translation service. 106 */ 107 public abstract TranslatedFieldSupport getTranslatedFieldSupport(); 108 109 /** 110 * Injected. 111 * 112 * @return The {@link org.apache.tapestry.engine.DirectService} engine. 113 */ 114 public abstract IEngineService getEngineService(); 115 116 //////////////////////////////////////////////////////// 117 // Parameters 118 //////////////////////////////////////////////////////// 119 120 public abstract Object getValue(); 121 public abstract void setValue(Object value); 122 123 public abstract ListItemRenderer getListItemRenderer(); 124 public abstract void setListItemRenderer(ListItemRenderer renderer); 125 126 public abstract IActionListener getListener(); 127 128 public abstract Object getListSource(); 129 public abstract void setListSource(Object value); 130 131 public abstract int getMaxResults(); 132 133 public abstract Object getParameters(); 134 135 public abstract String getOptions(); 136 137 public abstract String getUpdateElementClass(); 138 139 /** 140 * Used internally to track listener invoked searches versus 141 * normal rendering requests. 142 * 143 * @return True if search was triggered, false otherwise. 144 */ 145 public abstract boolean isSearchTriggered(); 146 public abstract void setSearchTriggered(boolean value); 147 148 public boolean isRequired() 149 { 150 return getValidatableFieldSupport().isRequired(this); 151 } 152 153 protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle) 154 { 155 // render search triggered response instead of normal render if 156 // listener was invoked 157 158 IForm form = TapestryUtils.getForm(cycle, this); 159 setForm(form); 160 161 if (form.wasPrerendered(writer, this)) 162 return; 163 164 if (!form.isRewinding() && !cycle.isRewinding() 165 && getResponse().isDynamic() && isSearchTriggered()) 166 { 167 setName(form); 168 169 // do nothing if it wasn't for this instance - such as in a loop 170 171 if (cycle.getParameter(getClientId()) == null) 172 return; 173 174 renderList(writer, cycle); 175 return; 176 } 177 178 // defer to super if normal render 179 180 super.renderComponent(writer, cycle); 181 } 182 183 /** 184 * Invoked only when a search has been triggered to render out the <li> list of 185 * dynamic suggestion options. 186 * 187 * @param writer 188 * The markup writer. 189 * @param cycle 190 * The associated request. 191 */ 192 public void renderList(IMarkupWriter writer, IRequestCycle cycle) 193 { 194 Defense.notNull(getListSource(), "listSource for Suggest component."); 195 196 Iterator values = (Iterator)getValueConverter().coerceValue(getListSource(), Iterator.class); 197 198 if (isParameterBound("maxResults")) 199 { 200 values = new SizeRestrictingIterator(values, getMaxResults()); 201 } 202 203 getListItemRenderer().renderList(writer, cycle, values); 204 } 205 206 protected void renderFormComponent(IMarkupWriter writer, IRequestCycle cycle) 207 { 208 String value = getTranslatedFieldSupport().format(this, getValue()); 209 boolean isTextArea = getTemplateTagName().equalsIgnoreCase("textarea"); 210 211 renderDelegatePrefix(writer, cycle); 212 213 if (isTextArea) 214 writer.begin(getTemplateTagName()); 215 else 216 writer.beginEmpty(getTemplateTagName()); 217 218 // only render input attributes if not a textarea 219 if (!isTextArea) 220 { 221 writer.attribute("type", "text"); 222 writer.attribute("autocomplete", "off"); 223 } 224 225 renderIdAttribute(writer, cycle); 226 writer.attribute("name", getName()); 227 228 if (isDisabled()) 229 writer.attribute("disabled", "disabled"); 230 231 renderInformalParameters(writer, cycle); 232 renderDelegateAttributes(writer, cycle); 233 234 getTranslatedFieldSupport().renderContributions(this, writer, cycle); 235 getValidatableFieldSupport().renderContributions(this, writer, cycle); 236 237 if (value != null) 238 { 239 if (!isTextArea) 240 writer.attribute("value", value); 241 else 242 writer.print(value); 243 } 244 245 if (!isTextArea) 246 writer.closeTag(); 247 else 248 writer.end(); 249 250 renderDelegateSuffix(writer, cycle); 251 252 // render update element 253 254 writer.begin("div"); 255 writer.attribute("id", getClientId() + "choices"); 256 writer.attribute("class", getUpdateElementClass()); 257 writer.end(); 258 259 // render javascript 260 261 JSONObject json = null; 262 String options = getOptions(); 263 264 try { 265 266 json = options != null ? new JSONObject(options) : new JSONObject(); 267 268 } catch (ParseException ex) 269 { 270 throw new ApplicationRuntimeException(ScriptaculousMessages.invalidOptions(options, ex), this.getBinding("options").getLocation(), ex); 271 } 272 273 // bind onFailure client side function if not already defined 274 275 if (!json.has("onFailure")) 276 { 277 json.put("onFailure", "tapestry.error"); 278 } 279 280 if (!json.has("encoding")) 281 { 282 json.put("encoding", cycle.getEngine().getOutputEncoding()); 283 } 284 285 Map parms = new HashMap(); 286 parms.put("inputId", getClientId()); 287 parms.put("updateId", getClientId() + "choices"); 288 parms.put("options", json.toString()); 289 290 Object[] specifiedParams = DirectLink.constructServiceParameters(getParameters()); 291 Object[] listenerParams = null; 292 if (specifiedParams != null) 293 { 294 listenerParams = new Object[specifiedParams.length + 1]; 295 System.arraycopy(specifiedParams, 0, listenerParams, 1, specifiedParams.length); 296 } else { 297 298 listenerParams = new Object[1]; 299 } 300 301 listenerParams[0] = getClientId(); 302 303 ILink updateLink = getEngineService().getLink(isStateful(), new DirectServiceParameter(this, listenerParams)); 304 parms.put("updateUrl", updateLink.getURL()); 305 306 PageRenderSupport pageRenderSupport = TapestryUtils.getPageRenderSupport(cycle, this); 307 getScript().execute(this, cycle, pageRenderSupport, parms); 308 } 309 310 /** 311 * Rewinds the component, doing translation, validation and binding. 312 */ 313 protected void rewindFormComponent(IMarkupWriter writer, IRequestCycle cycle) 314 { 315 String value = cycle.getParameter(getName()); 316 try 317 { 318 Object object = getTranslatedFieldSupport().parse(this, value); 319 getValidatableFieldSupport().validate(this, writer, cycle, object); 320 321 setValue(object); 322 } catch (ValidatorException e) 323 { 324 getForm().getDelegate().recordFieldInputValue(value); 325 getForm().getDelegate().record(e); 326 } 327 } 328 329 /** 330 * Triggers the listener. The parameters passed are the current text 331 * and those specified in the parameters parameter of the component. 332 * If the listener parameter is not bound, attempt to locate an implicit 333 * listener named by the capitalized component id, prefixed by "do". 334 */ 335 public void trigger(IRequestCycle cycle) 336 { 337 IActionListener listener = getListener(); 338 if (listener == null) 339 listener = getContainer().getListeners().getImplicitListener(this); 340 341 Object[] params = cycle.getListenerParameters(); 342 343 // replace the first param with the correct value 344 String inputId = (String)params[0]; 345 params[0] = cycle.getParameter(inputId); 346 347 cycle.setListenerParameters(params); 348 349 setSearchTriggered(true); 350 351 getListenerInvoker().invokeListener(listener, this, cycle); 352 } 353 354 public List getUpdateComponents() 355 { 356 return Arrays.asList(new Object[] { getClientId() }); 357 } 358 359 public boolean isAsync() 360 { 361 return true; 362 } 363 364 public boolean isJson() 365 { 366 return false; 367 } 368 369 /** 370 * Sets the default {@link ListItemRenderer} for component, to be overriden as 371 * necessary by component parameters. 372 */ 373 protected void finishLoad() 374 { 375 setListItemRenderer(DefaultListItemRenderer.SHARED_INSTANCE); 376 } 377 }