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