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