    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}