CompletionItems.java:
package de.aaworks.SimpleTest.client;
public interface CompletionItems {
/**
* Returns an array of all completion items matching
* @param match The user-entered text all compleition items have to
match
* @return Array of strings
*/
public String[] getCompletionItems(String match);
}
package de.aaworks.SimpleTest.client;
import com.google.gwt.user.client.ui.ChangeListener;
import com.google.gwt.user.client.ui.ClickListener;
import com.google.gwt.user.client.ui.KeyboardListener;
import com.google.gwt.user.client.ui.ListBox;
import com.google.gwt.user.client.ui.PopupPanel;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.TextBox;
import com.google.gwt.user.client.ui.Widget;
public class AutoCompleteTextBox extends TextBox implements
KeyboardListener,
ChangeListener, ClickListener {
protected PopupPanel choicesPopup = new PopupPanel(true);
protected ListBox choices = new ListBox();
protected CompletionItems items = new SimpleAutoCompletionItems(new
String[]{});
protected boolean popupAdded = false;
/**
* Default Constructor
*
*/
public AutoCompleteTextBox()
{
super();
this.addKeyboardListener(this);
choices.addChangeListener(this);
choices.addClickListener(this);
choicesPopup.add(choices);
}
/**
* Sets an "algorithm" returning completion items
* You can define your own way how the textbox retrieves
autocompletion items
* by implementing the CompletionItems interface and setting the
according object
* @see SimpleAutoCompletionItem
* @param items CompletionItem implementation
*/
public void setCompletionItems(CompletionItems items)
{
this.items = items;
}
/**
* Returns the used CompletionItems object
* @return CompletionItems implementation
*/
public CompletionItems getCompletionItems()
{
return this.items;
}
/**
* Not used at all
*/
public void onKeyDown(Widget arg0, char arg1, int arg2) {
}
/**
* Not used at all
*/
public void onKeyPress(Widget arg0, char arg1, int arg2) {
}
/**
* A key was released, start autocompletion
*/
public void onKeyUp(Widget arg0, char arg1, int arg2) {
if(arg1 == KEY_DOWN)
{
int selectedIndex = choices.getSelectedIndex();
selectedIndex++;
if(selectedIndex > choices.getItemCount())
{
selectedIndex = 0;
}
choices.setSelectedIndex(selectedIndex);
return;
}
if(arg1 == KEY_UP)
{
int selectedIndex = choices.getSelectedIndex();
selectedIndex--;
if(selectedIndex < 0)
{
selectedIndex = choices.getItemCount();
}
choices.setSelectedIndex(selectedIndex);
return;
}
if(arg1 == KEY_ENTER)
{
complete();
}
if(arg1 == KEY_ESCAPE)
{
choices.clear();
choicesPopup.hide();
}
String text = this.getText();
String[] matches = new String[]{};
if(text.length() > 0)
{
matches = items.getCompletionItems(text);
}
if(matches.length > 0)
{
choices.clear();
for(int i = 0; i < matches.length; i++)
{
choices.addItem((String) matches[i]);
}
// if there is only one match and it is what is in the
// text field anyways there is no need to show autocompletion
if(matches.length == 1 && matches[0].compareTo(text) == 0)
{
choicesPopup.hide();
} else {
choices.setSelectedIndex(0);
choices.setVisibleItemCount(matches.length + 1);
if(!popupAdded)
{
RootPanel.get().add(choicesPopup);
popupAdded = true;
}
choicesPopup.show();
choicesPopup.setPopupPosition(this.getAbsoluteLeft(),
this.getAbsoluteTop() + this.getOffsetHeight());
//choicesPopup.setWidth(this.getOffsetWidth() + "px");
choices.setWidth(this.getOffsetWidth() + "px");
}
} else {
choicesPopup.hide();
}
}
/**
* A mouseclick in the list of items
*/
public void onChange(Widget arg0) {
complete();
}
public void onClick(Widget arg0) {
complete();
}
// add selected item to textbox
protected void complete()
{
if(choices.getItemCount() > 0)
{
this.setText(choices.getItemText(choices.getSelectedIndex()));
}
choices.clear();
choicesPopup.hide();
}
}
package de.aaworks.SimpleTest.client;
import java.util.ArrayList;
public class SimpleAutoCompletionItems implements CompletionItems {
private String[] completions;
public SimpleAutoCompletionItems(String[] items)
{
completions = items;
}
public String[] getCompletionItems(String match) {
ArrayList matches = new ArrayList();
for (int i = 0; i < completions.length; i++) {
if (completions[i].toLowerCase().startsWith(match.toLowerCase())) {
matches.add(completions[i]);
}
}
String[] returnMatches = new String[matches.size()];
for(int i = 0; i < matches.size(); i++)
{
returnMatches[i] = (String)matches.get(i);
}
return returnMatches;
}
}
Finally, how to use it.
Wel, I've taylored it the way it is so everyone can implement the
Interface CompletionItems and get autocompletion items from wherever
they want (RPC calls and thelike). For simple autocompletion with
predefined items one can use the given sample implementation. Here's
some example:
final AutoCompleteTextBox box = new AutoCompleteTextBox();
box.setCompletionItems(new SimpleAutoCompletionItems(
new String[]{ "apple", "ape", "anything", "else"}));
Finally: the code is under public domain - feel free to use/change it
as you want. I'd be happy about hints on packaging and bugfixes. ;)
Hi Simp,
Cool. It works vey well.
Regards,
Mohan
Thank you, Oliver.
Hello, great peace of code, very interresting.
How do you make it RPC though ? Since it must use Asynchronous service,
I don't really know how to implement the interface.
I have so far, something looking like :
public class RemoteAutoCompletionItems implements CompletionItems {
private AutoCompleteBackEndServiceAsync service;
private ServiceDefTarget endpoint;
public RemoteAutoCompletionItems (String entryEndPoint) {
service = (AutoCompleteBackEndServiceAsync)
GWT.create(AutoCompleteBackEndService .class);
endpoint = (ServiceDefTarget) service;
endpoint.setServiceEntryPoint( entryEndPoint );
}
public String[] getCompletionItems(String match) {
AsyncCallback call = new AsyncCallback( ) {
public void onFailure(Throwable caught) {
// TODO Auto-generated method stub
}
public void onSuccess(Object result) {
}
};
service.getAutoComplete( match, call);
}
}
You currently are able to do something like that:
public class RemoteAutoCompletionItems implements CompletionItems {
private String[] items;
private MyServletAsync serv;
private AsyncCallback callback = new AsyncCallback() {
public void onSuccess(Object result) {
items = (String[]) result;
}
public void onFailure(Throwable caught) {
}
};
public RemoteAutoCompletionItems (String url) {
serv = (MyServletAsync) GWT.create(MyServlet.class);
ServiceDefTarget point = (ServiceDefTarget) serv;
point.setServiceEntryPoint(url);
}
public String[] getCompletionItems(String match) {
serv.getHTML2(2, callback);
if(items != null) return items;
return new String[0];
}
}
Obviously that's not perfect as it always lags behind what the user
entered.
The next version of the code actually attacks this problem by allowing
async updates of the list of items from the callback by having a
updateItems() method in the textbox callable by completionitems.
I'd love to hear input. :)
Olli
I have done something like you, I allow the completion items to update
the list of items with a method onMatch(), it was just a matter of
little refactoring.
Thanks
Olivier
/**
* A key was released, start autocompletion
*/
public void onKeyUp(Widget arg0, char arg1, int arg2) {
if (arg1 == KEY_DOWN) {
int selectedIndex = choices.getSelectedIndex();
selectedIndex++;
if (selectedIndex > choices.getItemCount()) {
selectedIndex = 0;
}
choices.setSelectedIndex(selectedIndex);
return;
}
if (arg1 == KEY_UP) {
int selectedIndex = choices.getSelectedIndex();
selectedIndex--;
if (selectedIndex < 0) {
selectedIndex = choices.getItemCount();
}
choices.setSelectedIndex(selectedIndex);
return;
}
if (arg1 == KEY_ENTER) {
if (visible) {
complete();
}
return;
}
if (arg1 == KEY_ESCAPE) {
choices.clear();
choicesPopup.hide();
visible = false;
return;
}
String text = this.getText();
matches = new String[] {};
if (text.length() > 0) {
items.getCompletionItems(text,this);
} else {
onMatch(text);
}
}
public void setMatches(String[] matches) {
this.matches = matches;
}
// use for Asynchronous Match
public void onMatch(String text) {
if (matches.length > 0) {
choices.clear();
for (int i = 0; i < matches.length; i++) {
choices.addItem((String) matches[i]);
}
// if there is only one match and it is what is in the
// text field anyways there is no need to show autocompletion
if (matches.length == 1 && matches[0].compareTo(text) == 0) {
choicesPopup.hide();
} else {
choices.setSelectedIndex(0);
choices.setVisibleItemCount(matches.length + 1);
if (!popupAdded) {
RootPanel.get().add(choicesPopup);
popupAdded = true;
}
choicesPopup.show();
visible = true;
choicesPopup.setPopupPosition(this.getAbsoluteLeft(), this
.getAbsoluteTop()
+ this.getOffsetHeight());
// choicesPopup.setWidth(this.getOffsetWidth() + "px");
choices.setWidth(this.getOffsetWidth() + "px");
}
} else {
visible = false;
choicesPopup.hide();
}
}
package com.gwt.components.client;
public interface MatchesRequiring {
public void onMatch(String text);
public void setMatches(String[] matches);
}
And the CompletionItems changes slightly, to be able to access the
widget :
/*Auto-Completion Textbox for GWTCopyright (C) 2006 Oliver Albers
http://gwt.components.googlepages.com/This library is free software;
you can redistribute it and/ormodify it under the terms of the GNU
Lesser General PublicLicense as published by the Free Software
Foundation; eitherversion 2.1 of the License, or (at your option) any
later version.This library is distributed in the hope that it will be
useful,but WITHOUT ANY WARRANTY; without even the implied warranty
ofMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNULesser General Public License for more details.You should have
received a copy of the GNU Lesser General PublicLicense along with this
library; if not, write to the Free SoftwareFoundation, Inc., 51
Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA*/package
com.gwt.components.client;
public interface CompletionItems {
/**
* Returns an array of all completion items matching
*
* @param match
* The user-entered text all compleition items have to
match
* @return Array of strings
*/
public void getCompletionItems(String match, MatchesRequiring
widget);
}
The code in RemoteAutoCompletion look like this :
public class RemoteAutoCompletionItems implements CompletionItems {
private AutoCompleteBackEndServiceAsync service;
private ServiceDefTarget endpoint;
public RemoteAutoCompletionItems (String entryEndPoint) {
service = (AutoCompleteBackEndServiceAsync)
GWT.create(AutoCompleteBackEndService .class);
endpoint = (ServiceDefTarget) service;
endpoint.setServiceEntryPoint( entryEndPoint );
}
public void getCompletionItems(final String match, final
MatchesRequiring widget) {
AsyncCallback call = new AsyncCallback( ) {
public void onFailure(Throwable caught) {
}
public void onSuccess(Object result) {
widget.setMatches( (String[]) result);
widget.onMatch( match );
}
};
service.getAutoComplete( match, call);
}
}
Olivier
an almost workaround is to comment out the automatic selection of the
first element of the list in object 'choices'. see:
// if there is only one match and it is what is in the
// text field anyways there is no need to show autocompletion
if(matches.length == 1 && matches[0].compareTo(text) == 0)
{
choicesPopup.hide();
} else {
//choices.setSelectedIndex(0);
choices.setVisibleItemCount(matches.length + 1);
I see only change events on that choices-popup, and there is no change
if you click on an already selected item !
adding a ClickListener did not solve the behavior. a proper fix would
be to use a MouseListener. hope that helps.
PS:
I also changed
choices.setVisibleItemCount(matches.length + 1);
to something like
choices.setVisibleItemCount(matches.length > MAX_ITEMS?
MAX_ITEMS:
matches.length);
cause I a have long lists.
and lots of thanx for the code so, it was very helpful for me !!
As some of you guys actually seem to use the code I set up a subversion
repository (publicle accessible) at http://www.aa-works.de/svn/
Also there is a Trac set up for bug reports etc:
http://www.aa-works.de/trac/
If somebody is interested I'd be interested in handing out write access
to the code. ;)
I've been using this control for a little while now, and overall it's
great. I just noticed something though: in Safari when you use the down
arrow, the control skips over one of the matches (eg: it goes down 2
rows instead of 1). I've tried it in IE & Firefox on the PC and Firefox
and Safari on the mac. This only seems to happen in Safari.
Any ideas what might be causing this behaviour?
Rusty
> This only seems to happen in Safari.
Too bad. I don't have a Mac and can't debug it then.
> Any ideas what might be causing this behaviour?
Maybe Safari generated two Keyboard events, but that's only a guess.
Could you add a Window.alert() or something like that to see if
onKeyUp is called twice?
Thanks
1. ListBox now responds to click events. Uses onBrowserEvent since a
ListBox bug prevents onClick from being called.
2. onKeyUp was refactored for better readability.
3. Completion occurs on tab as well as enter.
4. The onChange event handler is no longer needed.
5. Bug fixed: Pressing the up or down keys could cause the list
selection to go out of bounds.
6. Better matching logic that keeps invalid choices from being
displayed, even if an async call is delayed or fails. Assumes simple
case-insensitive "starts with" matching.
7. Generic async RPC mechanism.
8. Other minor tweaks.
New source code in the next post.
package com.gwt.components.client;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.ui.KeyboardListener;
import com.google.gwt.user.client.ui.ListBox;
import com.google.gwt.user.client.ui.PopupPanel;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.TextBox;
import com.google.gwt.user.client.ui.Widget;
public class AutoCompleteTextBoxAsync extends TextBox
implements KeyboardListener {
protected CompletionItemsAsync items = null;
protected boolean popupAdded = false;
protected boolean visible = false;
protected PopupPanel choicesPopup = new PopupPanel(true);
protected ListBox choices = new ListBox() {
public void onBrowserEvent(Event event) {
if (Event.ONCLICK == DOM.eventGetType(event)) {
complete();
}
}
};
/**
* Default Constructor
*/
public AutoCompleteTextBoxAsync()
{
super();
this.addKeyboardListener(this);
choices.sinkEvents(Event.ONCLICK);
this.setStyleName("AutoCompleteTextBox");
choicesPopup.add(choices);
choicesPopup.addStyleName("AutoCompleteChoices");
choices.setStyleName("list");
}
/**
* Sets an "algorithm" returning completion items
* You can define your own way how the textbox retrieves
autocompletion items
* by implementing the CompletionItems interface and setting the
according object
* @see SimpleAutoCompletionItem
* @param items CompletionItem implementation
*/
public void setCompletionItems(CompletionItemsAsync items)
{
this.items = items;
}
/**
* Returns the used CompletionItems object
* @return CompletionItems implementation
*/
public CompletionItemsAsync getCompletionItems()
{
return this.items;
}
/**
* Handle events that happen when keys are pressed.
*/
public void onKeyDown(Widget arg0, char arg1, int arg2) {
if(arg1 == KEY_ENTER)
{
enterKey(arg0, arg1, arg2);
}
else if(arg1 == KEY_TAB)
{
tabKey(arg0, arg1, arg2);
}
else if(arg1 == KEY_DOWN)
{
downKey(arg0, arg1, arg2);
}
else if(arg1 == KEY_UP)
{
upKey(arg0, arg1, arg2);
}
else if(arg1 == KEY_ESCAPE)
{
escapeKey(arg0, arg1, arg2);
}
}
/**
* Not used at all
*/
public void onKeyPress(Widget arg0, char arg1, int arg2) {
}
/**
* Handle events that happen when keys are released.
*/
public void onKeyUp(Widget arg0, char arg1, int arg2) {
switch(arg1) {
case KEY_ALT:
case KEY_CTRL:
case KEY_DOWN:
case KEY_END:
case KEY_ENTER:
case KEY_ESCAPE:
case KEY_HOME:
case KEY_LEFT:
case KEY_PAGEDOWN:
case KEY_PAGEUP:
case KEY_RIGHT:
case KEY_SHIFT:
case KEY_TAB:
case KEY_UP:
break;
default:
otherKey(arg0, arg1, arg2);
break;
}
}
// The down key was pressed.
protected void downKey(Widget arg0, char arg1, int arg2) {
int selectedIndex = choices.getSelectedIndex();
selectedIndex++;
if (selectedIndex >= choices.getItemCount())
{
selectedIndex = 0;
}
choices.setSelectedIndex(selectedIndex);
}
// The up key was pressed.
protected void upKey(Widget arg0, char arg1, int arg2) {
int selectedIndex = choices.getSelectedIndex();
selectedIndex--;
if(selectedIndex < 0)
{
selectedIndex = choices.getItemCount() - 1;
}
choices.setSelectedIndex(selectedIndex);
}
// The enter key was pressed.
protected void enterKey(Widget arg0, char arg1, int arg2) {
complete();
}
// The tab key was pressed.
protected void tabKey(Widget arg0, char arg1, int arg2) {
complete();
}
// The escape key was pressed.
protected void escapeKey(Widget arg0, char arg1, int arg2) {
choices.clear();
choicesPopup.hide();
this.visible = false;
}
// Any other non-special key was pressed.
protected void otherKey(Widget arg0, char arg1, int arg2) {
// Update the existing choices in the list box to reflect the
user's entry.
updateChoices(this.getText());
// If any text was entered, start an async callback.
if (this.getText().length() > 0 && items != null) {
items.getCompletionItems(this.getText(), new
CompletionItemsAsyncReturn() {
public void itemReturn(String[] matches) {
updateChoices(matches, getText());
}
});
}
}
// Hides/shows the choice box as needed.
// Assumes all choices are currently valid.
protected void hideChoicesIfNeeded(String text) {
// Hide the list box under any of these conditions:
// - the text box is empty
// - there are no matching choices
// - there is only one choice that exactly matches the text box
entry
// Show the list box under any other condition.
if (0 == text.length() || 0 == choices.getItemCount() ||
(1 == choices.getItemCount() &&
choices.getItemText(0).equals(text))) {
choices.clear();
choicesPopup.hide();
visible = false;
} else {
choices.setSelectedIndex(0);
choices.setVisibleItemCount(choices.getItemCount() + 1);
if(!popupAdded) {
RootPanel.get().add(choicesPopup);
popupAdded = true;
}
choicesPopup.show();
visible = true;
choicesPopup.setPopupPosition(this.getAbsoluteLeft(),
this.getAbsoluteTop() + this.getOffsetHeight());
choices.setWidth(this.getOffsetWidth() + "px");
}
}
// Removes all items in the choices menu that do not start with the
specified text.
protected void updateChoices(String text) {
int i = 0;
while (i < choices.getItemCount()) {
if
(choices.getItemText(i).toLowerCase().startsWith(text.toLowerCase())) {
++i;
} else {
choices.removeItem(i);
}
}
hideChoicesIfNeeded(text);
}
// Update the choices menu using the provided matches and entered
text.
protected void updateChoices(String[] matches, String text) {
choices.clear();
for(int i = 0; i < matches.length; i++)
{
choices.addItem((String) matches[i]);
}
updateChoices(text);
}
// add selected item to textbox
protected void complete()
{
if(this.visible && choices.getItemCount() > 0)
{
this.setText(choices.getItemText(choices.getSelectedIndex()));
}
choices.clear();
choicesPopup.hide();
this.visible = false;
}
}
--------------------------------------------------------------
CompletionItemsAsync.java
--------------------------------------------------------------
package com.gwt.components.client;
public interface CompletionItemsAsync {
/**
* Makes a call to find all completion items matching.
* @param match The user-entered text all completion items have
to match
* @param asyncReturn The object invoked when the async call
returns correctly
*/
public void getCompletionItems(String match,
CompletionItemsAsyncReturn asyncReturn);
}
--------------------------------------------------------------
CompletionItemsAsyncReturn.java
--------------------------------------------------------------
package com.gwt.components.client;
public interface CompletionItemsAsyncReturn {
/**
* Handles an array of items. Called by
<code>CompletionItemsAsync.getCompletionItems</code>
* @param items The array of compleition items
*/
public void itemReturn(String[] items);
}