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