/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.click.extras.control;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import org.apache.click.Context;
import org.apache.click.Page;
import org.apache.click.control.TextField;
import org.apache.click.element.CssImport;
import org.apache.click.element.JsImport;
import org.apache.click.element.JsScript;
import org.apache.click.util.ClickUtils;
import org.apache.click.util.HtmlStringBuffer;
/**
* Provides an Auto Complete Text Field control: <input type='text'>.
*
* <table class='htmlHeader' cellspacing='6'>
* <tr>
* <td>Text Field</td>
* <td><input type='text' value='string' title='AutoCompleteTextField Control'/></td>
* </tr>
* </table>
*
* <h3>AutoCompleteTextField Example</h3>
*
* The example below shows how to a create an AutoCompleteTextField. Note how
* the abstract method <tt>getAutoCompleteList()</tt> is implemented to provide
* the list of suggested values.
*
* <pre class="codeJava">
* AutoCompleteTextField nameField = <span class="kw">new</span> AutoCompleteTextField(<span class="st">"name"</span>) {
* public List getAutoCompleteList(String criteria) {
* <span class="kw">return</span> getCustomerService().getCustomerNamesLike(criteria);
* }
* };
* form.add(nameField); </pre>
*
* <a name="resources"></a>
* <h3>CSS and JavaScript resources</h3>
*
* AutoCompleteTextField depends on the <a class="external" target="_blank" href="http://www.prototypejs.org">Prototype</a>
* and <a class="external" target="_blank" href="http://script.aculo.us/">Scriptaculous/</a>
* JavaScript libraries to handle the auto-complete functionality. See
* <a class="external" target="_blank" href="http://wiki.github.com/madrobby/scriptaculous/ajax-autocompleter">Ajax.Autocompleter</a>
* for more details.
* <p/>
* The AutoCompleteTextField control makes use of the following resources
* (which Click automatically deploys to the application directory, <tt>/click</tt>):
*
* <ul>
* <li><tt>click/extras-control.css</tt></li>
* <li><tt>click/control.js</tt></li>
* <li><tt>click/prototype/prototype.js</tt></li>
* <li><tt>click/prototype/effects.js</tt></li>
* <li><tt>click/prototype/controls.js</tt></li>
* </ul>
*
* See also the W3C HTML reference:
* <a class="external" target="_blank" title="W3C HTML 4.01 Specification"
* href="http://www.w3.org/TR/html401/interact/forms.html#h-17.4">INPUT</a>
*/
public abstract class AutoCompleteTextField extends TextField {
// -------------------------------------------------------------- Constants
private static final long serialVersionUID = 1L;
// ----------------------------------------------------- Instance Variables
/**
* The JavaScript 'script.aculo.us' Autocompleter initialization options,
* default value is: <tt>{minChars:1}</tt>.
*/
protected String autoCompleteOptions = "{minChars:1}";
// ----------------------------------------------------------- Constructors
/**
* Construct the AutoCompleteTextField with the given name. The default text field size
* is 20 characters.
*
* @param name the name of the field
*/
public AutoCompleteTextField(String name) {
super(name);
}
/**
* Construct the AutoCompleteTextField with the given name and required status.
* The default text field size is 20 characters.
*
* @param name the name of the field
* @param required the field required status
*/
public AutoCompleteTextField(String name, boolean required) {
super(name);
setRequired(required);
}
/**
* Construct the AutoCompleteTextField with the given name and label. The default text
* field size is 20 characters.
*
* @param name the name of the field
* @param label the label of the field
*/
public AutoCompleteTextField(String name, String label) {
super(name, label);
}
/**
* Construct the AutoCompleteTextField with the given name, label and required status.
* The default text field size is 20 characters.
*
* @param name the name of the field
* @param label the label of the field
* @param required the field required status
*/
public AutoCompleteTextField(String name, String label, boolean required) {
super(name, label);
setRequired(required);
}
/**
* Construct the AutoCompleteTextField with the given name, label and size.
*
* @param name the name of the field
* @param label the label of the field
* @param size the size of the field
*/
public AutoCompleteTextField(String name, String label, int size) {
super(name, label);
setSize(size);
}
/**
* Create a AutoCompleteTextField with no name defined.
* <p/>
* <b>Please note</b> the control's name must be defined before it is valid.
*/
public AutoCompleteTextField() {
}
// ------------------------------------------------------- Abstract Methods
/**
* Return the list of suggested values for the given search criteria.
*
* @param criteria the search criteria
* @return the list of suggested values for the given search criteria
*/
abstract public List getAutoCompleteList(String criteria);
// --------------------------------------------------------- Public Methods
/**
* Return the JavaScript 'script.aculo.us' Autocompleter initialization
* options, default value is: <tt>{}</tt>.
*
* @return the JavaScript Autocompleter initialization options
*/
public String getAutoCompleteOptions() {
return autoCompleteOptions;
}
/**
* Set the JavaScript 'script.aculo.us' Autocompleter initialization
* options, default value is: <tt>{minChars:1}</tt>.
* <p/>
* Scriptaculous AutoCompleter supports sending arbitrary request parameters
* as part of its options. See the Ajax-AutoCompleter
* <a href="http://github.com/madrobby/scriptaculous/wikis/ajax-autocompleter" target="_blank">documentation</a>
* for some examples.
* <p/>
* Below is an example of how to send extra request parameters:
* <pre class="prettyprint">
* public void onInit() {
* AutoCompleteTextField cityField = new AutoCompleteTextField("cityField");
* HtmlStringBuffer buffer = new HtmlStringBuffer();
* buffer.append("{"); // Options opens with squiggly bracket
* buffer.append(stateField.getName());
* buffer.append("=");
* buffer.append(stateField.getValue());
* buffer.append("&");
* buffer.append(idField.getName());
* buffer.append("=");
* buffer.append(idField.getValue());
* buffer.append("}"); // Options closes with squiggly bracket
* field.setAutoCompleteOptions(options.toString());
* } </pre>
*
* Note that you can add any of the options specified on the
* Ajax-AutoCompleter wiki.
*
* @param options the JavaScript Autocompleter initialization options
*/
public void setAutoCompleteOptions(String options) {
this.autoCompleteOptions = options;
}
/**
* @see org.apache.click.control.Field#setParent(Object)
*
* @param parent the parent of the Control
* @throws IllegalStateException if {@link #name} is not defined
* @throws IllegalArgumentException if the given parent instance is
* referencing <tt>this</tt> object: <tt>if (parent == this)</tt>
*/
public void setParent(Object parent) {
if (parent == null) {
// If the field parent control is set to null (indicating the field
// is being removed), also remove the field from its parent page
Page page = getPage();
if (page != null) {
page.getControls().remove(this);
page.getModel().remove(getName());
}
}
super.setParent(parent);
}
/**
* Return the list of HEAD {@link org.apache.click.element.Element elements}
* (resources) to be included in the page. The resources are:
* <p/>
* <ul>
* <li>/click/extras-control.css</li>
* <li>/click/control.js</li>
* <li>/click/prototype/prototype.js</li>
* <li>/click/prototype/effects.js</li>
* <li>/click/prototype/controls.js</li>
* </ul>
*
* @see org.apache.click.Control#getHeadElements()
*
* @return the list of HEAD elements to be included in the page
* @throws IllegalStateException if the field's name has not been set
* @throws IllegalStateException if the field is not attached to the Page
*/
public List getHeadElements() {
// Check that the field name and parent Page has been set
String fieldName = getName();
if (fieldName == null) {
throw new IllegalStateException("AutoCompleteTextField name"
+ " is not defined. Set the name before calling"
+ " getHeadElements().");
}
Page page = getPage();
if (page == null) {
throw new IllegalStateException("The AutoCompleteTextField, '"
+ fieldName + "', is not attached to the Page. Add"
+ " AutoCompleteTextField to a parent form or container and"
+ " attach the parent to the Page before calling"
+ " getHeadElements().");
}
Context context = getContext();
if (headElements == null) {
headElements = super.getHeadElements();
String versionIndicator = ClickUtils.getResourceVersionIndicator(context);
headElements.add(new CssImport("/click/extras-control.css",
versionIndicator));
headElements.add(new JsImport("/click/control.js", versionIndicator));
headElements.add(new JsImport("/click/prototype/prototype.js",
versionIndicator));
headElements.add(new JsImport("/click/prototype/effects.js",
versionIndicator));
headElements.add(new JsImport("/click/prototype/controls.js",
versionIndicator));
}
// Note the addLoadEvent script is recreated and checked if it
// is contained in the headElement.
String fieldId = getId();
JsScript script = new JsScript();
script.setId(fieldId + "_autocomplete");
if (!headElements.contains(script)) {
// Script must be executed as soon as browser dom is ready
script.setExecuteOnDomReady(true);
String contextPath = context.getRequest().getContextPath();
HtmlStringBuffer buffer = new HtmlStringBuffer(150);
buffer.append("new Ajax.Autocompleter(");
buffer.append("'").append(fieldId).append("'");
buffer.append(",'").append(fieldId).append("_auto_complete_div'");
buffer.append(",'").append(contextPath).append(page.getPath()).append(
"'");
buffer.append(",").append(getAutoCompleteOptions()).append(");");
script.setContent(buffer.toString());
headElements.add(script);
}
return headElements;
}
/**
* Render the HTML representation of the AutoCompleteTextField.
*
* @see #toString()
*
* @param buffer the specified buffer to render the control's output to
*/
public void render(HtmlStringBuffer buffer) {
super.render(buffer);
buffer.elementStart("div");
buffer.appendAttribute("class", "auto_complete");
buffer.appendAttribute("id", getId() + "_auto_complete_div");
buffer.closeTag();
buffer.elementEnd("div");
}
// --------------------------------------------------------- Event Handlers
/**
* Register the field with the parent page to intercept POST autocompletion
* requests.
*
* @see org.apache.click.Control#onInit()
*/
public void onInit() {
super.onInit();
Page page = getPage();
if (page == null) {
// If parent page is not reachable, exit early
return;
}
// See whether control has been registered at Page level.
Object control = page.getModel().get(getName());
// If not registered, then register control
if (control == null) {
// Ensure current parent control does not change
Object parent = getParent();
page.addControl(this);
setParent(parent);
} else if (!(control instanceof AutoCompleteTextField)) {
String message =
"Non AutoCompleteTextField object '"
+ control.getClass().toString()
+ "' already registered in Page as: "
+ getName();
throw new IllegalStateException(message);
}
}
/**
* Process the page request and if an auto completion POST request then
* render an list of suggested values.
*
* @see org.apache.click.Control#onProcess()
*
* @return false if an auto complete request, otherwise returns true
*/
public boolean onProcess() {
Context context = getContext();
if (context.isPost()) {
// If an auto complete POST request then render suggested list,
// otherwise continue as normal
if (getForm().isFormSubmission()) {
return super.onProcess();
} else if (context.isAjaxRequest()) {
String criteria = context.getRequestParameter(getName());
if (criteria != null) {
List autoCompleteList = getAutoCompleteList(criteria);
renderAutoCompleteList(autoCompleteList);
return false;
}
}
}
return true;
}
// ------------------------------------------------------ Protected Methods
/**
* Render the suggested auto completion list to the servlet response.
*
* @param autoCompleteList the suggested list of auto completion values
*/
protected void renderAutoCompleteList(List autoCompleteList) {
HtmlStringBuffer buffer = new HtmlStringBuffer(10 + (autoCompleteList.size() * 20));
buffer.append("<ul>");
for (int i = 0; i < autoCompleteList.size(); i++) {
String value = autoCompleteList.get(i).toString();
buffer.append("<li>");
buffer.appendEscaped(value);
buffer.append("</li>");
}
buffer.append("</ul>");
HttpServletResponse response = getContext().getResponse();
response.setContentType(getPage().getContentType());
try {
PrintWriter writer = response.getWriter();
writer.print(buffer.toString());
writer.flush();
writer.close();
getPage().setPath(null);
} catch (IOException ioe) {
throw new RuntimeException(ioe);
}
}
}