001// Licensed under the Apache License, Version 2.0 (the "License");
002// you may not use this file except in compliance with the License.
003// You may obtain a copy of the License at
004//
005// http://www.apache.org/licenses/LICENSE-2.0
006//
007// Unless required by applicable law or agreed to in writing, software
008// distributed under the License is distributed on an "AS IS" BASIS,
009// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
010// See the License for the specific language governing permissions and
011// limitations under the License.
012
013package org.apache.tapestry5.corelib.mixins;
014
015import org.apache.tapestry5.*;
016import org.apache.tapestry5.annotations.*;
017import org.apache.tapestry5.commons.services.TypeCoercer;
018import org.apache.tapestry5.http.Link;
019import org.apache.tapestry5.internal.AbstractEventContext;
020import org.apache.tapestry5.internal.util.Holder;
021import org.apache.tapestry5.ioc.annotations.Inject;
022import org.apache.tapestry5.json.JSONArray;
023import org.apache.tapestry5.json.JSONObject;
024import org.apache.tapestry5.services.compatibility.DeprecationWarning;
025import org.apache.tapestry5.services.javascript.JavaScriptSupport;
026
027import java.util.Collections;
028import java.util.List;
029
030/**
031 * A mixin for a text field that allows for autocompletion of text fields. This is based on
032 * Twttter <a href="http://twitter.github.io/typeahead.js/">typeahead.js</a> version 0.10.5.
033 * 
034 * The container is responsible for providing an event handler for event "providecompletions". The context will be the
035 * partial input string sent from the client. The return value should be an array or list of completions, in
036 * presentation order. e.g.
037 * 
038 * <pre>
039 * String[] onProvideCompletionsFromMyField(String input)
040 * {
041 *   return . . .;
042 * }
043 * </pre>
044 *
045 * @tapestrydoc
046 */
047@Events(EventConstants.PROVIDE_COMPLETIONS)
048@MixinAfter
049public class Autocomplete
050{
051    static final String EVENT_NAME = "autocomplete";
052
053    /**
054     * The field component to which this mixin is attached.
055     */
056    @InjectContainer
057    private Field field;
058
059    @Inject
060    private ComponentResources resources;
061
062    @Environmental
063    private JavaScriptSupport jsSupport;
064
065    @Inject
066    private TypeCoercer coercer;
067
068    /**
069     * Overwrites the default minimum characters to trigger a server round trip (the default is 1).
070     */
071    @Parameter(defaultPrefix = BindingConstants.LITERAL)
072    private int minChars = 1;
073
074    /**
075     * Overrides the default check frequency for determining whether to send a server request. The default is .4
076     * seconds.
077     *
078     * @deprecated Deprecated in 5.4 with no replacement.
079     */
080    @Parameter(defaultPrefix = BindingConstants.LITERAL)
081    private double frequency;
082
083    /**
084     * If given, then the autocompleter will support multiple input values, seperated by any of the individual
085     * characters in the string.
086     *
087     * @deprecated Deprecated in 5.4 with no replacement.
088     */
089    @Parameter(defaultPrefix = BindingConstants.LITERAL)
090    private String tokens;
091    
092    /**
093     * Maximum number of suggestions shown in the UI. It maps to Typeahead's "limit" option. Default value: 5.
094     */
095    @Parameter("5")
096    private int maxSuggestions;
097    
098    /**
099     * The context for the "providecompletions" event. 
100     * This list of values will be converted into strings and included in
101     * the URI. The strings will be coerced back to whatever their values are and made available to event handler
102     * methods. The first parameter of the context passed to "providecompletions" event handlers will
103     * still be the partial string typed by the user, so the context passed through this parameter
104     * will be added from the second position on.
105     * 
106     * @since 5.4
107     */
108    @Parameter
109    private Object[] context;
110
111    @Inject
112    private DeprecationWarning deprecationWarning;
113
114    void pageLoaded()
115    {
116        deprecationWarning.ignoredComponentParameters(resources, "frequency", "tokens");
117    }
118
119    void beginRender(MarkupWriter writer)
120    {
121        writer.attributes("autocomplete", "off");
122    }
123
124    @Import(stylesheet="typeahead-bootstrap3.css")
125    void afterRender()
126    {
127        Link link = resources.createEventLink(EVENT_NAME, context);
128
129        JSONObject spec = new JSONObject("id", field.getClientId(),
130                "url", link.toString()).put("minChars", minChars).put("limit", maxSuggestions);
131
132        jsSupport.require("t5/core/autocomplete").with(spec);
133    }
134
135    Object onAutocomplete(final EventContext context, @RequestParameter("t:input")
136                          final String input)
137    {
138        final Holder<List> matchesHolder = Holder.create();
139
140        // Default it to an empty list.
141
142        matchesHolder.put(Collections.emptyList());
143
144        ComponentEventCallback callback = new ComponentEventCallback()
145        {
146            public boolean handleResult(Object result)
147            {
148                List matches = coercer.coerce(result, List.class);
149
150                matchesHolder.put(matches);
151
152                return true;
153            }
154        };
155
156        EventContext newContext = new AbstractEventContext() {
157
158          @Override
159          public int getCount() {
160            return context.getCount() + 1;
161          }
162
163          @Override
164          public <T> T get(Class<T> desiredType, int index) {
165            if (index == 0)
166            {
167                return coercer.coerce(input, desiredType);
168            }
169            return context.get(desiredType, index-1);
170          }
171        };
172
173        
174        resources.triggerContextEvent(EventConstants.PROVIDE_COMPLETIONS, newContext, callback);
175
176        JSONObject reply = new JSONObject();
177
178        reply.put("matches", JSONArray.from(matchesHolder.get()));
179
180        // A JSONObject response is always preferred, as that triggers the whole partial page render pipeline.
181        return reply;
182    }
183}