org.wicketstuff.select2.AbstractSelect2Choice Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of wicketstuff-select2 Show documentation
Show all versions of wicketstuff-select2 Show documentation
Bridges Apache Wicket with Select2 components
/*
* 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 com.github.openjson.JSONException;
import com.github.openjson.JSONStringer;
import org.apache.wicket.IRequestListener;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.event.IEvent;
import org.apache.wicket.markup.ComponentTag;
import org.apache.wicket.markup.MarkupStream;
import org.apache.wicket.markup.html.form.FormComponent;
import org.apache.wicket.model.IModel;
import org.apache.wicket.request.IRequestParameters;
import org.apache.wicket.request.Request;
import org.apache.wicket.request.cycle.RequestCycle;
import org.apache.wicket.request.http.WebRequest;
import org.apache.wicket.request.http.WebResponse;
import org.apache.wicket.util.string.AppendingStringBuffer;
import org.apache.wicket.util.string.Strings;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
/**
* Base class for Select2 components
*
* @param
* type of choice object
* @param
* type of model object
* @author igor
*/
public abstract class AbstractSelect2Choice extends FormComponent implements IRequestListener
{
private static final long serialVersionUID = 1L;
private final Select2Behavior select2Behavior = new Select2Behavior();
private ChoiceProvider provider;
private boolean convertInputPerformed = false;
/**
* @param id
* markup id
*/
public AbstractSelect2Choice(String id)
{
this(id, null, null);
}
/**
* Constructor
*
* @param id
* component id
* @param model
* component model
*/
public AbstractSelect2Choice(String id, IModel model)
{
this(id, model, null);
}
/**
* Constructor.
*
* @param id
* component id
* @param provider
* choice provider
*/
public AbstractSelect2Choice(String id, ChoiceProvider provider)
{
this(id, null, provider);
}
/**
* Construct.
*
* @param id
* markup id
* @param model
* model for select
* @param provider
* choice provider
*/
public AbstractSelect2Choice(String id, IModel model, ChoiceProvider provider)
{
super(id, model);
this.provider = provider;
add(select2Behavior);
setOutputMarkupId(true);
}
/**
* Returns the {@link Settings}. The settings
*
* closeOnSelect = true
* escapeMarkup = "Utils.escapeMarkup"
* width = {@link Settings.Widths#RESOLVE};
*
* are the the Select2 default as documented on https://select2.org/configuration/options-api, which are different from the default in {@link Settings}
*
* @return Select2 settings for this component.
*/
public final Settings getSettings()
{
return select2Behavior.getSettings();
}
/**
* Sets the choice provider
*
* @param provider
* provider to set
*/
public final void setProvider(ChoiceProvider provider)
{
this.provider = provider;
}
/**
* @return choice provider
*/
public final ChoiceProvider getProvider()
{
if (provider == null)
{
throw new IllegalStateException("Select2 choice component: " + getId() +
" does not have a ChoiceProvider set");
}
return provider;
}
@Override
public final void convertInput()
{
// AbstractSelect2Choice uses ChoiceProvider to convert IDS into objects.
// The #getConverter() method is not supported by Select2Choice.
String[] inputAsArray = getInputAsArray();
M convertedInput = convertValue(inputAsArray);
setConvertedInput(convertedInput);
convertInputPerformed = true;
}
/**
* Convert IDS into choices.
*
* @param ids
* list of identities
* @return collection of choices or empty collection
*/
protected final Collection convertIdsToChoices(List ids)
{
if (ids == null || ids.isEmpty())
{
return Collections.emptyList();
}
return getProvider().toChoices(ids);
}
/**
* 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();
}
/**
* @return current value, suitable for rendering as selected value in select2 component
*/
protected final M getCurrentValue()
{
if (hasRawInput())
{
// since raw input is retained, we need explicitly convert it to target value
if (convertInputPerformed)
{
return getConvertedInput();
}
else
{
String raw = getRawInput();
M result = null;
if (raw != null)
{
String[] values = raw.split(FormComponent.VALUE_SEPARATOR);
result = convertValue(values);
}
return result;
}
}
else
{
return getModelObject();
}
}
@Override
protected void onInitialize()
{
super.onInitialize();
// configure the ajax callbacks
if (isAjax() && !getSettings().isStateless())
{
AjaxSettings ajax = getSettings().getAjax(true);
ajax.setData(String.format(
"function(params) { return { '%s': params.term, page: params.page, '%s':true, '%s':[window.location.protocol, '//', window.location.host, window.location.pathname].join('')}; }",
getSettings().getQueryParam(), WebRequest.PARAM_AJAX, WebRequest.PARAM_AJAX_BASE_URL));
ajax.setProcessResults("function(data, page) { return { results: data.items, pagination: { more: data.more } }; }");
}
else if (getSettings().isStateless()) //configure stateless mode
{
AjaxSettings ajax = getSettings().getAjax(true);
ajax.setProcessResults("function(data, page) { return { results: data.items, pagination: { more: data.more } }; }");
}
}
@Override
protected void onConfigure()
{
super.onConfigure();
if(getSettings().isStateless())
{
if(Strings.isEmpty(getSettings().getMountPath()))
{
throw new IllegalStateException("Select2 in stateless mode should specify a mountPath");
}
getSettings().getAjax(true).setUrl(getSettings().getMountPath());
}
else if (isAjax())
{
getSettings().getAjax(true).setUrl(urlForListener(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()
{
if(getSettings().isStateless())
{
return true;
}
return !isAjax();
}
public boolean isAjax()
{
return provider != null;
}
/**
* Utility method to generate JSON response.
*
* @param provider
* @param outputStream
*/
public static void generateJSON(ChoiceProvider provider, OutputStream outputStream)
{
generateJSON(Settings.DEFAULT_QUERY_PARAM, provider, outputStream);
}
/**
* Utility method to generate JSON response.
*
* @param provider
* @param outputStream
* @param queryParam - parameter to be used as Ajax query param
*/
public static void generateJSON(String queryParam, ChoiceProvider provider, OutputStream outputStream)
{
// this is the callback that retrieves matching choices used to populate the dropdown
Request request = RequestCycle.get().getRequest();
IRequestParameters params = request.getRequestParameters();
// retrieve choices matching the search term
String term = params.getParameterValue(queryParam).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 response = new Response<>();
provider.query(term, page, response);
// jsonize and write out the choices to the response
OutputStreamWriter out = new OutputStreamWriter(outputStream, request.getCharset());
JSONStringer json = new JSONStringer();
try
{
json.object();
json.key("items").array();
for (T item : response)
{
json.object();
provider.toJson(item, json);
json.endObject();
}
json.endArray();
json.key("more").value(response.getHasMore()).endObject();
out.write(json.toString());
}
catch (JSONException | IOException 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);
}
try
{
out.close();
}
catch (IOException e)
{
throw new RuntimeException("Could not write Json to servlet response", e);
}
}
@Override
public void onRequest()
{
WebResponse webResponse = (WebResponse) getRequestCycle().getResponse();
webResponse.setContentType("application/json");
generateJSON(getSettings().getQueryParam(), provider, webResponse.getOutputStream());
}
@Override
protected void onDetach()
{
convertInputPerformed = false;
if (isAjax())
{
getProvider().detach();
}
super.onDetach();
}
/**
* Append a single option markup.
*
* @param buffer buffer to append to
* @param choice choice to create option markup for
*/
protected void appendOptionHtml(final AppendingStringBuffer buffer, T choice)
{
buffer.append("");
}
/**
* Processes the component tag.
*
* @param tag
* Tag to modify
* @see org.apache.wicket.Component#onComponentTag(ComponentTag)
*/
@Override
protected void onComponentTag(ComponentTag tag)
{
// Must be attached to an select tag
checkComponentTag(tag, "select");
// Default handling for component tag
super.onComponentTag(tag);
}
/**
* this method will add default options
*
* @param markupStream
* The markup stream
* @param openTag
* The open tag for the body
*/
@Override
public void onComponentTagBody(final MarkupStream markupStream, final ComponentTag openTag)
{
if (provider == null) {
super.onComponentTagBody(markupStream, openTag);
} else {
M currentValue = getCurrentValue();
replaceComponentTagBody(markupStream, openTag, createOptionsHtml(currentValue));
}
}
/**
* Creates the HTML option(s) markup representing the current value.
*
* @param currentValue
* the current value
* @return
* the HTML markup for any option(s)
*/
protected abstract CharSequence createOptionsHtml(M currentValue);
/**
* Empty input is acceptable
*/
@Override
public boolean isInputNullable()
{
return true;
}
}