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