Java tutorial
/* * 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.wicketstuff.objectautocomplete; import org.apache.wicket.AttributeModifier; import org.apache.wicket.Component; import org.apache.wicket.ajax.AjaxEventBehavior; import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.ajax.attributes.AjaxRequestAttributes; import org.apache.wicket.ajax.markup.html.AjaxFallbackLink; import org.apache.wicket.markup.ComponentTag; import org.apache.wicket.markup.html.WebComponent; import org.apache.wicket.markup.html.WebMarkupContainer; import org.apache.wicket.markup.html.basic.Label; import org.apache.wicket.markup.html.form.FormComponentPanel; import org.apache.wicket.markup.html.form.HiddenField; import org.apache.wicket.markup.html.form.TextField; import org.apache.wicket.markup.html.image.Image; import org.apache.wicket.model.CompoundPropertyModel; import org.apache.wicket.model.IModel; import org.apache.wicket.model.IWrapModel; import org.apache.wicket.model.Model; import org.apache.wicket.model.PropertyModel; import org.apache.wicket.util.string.Strings; import org.apache.wicket.util.value.IValueMap; import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; /** * Wicket component for selecting a single object of type T with an identifier of type I via * autocompletion. The textfield used for autocompletion is nothing more than a * <em>search field</em> where the autocomplete menu presents the search results. Selecting an entry * of the search results (either via keyboard or mouse click) "picks" an entity, whose id is stored * in the model of this component (So, the model object type of this model is I). * * A subclass must provide the list of T-objects, which are presented in the autocompletion menu. * * @author roland * @since May 21, 2008 */ public class ObjectAutoCompleteField<O /* object */, I /* its id */extends Serializable> extends FormComponentPanel<I> implements ObjectAutoCompleteCancelListener { private static final long serialVersionUID = 1L; // Used for removing non-conformant span attributes final private static Set<String> ALLOWED_SPAN_ATTRIBUTES = new HashSet<String>(Arrays.asList("wicket:id", "id", "class", "lang", "dir", "title", "style", "align", "onclick", "ondblclick", "onmousedown", "onmouseup", "onmouseover", "onmousemove", "onmouseout", "onkeypress", "onkeydown", "onkeyup")); // Listener to be notified on a selection change. These need not to be components private final List<ObjectAutoCompleteSelectionChangeListener<I>> selectionChangeListeners = new ArrayList<ObjectAutoCompleteSelectionChangeListener<I>>(); // Remember old id in case a search operation is aborted private I selectedObjectId; // Used for state handling private I backupObjectId; private String backupText; // Textfield used to search for the object private TextField<String> searchTextField; // Hiddenfield carrying the selected object id private HiddenField<I> objectField; // whether the input field should be cleared on selection private final boolean clearInputOnSelection; /** * Package scoped constructor to be used by the builder to create an auto completion fuild via * the builder pattern. I.e. use * {@link org.wicketstuff.objectautocomplete.ObjectAutoCompleteBuilder#build(String)} for * creating an object auto completion component * * @param pId * id of the component to add * @param pModel * the model * @param pBuilder * builder object used to create this field. */ ObjectAutoCompleteField(String pId, IModel<I> pModel, ObjectAutoCompleteBuilder<O, I> pBuilder) { super(pId, pModel); // Remember original object. This is the one we are working on. selectedObjectId = getModelObject(); setOutputMarkupId(true); // Register ourself as a cancel listener to restore asn old id pBuilder.cancelListener(this); // Register all change selection listeners for (ObjectAutoCompleteSelectionChangeListener<I> listener : pBuilder.selectionChangeListener) { registerForUpdateOnSelectionChange(listener); } // Search Text model contains the text selected Model<String> searchTextModel = new Model<String>(); addSearchTextField(searchTextModel, pBuilder); addReadOnlyPanel(searchTextModel, pBuilder); clearInputOnSelection = pBuilder.clearInputOnSelection; } /** * Register a listener that needs to be updated when the selection changes, i.e. the user * selected an object from the suggestion list. The listener is called with the id-model and the * ajax request target which can be used for updating one self * * @param pListener * the listener to notify */ public void registerForUpdateOnSelectionChange(ObjectAutoCompleteSelectionChangeListener<I> pListener) { selectionChangeListeners.add(pListener); } // ========================================================================================================== // the 'search part' if in lookup mode private void addSearchTextField(final Model<String> pSearchTextModel, ObjectAutoCompleteBuilder<O, I> pBuilder) { searchTextField = new TextField<String>("search", pSearchTextModel) { private static final long serialVersionUID = 1L; @Override public boolean isVisible() { return isSearchMode(); } }; searchTextField.setOutputMarkupId(true); // this disables Firefox autocomplete searchTextField.add(AttributeModifier.append("autocomplete", "off")); objectField = new HiddenField<I>("hiddenId", new PropertyModel<I>(this, "selectedObjectId")); objectField.setOutputMarkupId(true); if (pBuilder.idType != null) { objectField.setType(pBuilder.idType); } add(objectField); searchTextField.add(pBuilder.buildBehavior(objectField), new ObjectUpdateBehavior() // newFormUpdateBehaviour(searchTextField) ); add(searchTextField); } @Override protected IModel<I> initModel() { @SuppressWarnings("unchecked") IModel<I> model = (IModel<I>) super.initModel(); if (model instanceof IWrapModel) { IWrapModel<I> iwModel = (IWrapModel<I>) model; if (iwModel.getWrappedModel() instanceof CompoundPropertyModel) { @SuppressWarnings("unchecked") CompoundPropertyModel<I> cpModel = (CompoundPropertyModel<I>) iwModel.getWrappedModel(); objectField.setModel(new PropertyModel<I>(cpModel, getId())); } } return model; } // the 'read only part' if the object has been selected private void addReadOnlyPanel(final Model<String> pSearchTextModel, ObjectAutoCompleteBuilder<O, I> pBuilder) { final WebMarkupContainer wac = new WebMarkupContainer("readOnlyPanel") { private static final long serialVersionUID = 1L; @Override public boolean isVisible() { return !isSearchMode(); } }; wac.setOutputMarkupId(true); ObjectReadOnlyRenderer<I> roRenderer = pBuilder.readOnlyObjectRenderer; Component objectReadOnlyComponent; if (roRenderer != null) { objectReadOnlyComponent = roRenderer.getObjectRenderer("selectedValue", new PropertyModel<I>(this, "selectedObjectId"), pSearchTextModel); } else { objectReadOnlyComponent = new Label("selectedValue", pSearchTextModel); } objectReadOnlyComponent.setOutputMarkupId(true); AjaxFallbackLink<Void> deleteLink = new AjaxFallbackLink<Void>("searchLink") { private static final long serialVersionUID = 1L; @Override public void onClick(AjaxRequestTarget target) { changeToSearchMode(target); } }; Component linkImage = new WebComponent("searchLinkImage").setVisible(false); if (pBuilder.imageResource != null || pBuilder.imageResourceReference != null) { linkImage = pBuilder.imageResource != null ? new Image("searchLinkImage", pBuilder.imageResource) : new Image("searchLinkImage", pBuilder.imageResourceReference); deleteLink.add(new Label(ObjectAutoCompleteBuilder.SEARCH_LINK_PANEL_ID).setVisible(false)); } else if (pBuilder.searchLinkContent != null) { deleteLink.add(pBuilder.searchLinkContent); } else if (!pBuilder.unchangeable) { deleteLink.add(new Label(ObjectAutoCompleteBuilder.SEARCH_LINK_PANEL_ID, pBuilder.searchLinkText)); } deleteLink.add(linkImage); if (pBuilder.searchOnClick) { deleteLink.setVisible(false); objectReadOnlyComponent.add(new AjaxEventBehavior("onclick") { private static final long serialVersionUID = 1L; @Override protected void onEvent(AjaxRequestTarget target) { changeToSearchMode(target); } }); } wac.add(objectReadOnlyComponent); wac.add(deleteLink); add(wac); } private void changeToSearchMode(AjaxRequestTarget target) { backupObjectId = selectedObjectId; backupText = searchTextField.getModelObject(); selectedObjectId = null; ObjectAutoCompleteField.this.setModelObject(null); if (target != null) { target.add(ObjectAutoCompleteField.this); String id = searchTextField.getMarkupId(); target.appendJavaScript( "Wicket.DOM.get('" + id + "').focus();" + "Wicket.DOM.get('" + id + "').select();"); } } /** * Callback called in case the user cancels a search via 'escape' * * @param pTarget * target to which the components to update are added */ public void searchCanceled(AjaxRequestTarget pTarget, boolean pForceRestore) { if (backupObjectId != null) { if (Strings.isEmpty(searchTextField.getModelObject()) && !pForceRestore) { searchTextField.setModelObject(null); backupText = null; backupObjectId = null; } else if (backupText != null) { searchTextField.setModelObject(backupText); } selectedObjectId = backupObjectId; pTarget.add(ObjectAutoCompleteField.this); } else { clearSearchInput(); pTarget.add(searchTextField); } notifyListeners(pTarget); } private void clearSearchInput() { searchTextField.setModelObject(null); selectedObjectId = null; searchTextField.clearInput(); backupText = null; } // mode detection based on the existance of a seleced model private boolean isSearchMode() { return selectedObjectId == null; } /** * Called when form is submitted. We are simply store the object remembered internally. */ @Override public void convertInput() { setConvertedInput(selectedObjectId); } @Override // ensure that this component is embedded in a <span> ... </span> protected void onComponentTag(ComponentTag pTag) { super.onComponentTag(pTag); pTag.setName("span"); // Remove non-conformant <span> attributes IValueMap attribMap = pTag.getAttributes(); Iterator<Map.Entry<String, Object>> attrIterator = attribMap.entrySet().iterator(); while (attrIterator.hasNext()) { Map.Entry<String, Object> entry = attrIterator.next(); String key = entry.getKey().toLowerCase(); if (!ALLOWED_SPAN_ATTRIBUTES.contains(key)) { attrIterator.remove(); } } } private void notifyListeners(AjaxRequestTarget pTarget) { for (ObjectAutoCompleteSelectionChangeListener<I> listener : selectionChangeListeners) { listener.selectionChanged(pTarget, objectField.getModel()); } } /** * Access to internal text field which can be used for adding some decorations or behaviours * (like JS-Handler or specific CSS). Please be conservative in what you are doing, i.e. do not * modify its model. * * Note: This method might vanish until the first release, it's probably better to provide * delegation methods. * * @return textfield textfield holding the search input */ public TextField<String> getSearchTextField() { return searchTextField; } // ========================================================================================= // Behaviour, when user leaves the input field. class ObjectUpdateBehavior extends AjaxEventBehavior { private static final long serialVersionUID = 1L; ObjectUpdateBehavior() { // uses a custom event name to avoid clashes with 'change' event super("objectchange"); } @Override protected void onEvent(AjaxRequestTarget target) { objectField.processInput(); searchTextField.processInput(); target.add(ObjectAutoCompleteField.this); notifyListeners(target); if (clearInputOnSelection) { clearSearchInput(); } } @Override protected void updateAjaxAttributes(AjaxRequestAttributes attributes) { super.updateAjaxAttributes(attributes); attributes.setMethod(AjaxRequestAttributes.Method.POST); String searchTextFieldMarkupId = searchTextField.getMarkupId(); String objectFieldMarkupId = objectField.getMarkupId(); String deps = "return [].concat(Wicket.Form.serializeElement('" + searchTextFieldMarkupId + "')).concat(Wicket.Form.serializeElement('" + objectFieldMarkupId + "'))"; attributes.getDynamicExtraParameters().add(deps); } } }