blob: 03a96da3e7ab46f586a2c5a6710e5c4c635be7ae [file] [log] [blame]
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package org.apache.tapestry5.corelib.mixins;
import org.apache.tapestry5.*;
import org.apache.tapestry5.annotations.*;
import org.apache.tapestry5.internal.util.Holder;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.ioc.services.TypeCoercer;
import org.apache.tapestry5.json.JSONArray;
import org.apache.tapestry5.json.JSONObject;
import org.apache.tapestry5.services.compatibility.DeprecationWarning;
import org.apache.tapestry5.services.javascript.JavaScriptSupport;
import java.util.Collections;
import java.util.List;
/**
* A mixin for a text field that allows for autocompletion of text fields. This is based on
* Twttter <a href="http://twitter.github.io/typeahead.js/">typeahead.js</a> version 0.10.5.
* <p/>
* The container is responsible for providing an event handler for event "providecompletions". The context will be the
* partial input string sent from the client. The return value should be an array or list of completions, in
* presentation order. e.g.
* <p/>
* <pre>
* String[] onProvideCompletionsFromMyField(String input)
* {
* return . . .;
* }
* </pre>
*
* @tapestrydoc
*/
@Events(EventConstants.PROVIDE_COMPLETIONS)
@MixinAfter
public class Autocomplete
{
static final String EVENT_NAME = "autocomplete";
/**
* The field component to which this mixin is attached.
*/
@InjectContainer
private Field field;
@Inject
private ComponentResources resources;
@Environmental
private JavaScriptSupport jsSupport;
@Inject
private TypeCoercer coercer;
/**
* Overwrites the default minimum characters to trigger a server round trip (the default is 1).
*/
@Parameter(defaultPrefix = BindingConstants.LITERAL)
private int minChars = 1;
/**
* Overrides the default check frequency for determining whether to send a server request. The default is .4
* seconds.
*
* @deprecated Deprecated in 5.4 with no replacement.
*/
@Parameter(defaultPrefix = BindingConstants.LITERAL)
private double frequency;
/**
* If given, then the autocompleter will support multiple input values, seperated by any of the individual
* characters in the string.
*
* @deprecated Deprecated in 5.4 with no replacement.
*/
@Parameter(defaultPrefix = BindingConstants.LITERAL)
private String tokens;
/**
* Maximum number of suggestions shown in the UI. It maps to Typeahead's "limit" option. Default value: 5.
*/
@Parameter("5")
private int maxSuggestions;
/**
* The context for the "providecompletions" event.
* This list of values will be converted into strings and included in
* the URI. The strings will be coerced back to whatever their values are and made available to event handler
* methods. The first parameter of the context passed to "providecompletions" event handlers will
* still be the partial string typed by the user, so the context passed through this parameter
* will be added from the second position on.
*
* @since 5.4
*/
@Parameter
private Object[] context;
@Inject
private DeprecationWarning deprecationWarning;
void pageLoaded()
{
deprecationWarning.ignoredComponentParameters(resources, "frequency", "tokens");
}
void beginRender(MarkupWriter writer)
{
writer.attributes("autocomplete", "off");
}
@Import(stylesheet="typeahead-bootstrap3.css")
void afterRender()
{
Link link = resources.createEventLink(EVENT_NAME, context);
JSONObject spec = new JSONObject("id", field.getClientId(),
"url", link.toString()).put("minChars", minChars).put("limit", maxSuggestions);
jsSupport.require("t5/core/autocomplete").with(spec);
}
Object onAutocomplete(List<String> context, @RequestParameter("t:input")
String input)
{
final Holder<List> matchesHolder = Holder.create();
// Default it to an empty list.
matchesHolder.put(Collections.emptyList());
ComponentEventCallback callback = new ComponentEventCallback()
{
public boolean handleResult(Object result)
{
List matches = coercer.coerce(result, List.class);
matchesHolder.put(matches);
return true;
}
};
Object[] newContext;
if (context.size() == 0) {
newContext = new Object[] {input};
}
else {
newContext = new Object[context.size() + 1];
newContext[0] = input;
for (int i = 1; i < newContext.length; i++) {
newContext[i] = context.get(i - 1);
}
}
resources.triggerEvent(EventConstants.PROVIDE_COMPLETIONS, newContext, callback);
JSONObject reply = new JSONObject();
reply.put("matches", JSONArray.from(matchesHolder.get()));
// A JSONObject response is always preferred, as that triggers the whole partial page render pipeline.
return reply;
}
}