Java tutorial
/* * Copyright 2012 Igor Vaynberg * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this work except in compliance with * the License. You may obtain a copy of the License in the LICENSE file, or 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.select2; import org.apache.wicket.IResourceListener; import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.ajax.json.JSONException; import org.apache.wicket.ajax.json.JSONWriter; import org.apache.wicket.event.IEvent; import org.apache.wicket.markup.head.IHeaderResponse; import org.apache.wicket.markup.head.OnDomReadyHeaderItem; import org.apache.wicket.markup.html.form.ChoiceRenderer; import org.apache.wicket.markup.html.form.HiddenField; import org.apache.wicket.model.IModel; import org.apache.wicket.request.IRequestParameters; import org.apache.wicket.request.Request; import org.apache.wicket.request.http.WebRequest; import org.apache.wicket.request.http.WebResponse; import org.apache.wicket.util.string.Strings; import org.wicketstuff.select2.json.JsonBuilder; import java.io.IOException; import java.io.OutputStreamWriter; import java.util.ArrayList; import java.util.List; /** * Base class for Select2 components * * @param <T> type of choice object * @param <M> type of model object * @author igor * * @param <T> * type of choice object * @param <M> * type of model object */ abstract class AbstractSelect2Choice<T, M> extends HiddenField<M> implements IResourceListener { private static final long serialVersionUID = 1L; private final Settings settings = new Settings(); private ChoiceProvider<T> provider; private List<T> choices; private ChoiceRenderer<T> renderer; /** * Constructor * * @param id * component id */ public AbstractSelect2Choice(String id) { this(id, null, null, null); } /** * Constructor * * @param id * component id * @param model * component model */ public AbstractSelect2Choice(String id, IModel<M> model) { this(id, model, null, null); } /** * Constructor. * * @param id * component id * @param provider * choice provider */ public AbstractSelect2Choice(String id, ChoiceProvider<T> provider) { this(id, null, provider); } /** * Constructor * * @param id * component id * @param model * component model * @param provider * choice provider */ public AbstractSelect2Choice(String id, IModel<M> model, ChoiceProvider<T> provider) { super(id, model); this.provider = provider; add(new Select2ResourcesBehavior()); setOutputMarkupId(true); } /** * Construct. * * @param id markup id * @param model model for select * @param collection list options for select */ public AbstractSelect2Choice(String id, IModel<M> model, List<T> collection) { this(id, model, collection, null); } /** * Construct. * * @param id markup id * @param model model for select * @param choices list options for select * @param renderer renderer list item * @see HiddenField#HiddenField(String, IModel) */ public AbstractSelect2Choice(String id, IModel<M> model, List<T> choices, ChoiceRenderer<T> renderer) { super(id, model); if (null == choices) { throw new IllegalStateException( "Select2 choice component: " + getId() + " does not have a List<T> set"); } if (null == renderer) { throw new IllegalStateException( "Select2 choice component: " + getId() + " does not have a WCLChoiceRenderer<T> set"); } add(new Select2ResourcesBehavior()); this.choices = new ArrayList<>(choices); this.renderer = renderer; setOutputMarkupId(true); } /** * @return Select2 settings for this component */ public final Settings getSettings() { return settings; } /** * Sets the choice provider * * @param provider provider to set */ public final void setProvider(ChoiceProvider<T> provider) { this.provider = provider; } /** * @return data */ public final List<T> getChoices() { return choices; } public final void setChoices(List<T> choices) { this.choices = choices; } public final ChoiceRenderer<T> getRenderer() { return renderer; } public final void setRenderer(ChoiceRenderer<T> renderer) { this.renderer = renderer; } /** * @return choice provider */ public final ChoiceProvider<T> getProvider() { if (provider == null) { throw new IllegalStateException( "Select2 choice component: " + getId() + " does not have a ChoiceProvider set"); } return provider; } /** * Gets the markup id that is safe to use in jQuery by escaping dots in the * default {@link #getMarkup()} * * @return markup id */ protected String getJquerySafeMarkupId() { return getMarkupId().replace(".", "\\\\."); } /** * Escapes single quotes in localized strings to be used as JavaScript * strings enclosed in single quotes * * @param key * resource key for localized message * @return localized string with escaped single quotes */ protected String getEscapedJsString(String key) { String value = getString(key); return Strings.replaceAll(value, "'", "\\'").toString(); } @Override public void renderHead(IHeaderResponse response) { super.renderHead(response); if (!isAjax()) { // attach select options renderChoices(); } // initialize select2 response.render(OnDomReadyHeaderItem .forScript(JQuery.execute("$('#%s').select2(%s);", getJquerySafeMarkupId(), settings.toJson()))); // select current value renderInitializationScript(response); } /** * Renders script used to initialize the value of Select2 after it is * created so it matches the current model object. * * @param response * header response */ protected abstract void renderInitializationScript(IHeaderResponse response); @Override protected void onInitialize() { super.onInitialize(); // configure the ajax callbacks if (isAjax()) { AjaxSettings ajax = settings.getAjax(true); ajax.setData(String.format( "function(term, page) { return { term: term, page:page, '%s':true, '%s':[window.location.protocol, '//', window.location.host, window.location.pathname].join('')}; }", WebRequest.PARAM_AJAX, WebRequest.PARAM_AJAX_BASE_URL)); ajax.setResults("function(data, page) { return data; }"); } // configure the localized strings/renderers getSettings().setFormatNoMatches("function() { return '" + getEscapedJsString("noMatches") + "';}"); getSettings().setFormatInputTooShort("function(input, min) { return min - input.length == 1 ? '" + getEscapedJsString("inputTooShortSingular") + "' : '" + getEscapedJsString("inputTooShortPlural") + "'.replace('{number}', min - input.length); }"); getSettings().setFormatSelectionTooBig( "function(limit) { return limit == 1 ? '" + getEscapedJsString("selectionTooBigSingular") + "' : '" + getEscapedJsString("selectionTooBigPlural") + "'.replace('{limit}', limit); }"); getSettings().setFormatLoadMore("function() { return '" + getEscapedJsString("loadMore") + "';}"); getSettings().setFormatSearching("function() { return '" + getEscapedJsString("searching") + "';}"); } @Override protected void onConfigure() { super.onConfigure(); if (isAjax()) { getSettings().getAjax().setUrl(urlFor(IResourceListener.INTERFACE, null)); } } @Override public void onEvent(IEvent<?> event) { super.onEvent(event); if (event.getPayload() instanceof AjaxRequestTarget) { AjaxRequestTarget target = (AjaxRequestTarget) event.getPayload(); if (target.getComponents().contains(this)) { // if this component is being repainted by ajax, directly, we // must destroy Select2 so it removes // its elements from DOM target.prependJavaScript(JQuery.execute("$('#%s').select2('destroy');", getJquerySafeMarkupId())); } } } @Override protected boolean getStatelessHint() { return !isAjax(); } public boolean isAjax() { return provider != null; } @Override public void onResourceRequested() { // this is the callback that retrieves matching choices used to populate // the dropdown Request request = getRequestCycle().getRequest(); IRequestParameters params = request.getRequestParameters(); // retrieve choices matching the search term String term = params.getParameterValue("term").toOptionalString(); int page = params.getParameterValue("page").toInt(1); // select2 uses 1-based paging, but in wicket world we are used to // 0-based page -= 1; Response<T> response = new Response<T>(); provider.query(term, page, response); // jsonize and write out the choices to the response WebResponse webResponse = (WebResponse) getRequestCycle().getResponse(); webResponse.setContentType("application/json"); OutputStreamWriter out = new OutputStreamWriter(webResponse.getOutputStream(), getRequest().getCharset()); JSONWriter json = new JSONWriter(out); try { json.object(); json.key("results").array(); for (T item : response) { json.object(); provider.toJson(item, json); json.endObject(); } json.endArray(); json.key("more").value(response.getHasMore()).endObject(); } catch (JSONException e) { throw new RuntimeException("Could not write Json response", e); } try { out.flush(); } catch (IOException e) { throw new RuntimeException("Could not write Json to servlet response", e); } } @Override protected void onDetach() { if (isAjax()) { provider.detach(); } super.onDetach(); } /** * render single choice * * @param choice choice * @param jsonBuilder json builder */ protected void renderChoice(T choice, JsonBuilder jsonBuilder) { String key = renderer.getIdValue(choice, choices.lastIndexOf(choice)); Object value = renderer.getDisplayValue(choice); jsonBuilder.key("id").value(key).key("text").value(value); } // render options on select2 private void renderChoices() { JsonBuilder selection = new JsonBuilder(); try { selection.object(); selection.key("more").value(false); selection.key("results").array(); attachChoicesJson(selection); selection.endArray().endObject(); } catch (JSONException e) { throw new RuntimeException("Error converting model object to Json", e); } getSettings().setData(selection.toJson().toString()); } private void attachChoicesJson(JsonBuilder jsonBuilder) throws JSONException { for (T choice : choices) { jsonBuilder.object(); renderChoice(choice, jsonBuilder); jsonBuilder.endObject(); } } }