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