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 &lt;div&gt; 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 &lt;div&gt; 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 &lt;ul&gt; element with
238         * nested &lt;li&gt; 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    }