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