| // 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; |
| } |
| } |