// Copyright 2010 The Apache Software Foundation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gsoc.web.mixins;
import org.apache.tapestry5.Asset;
import org.apache.tapestry5.BindingConstants;
import org.apache.tapestry5.CSSClassConstants;
import org.apache.tapestry5.ComponentEventCallback;
import org.apache.tapestry5.ComponentResources;
import org.apache.tapestry5.ContentType;
import org.apache.tapestry5.EventConstants;
import org.apache.tapestry5.Link;
import org.apache.tapestry5.MarkupWriter;
import org.apache.tapestry5.OptionModel;
import org.apache.tapestry5.RenderSupport;
import org.apache.tapestry5.SelectModel;
import org.apache.tapestry5.annotations.Environmental;
import org.apache.tapestry5.annotations.Events;
import org.apache.tapestry5.annotations.Import;
import org.apache.tapestry5.annotations.InjectContainer;
import org.apache.tapestry5.annotations.Parameter;
import org.apache.tapestry5.annotations.Path;
import org.apache.tapestry5.annotations.RequestParameter;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.ioc.services.TypeCoercer;
import org.apache.tapestry5.json.JSONObject;
import org.apache.tapestry5.services.MarkupWriterFactory;
import org.apache.tapestry5.services.ResponseRenderer;
import org.apache.tapestry5.services.javascript.JavaScriptSupport;
import org.apache.tapestry5.util.TextStreamResponse;
import com.google.gsoc.web.components.AutoCompleteObject;
/**
* A mixin for a AutoCompleteObject that allows for autocompletion of text fields. This is based on Prototype's autocompleter
* control.
* <p/>
* The mixin renders an (initially invisible) progress indicator after the field (it will also be after the error icon
* most fields render). The progress indicator is made visible during the request to the server. The mixin then renders
* a <div> that will be filled in on the client side with dynamically obtained selections.
* <p/>
*
* The container is responsible for providing an event handler for event "providecompletions". The context will be the
* partial input string sent from the client. The return value must be a SelectModel with the possibles options
* <p/>
*
* <pre>
* SelectModel onProvideCompletionsFromMyField(String input)
* {
* return . . .;
* }
* </pre>
*/
/**
*
* @author Pablo Henrique dos Reis
*
*/
@Import(library = {"${tapestry.scriptaculous}/controls.js", "autocompleteobject.js"})
@Events(EventConstants.PROVIDE_COMPLETIONS)
public class MixinAutoCompleteObject
{
static final String PREFIX_COMPONENTE = "autocomplete:";
static final String EVENT_NAME = "autocomplete";
private static final String PARAM_NAME = "t:input";
/**
* The field component to which this mixin is attached.
*/
@InjectContainer
private AutoCompleteObject field;
@Inject
private ComponentResources resources;
@Inject
private JavaScriptSupport javaScriptSupport;
@Inject
private TypeCoercer coercer;
@Inject
private MarkupWriterFactory factory;
@Inject
@Path("${tapestry.spacer-image}")
private Asset spacerImage;
/**
* Overwrites the default minimum characters to trigger a server round trip (the default is 1).
*/
@Parameter(defaultPrefix = BindingConstants.LITERAL)
private int minChars;
@Inject
private ResponseRenderer responseRenderer;
/**
* Overrides the default check frequency for determining whether to send a server request. The default is .4
* seconds.
*/
@Parameter(defaultPrefix = BindingConstants.LITERAL)
private double frequency;
private SelectModel model;
/**
* Mixin afterRender phrase occurs after the component itself. This is where we write the <div> element and
* the JavaScript.
*
* @param writer
*/
void afterRender(MarkupWriter writer)
{
String id = field.getClientId();
String menuId = id + ":menu";
String loaderId = id + ":loader";
// The spacer image is used as a placeholder, allowing CSS to determine what image
// is actually displayed.
writer.element("img",
"src", spacerImage.toClientURL(),
"class", "t-autoloader-icon " + CSSClassConstants.INVISIBLE,
"alt", "",
"id", loaderId);
writer.end();
writer.element("div",
"id", menuId,
"class", "t-autocomplete-menu");
writer.end();
Link link = resources.createEventLink(EVENT_NAME);
JSONObject config = new JSONObject();
config.put("paramName", PARAM_NAME);
config.put("indicator", loaderId);
if (resources.isBound("minChars")) config.put("minChars", minChars);
if (resources.isBound("frequency")) config.put("frequency", frequency);
String methodAfterUpdate = "recordFields" + getIdField();
config.put("updateElement", methodAfterUpdate);
String configString = config.toString();
configString = configString.toString().replace("\""+methodAfterUpdate + "\"" ,methodAfterUpdate);
// Let subclasses do more.
configure(config);
javaScriptSupport.addScript(
"new Ajax.Autocompleter('%s', '%s', '%s', %s);", id, menuId,
link.toAbsoluteURI(), configString );
}
/**
* Return the id field with the first character uppercase
*
*/
protected String getIdField() {
String id = field.getClientId();
String initial = id.substring(0, 1);
return initial.toUpperCase() + id.substring(1);
}
/**
* Invoked to allow subclasses to further configure the parameters passed to the JavaScript Ajax.Autocompleter
* options. The values minChars and frequency my be pre-configured. Subclasses may override this method to
* configure additional features of the Ajax.Autocompleter.
* <p/>
* <p/>
* This implementation does nothing.
*
* @param config
* parameters object
*/
protected void configure(JSONObject config)
{
}
Object onAutocomplete(@RequestParameter(PARAM_NAME) String input)
{
ComponentEventCallback<SelectModel> callback = new ComponentEventCallback<SelectModel>()
{
public boolean handleResult(SelectModel result)
{
SelectModel matches = coercer.coerce(result, SelectModel.class);
model = matches;
return true;
}
};
resources.triggerEvent(EventConstants.PROVIDE_COMPLETIONS, new Object[] { input }, callback);
ContentType contentType = responseRenderer.findContentType(this);
MarkupWriter writer = factory.newPartialMarkupWriter(contentType);
generateResponseMarkup(writer, model);
return new TextStreamResponse(contentType.toString(), writer.toString());
}
/**
* Generates the markup response that will be returned to the client; this should be an <ul> element with
* nested <li> elements. Subclasses may override this to produce more involved markup (including images and
* CSS class attributes).
*
* @param writer
* to write the list to
* @param model
* to write each option
*/
protected void generateResponseMarkup(MarkupWriter writer, SelectModel model)
{
writer.element("ul");
if(model != null)
{
for (OptionModel o : model.getOptions())
{
writer.element("li",
"id", toClient(o.getValue()));
writer.write(o.getLabel());
writer.end();
}
}
writer.end(); // ul
}
public String toClient(Object object)
{
return field.toClient(object);
}
}