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