001    // Copyright 2007, 2008 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.tapestry.corelib.mixins;
016    
017    import org.apache.tapestry.*;
018    import org.apache.tapestry.ContentType;
019    import org.apache.tapestry.annotations.*;
020    import org.apache.tapestry.internal.services.ResponseRenderer;
021    import org.apache.tapestry.internal.util.Holder;
022    import org.apache.tapestry.ioc.annotations.Inject;
023    import org.apache.tapestry.ioc.services.TypeCoercer;
024    import org.apache.tapestry.json.JSONObject;
025    import org.apache.tapestry.services.MarkupWriterFactory;
026    import org.apache.tapestry.services.Request;
027    import org.apache.tapestry.util.TextStreamResponse;
028    
029    import java.util.Collections;
030    import java.util.List;
031    
032    /**
033     * A mixin for a text field that allows for autocompletion of text fields. This is based on Prototype's autocompleter
034     * control.
035     * <p/>
036     * The mixin renders an (initially invisible) progress indicator after the field (it will also be after the error icon
037     * most fields render). The progress indicator is made visible during the request to the server. The mixin then renders
038     * a &lt;div&gt; that will be filled in on the client side with dynamically obtained selections.
039     * <p/>
040     * Multiple selection on the client is enabled by binding the tokens parameter (however, the mixin doesn't help split
041     * multiple selections up on the server, that is still your code's responsibility).
042     * <p/>
043     * The container is responsible for providing an event handler for event "providecompletions".  The context will be the
044     * partial input string sent from the client.  The return value should be an array or list of completions, in
045     * presentation order.  I.e.
046     * <p/>
047     * <pre>
048     * String[] onProvideCompletionsFromMyField(String input)
049     * {
050     *   return . . .;
051     * }
052     * </pre>
053     */
054    @IncludeJavaScriptLibrary("${tapestry.scriptaculous}/controls.js")
055    public class Autocomplete
056    {
057        static final String EVENT_NAME = "autocomplete";
058    
059        private static final String PARAM_NAME = "t:input";
060    
061        /**
062         * The field component to which this mixin is attached.
063         */
064        @InjectContainer
065        private Field _field;
066    
067        @Inject
068        private ComponentResources _resources;
069    
070        @Environmental
071        private PageRenderSupport _pageRenderSupport;
072    
073        @Inject
074        private Request _request;
075    
076        @Inject
077        private TypeCoercer _coercer;
078    
079        @Inject
080        private MarkupWriterFactory _factory;
081    
082        @Inject
083        @Path("classpath:org/apache/tapestry/ajax-loader.gif")
084        private Asset _loader;
085    
086        /**
087         * Overwrites the default minimum characters to trigger a server round trip (the default is 1).
088         */
089        @Parameter(defaultPrefix = "literal")
090        private int _minChars;
091    
092        @Inject
093        private ResponseRenderer _responseRenderer;
094    
095    
096        /**
097         * Overrides the default check frequency for determining whether to send a server request. The default is .4
098         * seconds.
099         */
100        @Parameter(defaultPrefix = "literal")
101        private double _frequency;
102    
103        /**
104         * If given, then the autocompleter will support multiple input values, seperated by any of the individual
105         * characters in the string.
106         */
107        @Parameter(defaultPrefix = "literal")
108        private String _tokens;
109    
110        /**
111         * Mixin afterRender phrase occurs after the component itself. This is where we write the &lt;div&gt; element and
112         * the JavaScript.
113         *
114         * @param writer
115         */
116        void afterRender(MarkupWriter writer)
117        {
118            String id = _field.getClientId();
119    
120            String menuId = id + ":menu";
121            String loaderId = id + ":loader";
122    
123            // This image is made visible while the request is being processed.
124            // To be honest, I think Prototype hides it too soon, it should wait
125            // until the <div> is fully positioned and updated.
126    
127            writer.element("img",
128    
129                           "src", _loader.toClientURL(),
130    
131                           "class", TapestryConstants.INVISIBLE_CLASS,
132    
133                           "id", loaderId);
134            writer.end();
135    
136            writer.element("div",
137    
138                           "id", menuId,
139    
140                           "class", "t-autocomplete-menu");
141            writer.end();
142    
143            Link link = _resources.createActionLink(EVENT_NAME, false);
144    
145    
146            JSONObject config = new JSONObject();
147            config.put("paramName", PARAM_NAME);
148            config.put("indicator", loaderId);
149    
150            if (_resources.isBound("minChars")) config.put("minChars", _minChars);
151    
152            if (_resources.isBound("frequency")) config.put("frequency", _frequency);
153    
154            if (_resources.isBound("tokens"))
155            {
156                for (int i = 0; i < _tokens.length(); i++)
157                {
158                    config.accumulate("tokens", _tokens.substring(i, i + 1));
159                }
160            }
161    
162            // Let subclasses do more.
163            configure(config);
164    
165            _pageRenderSupport.addScript("new Ajax.Autocompleter('%s', '%s', '%s', %s);", id, menuId, link.toAbsoluteURI(),
166                                         config);
167        }
168    
169        Object onAutocomplete()
170        {
171            String input = _request.getParameter(PARAM_NAME);
172    
173            final Holder<List> matchesHolder = Holder.create();
174    
175            // Default it to an empty list.
176    
177            matchesHolder.put(Collections.emptyList());
178    
179            ComponentEventCallback callback = new ComponentEventCallback()
180            {
181                public boolean handleResult(Object result)
182                {
183                    List matches = _coercer.coerce(result, List.class);
184    
185                    matchesHolder.put(matches);
186    
187                    return true;
188                }
189            };
190    
191            _resources.triggerEvent("providecompletions", new Object[] { input }, callback);
192    
193            ContentType contentType = _responseRenderer.findContentType(this);
194    
195            MarkupWriter writer = _factory.newMarkupWriter(contentType);
196    
197            generateResponseMarkup(writer, matchesHolder.get());
198    
199            return new TextStreamResponse(contentType.getMimeType(), writer.toString());
200        }
201    
202        /**
203         * Invoked to allow subclasses to further configure the parameters passed to the JavaScript Ajax.Autocompleter
204         * options. The values minChars, frequency and tokens my be pre-configured. Subclasses may override this method to
205         * configure additional features of the Ajax.Autocompleter.
206         * <p/>
207         * <p/>
208         * This implementation does nothing.
209         *
210         * @param config parameters object
211         */
212        protected void configure(JSONObject config)
213        {
214        }
215    
216        /**
217         * Generates the markup response that will be returned to the client; this should be an &lt;ul&gt; element with
218         * nested &lt;li&gt; elements. Subclasses may override this to produce more involved markup (including images and
219         * CSS class attributes).
220         *
221         * @param writer  to write the list to
222         * @param matches list of matching objects, each should be converted to a string
223         */
224        protected void generateResponseMarkup(MarkupWriter writer, List matches)
225        {
226            writer.element("ul");
227    
228            for (Object o : matches)
229            {
230                writer.element("li");
231                writer.write(o.toString());
232                writer.end();
233            }
234    
235            writer.end(); // ul
236        }
237    }