    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}