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