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