package lt.inventi.wicket.component.autocomplete; import java.io.Serializable; import java.util.List; import java.util.Map; import net.sf.json.JSONArray; import net.sf.json.JSONFunction; import net.sf.json.JSONObject; import org.apache.wicket.AttributeModifier; import org.apache.wicket.Component; import org.apache.wicket.behavior.Behavior; import org.apache.wicket.markup.ComponentTag; import org.apache.wicket.markup.head.CssHeaderItem; import org.apache.wicket.markup.head.IHeaderResponse; import org.apache.wicket.markup.head.JavaScriptHeaderItem; import org.apache.wicket.markup.head.OnDomReadyHeaderItem; import org.apache.wicket.markup.html.form.FormComponentPanel; import org.apache.wicket.markup.html.form.HiddenField; import org.apache.wicket.markup.html.form.SubmitLink; import org.apache.wicket.markup.html.form.TextField; import org.apache.wicket.markup.parser.XmlTag.TagType; import org.apache.wicket.model.IModel; import org.apache.wicket.model.Model; import org.apache.wicket.model.PropertyModel; import org.apache.wicket.model.StringResourceModel; import org.apache.wicket.request.IRequestCycle; import org.apache.wicket.request.IRequestHandler; import org.apache.wicket.request.IRequestParameters; import org.apache.wicket.request.cycle.RequestCycle; import org.apache.wicket.request.http.WebResponse; import org.apache.wicket.request.resource.JavaScriptResourceReference; import lt.inventi.wicket.resource.ResourceSettings; /** * Provides autocomplete functionality, based on jquery autocomplete plugin. * <p> * This class adds some extra functionality, which is not available in the * default implementation, such as autocomplete result expansion with mouse * click or keypress and creation of new autocompleted items. * * <p> * To update via Ajax use {@link AutocompleteAjaxUpdateBehaviour}. * * @param <T> */ public class Autocomplete<ID extends Serializable, T, S> extends FormComponentPanel<T> implements IQueryListener { private static final long serialVersionUID = 1L; /** * Json parameters */ public static final String VALUE_PARAM = "value"; public static final String LABEL_PARAM = "label"; public static final String ID_PARAM = "id"; private boolean addNewLinkEnabled = false; private SubmitLink addNewLink; private ValueField valueField; private HiddenField<ID> idField; private AutocompleteDataProvider<T> dataProvider; private AutocompleteDataValueProvider<T> dataValueProvider; private AutocompleteSearchProvider<S> searchProvider; private AddNewItemHandler<T> newItemHandler; private boolean labelWasSet; public Autocomplete(String id) { this(id, null); } public Autocomplete(String id, IModel<T> model) { super(id, model); valueField = new ValueField("value"); add(valueField); } protected void setDataProvider(AutocompleteDataProvider<T> dataProvider) { this.dataProvider = dataProvider; } protected void setSearchProvider(AutocompleteSearchProvider<S> searchProvider) { this.searchProvider = searchProvider; } protected void setDataValueProvider(AutocompleteDataValueProvider<T> dataValueProvider) { this.dataValueProvider = dataValueProvider; } protected void setNewItemHandler(AddNewItemHandler<T> newItemHandler) { if (newItemHandler == null) { throw new IllegalArgumentException("New item handler cannot be null!"); } this.addNewLinkEnabled = true; this.newItemHandler = newItemHandler; } /** * Dynamic control of allowing/disallowing new item addition. */ public Autocomplete<ID, T, S> disallowAddingNewItems() { this.addNewLinkEnabled = false; return this; } /** * Dynamic control of allowing/disallowing new item addition. */ public Autocomplete<ID, T, S> allowAddingNewItems() { this.addNewLinkEnabled = false; return this; } /** * This binds id field to the specific atribute. Override this to provide specific model. * Default implementation uses "id" as attribute name. * * @return */ protected IModel<ID> getIdModel() { return new PropertyModel<ID>(getDefaultModel(), "id"); } /** * Allows to provide extra JS function which will be invoked when item is * selected. This is useful to process extra attributes. For example: * * <pre> * function(event, ui){ extraVar = ui.item.extraAttr; } * </pre> * * @return */ protected String getSelectFunction() { return null; } @Override protected void onInitialize() { if (dataProvider == null) { throw new IllegalStateException(this.getClass() + " data provider can't be null"); } if (dataValueProvider == null) { throw new IllegalStateException(this.getClass() + " data label provider can't be null"); } if (searchProvider == null) { throw new IllegalStateException(this.getClass() + " search provider can't be null"); } if (!labelWasSet) { valueField.setLabel(new StringResourceModel(getId(), this.getParent(), null)); } String styleClass = (String) getMarkupAttributes().get("class"); valueField.add(AttributeModifier.replace("class", styleClass)); valueField.setOutputMarkupId(true); valueField.setModel(new ValueModel()); idField = new HiddenField<ID>("id", new IdModel(getIdModel())) { @Override public void updateModel() { // do nothing } }; add(idField); addNewLink = new SubmitLink("addNew") { @Override public void onSubmit() { NewAutocompleteItemCallback<T> callback = new NewAutocompleteItemCallback<T>() { @Override public void saved(T newEntity) { Autocomplete.this.saved(newEntity); } }; newItemHandler.onNewItem(getValueField().getInput(), callback); } @Override public boolean isVisible() { return addNewLinkEnabled; } }; add(addNewLink); addNewLink.setDefaultFormProcessing(false); addNewLink.setOutputMarkupId(true); super.onInitialize(); } protected void saved(T newEntity) { if (newEntity != null) { Autocomplete.this.setModelObject(newEntity); Autocomplete.this.clearAllInput(); } } @Override public Component add(Behavior... behaviors) { for (Behavior behavior : behaviors) { super.add(behavior); } return this; } @Override public void onQuery() { RequestCycle.get().scheduleRequestHandlerAfterCurrent(new IRequestHandler() { @Override public void respond(IRequestCycle requestCycle) { final WebResponse response = (WebResponse) requestCycle.getResponse(); IRequestParameters params = requestCycle.getRequest().getRequestParameters(); String criteria = params.getParameterValue("term").toString(); String limitString = params.getParameterValue("limit").toString(); int limit; try { limit = Integer.valueOf(limitString); } catch (NumberFormatException e) { limit = 10; } response.setContentType("application/json; charset=utf-8"); // make sure the request is not cached response.setHeader("Expires", "Mon, 26 Jul 1997 05:00:00 GMT"); response.setHeader("Cache-Control", "no-cache, must-revalidate"); response.setHeader("Pragma", "no-cache"); CharSequence text = generateResponse(criteria, limit); response.write(text); } @Override public void detach(IRequestCycle requestCycle) { // do nothing } }); } @Override public void renderHead(IHeaderResponse response) { response.render(JavaScriptHeaderItem.forReference(ResourceSettings.get().js().jqueryUi.uiWidgetAutocomplete)); response.render(JavaScriptHeaderItem.forReference(new JavaScriptResourceReference(Autocomplete.class, "Autocomplete.js"))); response.render(CssHeaderItem.forReference(AutocompleteCssResourceReference.get())); JSONObject cfg = new JSONObject(); cfg.put("source", urlFor(IQueryListener.INTERFACE, null)); String selectFunction = getSelectFunction(); if (selectFunction != null) { cfg.put("select", JSONFunction.parse(selectFunction)); } String script = autocompleteScript(valueField.getMarkupId(), cfg.toString()); response.render(OnDomReadyHeaderItem.forScript(script)); } protected String autocompleteScript(String id, String configJson){ return String.format("$('#%s').objectautocomplete(%s)", id, configJson); } private CharSequence generateResponse(String criteria, int size) { JSONArray response = new JSONArray(); List<S> result = searchProvider.searchItems(criteria, size); if (result != null) { for (S item : result) { JSONObject obj = new JSONObject(); Map<String, String> params = searchProvider.getJsonParameters(item); obj.putAll(params); response.add(obj); } } if (addNewLinkEnabled) { JSONObject link = new JSONObject(); link.put("addNew", true); link.put("id", addNewLink.getMarkupId()); response.add(link); } return response.toString(); } /** * Clears input for the value field. Useful if you updated model, and want to reflect its * changes in the page. */ public void clearAllInput() { idField.clearInput(); valueField.clearInput(); } /** * Sets label model for the value field. It will be used in error messages to replace ${label) * property * * @param labelModel */ public void setLabelModel(IModel<String> labelModel) { valueField.setLabel(labelModel); labelWasSet = true; } @Override protected void onComponentTag(ComponentTag tag) { tag.setType(TagType.OPEN); tag.setName("span"); tag.append("class", "autocomplete", " "); tag.remove("type"); super.onComponentTag(tag); } public ValueField getValueField() { return valueField; } public HiddenField<ID> getIdField() { return idField; } public class ValueField extends TextField<String> { public ValueField(String id) { super(id, new Model<String>()); } @Override public String getValidatorKeyPrefix() { return Autocomplete.this.getId(); } @Override public void updateModel() { // do nothing } } @Override protected void convertInput() { setConvertedInput(doConvertValue(getInputAsArray())); } private T doConvertValue(String[] value) { String key = idField.getInput(); T oldObject = getModelObject(); if (oldObject != null) { String id = dataProvider.getId(oldObject); if (key.equals(id)) { return oldObject; } } String valueString = valueField.getInput(); T objectValue = dataProvider.getObject(key, valueString, oldObject); return objectValue; } protected static boolean hasText(String s) { return s != null && !s.trim().isEmpty(); } private class IdModel implements IModel<ID> { private IModel<ID> parentModel; public IdModel(IModel<ID> parentModel) { this.parentModel = parentModel; } @Override public void detach() { // do nothing } @Override public ID getObject() { return parentModel.getObject(); } @Override public void setObject(ID object) { throw new UnsupportedOperationException("Id model should not be set by the autocomplete!"); } } private class ValueModel implements IModel<String> { private T lastObject; private String lastValue; @Override public String getObject() { T object = Autocomplete.this.getModelObject(); if (object == null) { return null; } if (object != lastObject) { lastObject = object; lastValue = dataValueProvider.extractValue(object); } return lastValue; } @Override public void setObject(String object) { throw new UnsupportedOperationException("Value model should not be set by the autocomplete!"); } @Override public void detach() { this.lastObject = null; } } }