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.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.9.3. 033 * <p/> 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 * <p/> 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 @Inject 093 private DeprecationWarning deprecationWarning; 094 095 void pageLoaded() 096 { 097 deprecationWarning.ignoredComponentParameters(resources, "frequency", "tokens"); 098 } 099 100 void beginRender(MarkupWriter writer) 101 { 102 writer.attributes("autocomplete", "off"); 103 } 104 105 @Import(stylesheet="Autocomplete.css") 106 void afterRender() 107 { 108 Link link = resources.createEventLink(EVENT_NAME); 109 110 JSONObject spec = new JSONObject("id", field.getClientId(), 111 "url", link.toString()).put("minChars", minChars); 112 113 jsSupport.require("t5/core/autocomplete").with(spec); 114 } 115 116 Object onAutocomplete(@RequestParameter("t:input") 117 String input) 118 { 119 final Holder<List> matchesHolder = Holder.create(); 120 121 // Default it to an empty list. 122 123 matchesHolder.put(Collections.emptyList()); 124 125 ComponentEventCallback callback = new ComponentEventCallback() 126 { 127 public boolean handleResult(Object result) 128 { 129 List matches = coercer.coerce(result, List.class); 130 131 matchesHolder.put(matches); 132 133 return true; 134 } 135 }; 136 137 resources.triggerEvent(EventConstants.PROVIDE_COMPLETIONS, new Object[] 138 {input}, callback); 139 140 JSONObject reply = new JSONObject(); 141 142 reply.put("matches", JSONArray.from(matchesHolder.get())); 143 144 // A JSONObject response is always preferred, as that triggers the whole partial page render pipeline. 145 return reply; 146 } 147}