    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}