001    /*
002     * Apache License
003     * Version 2.0, January 2004
004     * http://www.apache.org/licenses/
005     *
006     * Copyright 2008-2010 by chenillekit.org
007     *
008     * Licensed under the Apache License, Version 2.0 (the "License");
009     * you may not use this file except in compliance with the License.
010     * You may obtain a copy of the License at
011     *
012     * http://www.apache.org/licenses/LICENSE-2.0
013     */
014    
015    package org.chenillekit.tapestry.core.components.prototype_ui;
016    
017    import org.apache.commons.lang.StringUtils;
018    import org.apache.tapestry5.BindingConstants;
019    import org.apache.tapestry5.ComponentEventCallback;
020    import org.apache.tapestry5.ComponentResources;
021    import org.apache.tapestry5.FieldTranslator;
022    import org.apache.tapestry5.MarkupWriter;
023    import org.apache.tapestry5.ValidationException;
024    import org.apache.tapestry5.ValidationTracker;
025    import org.apache.tapestry5.annotations.Environmental;
026    import org.apache.tapestry5.annotations.Import;
027    import org.apache.tapestry5.annotations.Parameter;
028    import org.apache.tapestry5.corelib.base.AbstractField;
029    import org.apache.tapestry5.internal.util.Holder;
030    import org.apache.tapestry5.ioc.annotations.Inject;
031    import org.apache.tapestry5.ioc.services.PropertyAccess;
032    import org.apache.tapestry5.ioc.services.TypeCoercer;
033    import org.apache.tapestry5.json.JSONArray;
034    import org.apache.tapestry5.json.JSONObject;
035    import org.apache.tapestry5.services.Request;
036    import org.apache.tapestry5.services.ResponseRenderer;
037    import org.apache.tapestry5.services.javascript.JavaScriptSupport;
038    
039    import java.util.Collections;
040    import java.util.List;
041    
042    import static org.apache.tapestry5.ioc.internal.util.CollectionFactory.newList;
043    
044    /**
045     * This AutoComplete component based on <a href="http://www.prototype-ui.com/">Prototype-UI's</a>
046     * <a href="http://blog.xilinus.com/2008/2/22/new-component-auto_complete-in-prototype-ui">autocomplete</a> widget.
047     *
048     * @version $Id: AutoComplete.java 674 2010-07-29 12:47:25Z homburgs $
049     */
050    @Import(library = {"../../Chenillekit.js", "prototype-ui.js", "AutoComplete.js"},
051                    stylesheet = {"themes/auto_complete/default.css", "themes/shadow/drop_shadow.css",
052                                    "themes/shadow/auto_complete.css"})
053    public class AutoComplete extends AbstractField
054    {
055            static final String EVENT_NAME = "autocomplete";
056    
057            private static final String PARAM_NAME = "search";
058    
059            /**
060             * The value to read or update.
061             */
062            @Parameter(required = true, allowNull = false)
063            private List<Object> selected;
064    
065            /**
066             * The object which will perform translation between server-side and client-side representations. If not specified,
067             * a value will usually be generated based on the type of the value parameter.
068             */
069            @Parameter(required = true, allowNull = false, defaultPrefix = BindingConstants.TRANSLATE)
070            private FieldTranslator<Object> translate;
071    
072            /**
073             * this parameter contains the name of the object property, that should display to user in the item list and the
074             * box of selected items.
075             */
076            @Parameter(required = true, allowNull = false, defaultPrefix = BindingConstants.LITERAL, name = "label")
077            private String labelPropertyName;
078    
079            @Inject
080            private Request request;
081    
082            @Inject
083            private ResponseRenderer responseRenderer;
084    
085            @Inject
086            private TypeCoercer coercer;
087    
088            @Inject
089            private ComponentResources resources;
090    
091            @Environmental
092            private JavaScriptSupport javascriptSupport;
093    
094            @Inject
095            private PropertyAccess propertyAccess;
096    
097            @Environmental
098            private ValidationTracker tracker;
099    
100            void beginRender(MarkupWriter writer)
101            {
102                    writer.element("input",
103                                               "type", "hidden",
104                                               "name", getControlName(),
105                                               "id", getClientId() + "-internal");
106                    writer.end();
107    
108                    writer.element("input",
109                                               "type", "text",
110                                               "id", getClientId());
111    
112            }
113    
114            void afterRender(MarkupWriter writer)
115            {
116                    writer.end();
117    
118                    JSONObject config = new JSONObject();
119                    config.put("url", resources.createEventLink(EVENT_NAME).toAbsoluteURI());
120                    config.put("preSelected", generateResponseMarkup(selected));
121    
122                    configure(config);
123    
124                    javascriptSupport.addScript("new Ck.AutoComplete('%s', %s);", getClientId(), config);
125            }
126    
127            /**
128             * Invoked to allow subclasses to further configure the parameters passed to this component's javascript
129             * options. Subclasses may override this method to configure additional features of this component.
130             * <p/>
131             * This implementation does nothing. For more information about window options look at
132             * this <a href="http://prototype-window.xilinus.com/documentation.html#initialize">page</a>.
133             *
134             * @param options windows option object
135             */
136            protected void configure(JSONObject options)
137            {
138    
139            }
140    
141            JSONArray onAutocomplete()
142            {
143                    String input = request.getParameter(PARAM_NAME);
144    
145                    final Holder<List> matchesHolder = Holder.create();
146    
147                    // Default it to an empty list.
148    
149                    matchesHolder.put(Collections.emptyList());
150    
151                    ComponentEventCallback callback = new ComponentEventCallback()
152                    {
153                            public boolean handleResult(Object result)
154                            {
155                                    List matches = coercer.coerce(result, List.class);
156    
157                                    matchesHolder.put(matches);
158    
159                                    return true;
160                            }
161                    };
162    
163                    resources.triggerEvent("providecompletions", new Object[]{input}, callback);
164    
165                    return generateResponseMarkup(matchesHolder.get());
166            }
167    
168            /**
169             * Method implemented by subclasses to actually do the work of processing the submission of the form. The element's
170             * elementName property will already have been set. This method is only invoked if the field is <strong>not {@link
171             * #isDisabled() disabled}</strong>.
172             *
173             * @param elementName the name of the element (used to find the correct parameter in the request)
174             */
175            protected void processSubmission(String elementName)
176            {
177                    String parameterValue = request.getParameter(elementName);
178                    String[] values = parameterValue.split(",");
179    
180                    // Use a couple of local variables to cut down on access via bindings
181                    List<Object> selected = this.selected;
182    
183                    if (selected == null) selected = newList();
184                    else selected.clear();
185    
186                    int count = values.length;
187                    try
188                    {
189                            for (int i = 0; i < count; i++)
190                            {
191                                    String value = StringUtils.trim(values[i]);
192    
193                                    Object objectValue = translate.parse(value);
194    
195                                    if (objectValue != null)
196                                            selected.add(objectValue);
197                            }
198                    }
199                    catch (ValidationException ex)
200                    {
201                            tracker.recordError(this, ex.getMessage());
202                    }
203    
204                    this.selected = selected;
205            }
206    
207            /**
208             * Generates the markup response that will be returned to the client; this should be an &lt;ul&gt; element with
209             * nested &lt;li&gt; elements. Subclasses may override this to produce more involved markup (including images and
210             * CSS class attributes).
211             *
212             * @param matches list of matching objects, each should be converted to a string
213             */
214            protected JSONArray generateResponseMarkup(List matches)
215            {
216                    JSONArray jsonObject = new JSONArray();
217                    for (Object o : matches)
218                    {
219                            Object value = translate.toClient(o);
220                            Object label = propertyAccess.get(o, labelPropertyName);
221                            JSONObject item = new JSONObject();
222                            item.put("text", label);
223                            item.put("value", value);
224                            jsonObject.put(item);
225                    }
226    
227                    return jsonObject;
228            }
229    }