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