/* Copyright 2014 InterCommIT b.v. * * This file is part of the "Weaves" project hosted on https://github.com/intercommit/Weaves * * Weaves is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * any later version. * * Weaves is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with Weaves. If not, see <http://www.gnu.org/licenses/>. * */ package nl.intercommit.weaves.components; import java.util.List; import org.apache.commons.lang.StringUtils; import org.apache.tapestry5.Binding; import org.apache.tapestry5.BindingConstants; import org.apache.tapestry5.ComponentResources; import org.apache.tapestry5.EventConstants; import org.apache.tapestry5.FieldValidationSupport; import org.apache.tapestry5.FieldValidator; import org.apache.tapestry5.Link; import org.apache.tapestry5.MarkupWriter; import org.apache.tapestry5.SelectModel; import org.apache.tapestry5.ValidationException; import org.apache.tapestry5.ValidationTracker; import org.apache.tapestry5.ValueEncoder; import org.apache.tapestry5.annotations.AfterRenderTemplate; import org.apache.tapestry5.annotations.BeforeRenderTemplate; import org.apache.tapestry5.annotations.Environmental; import org.apache.tapestry5.annotations.Import; import org.apache.tapestry5.annotations.Mixin; import org.apache.tapestry5.annotations.Parameter; import org.apache.tapestry5.annotations.RequestParameter; import org.apache.tapestry5.annotations.SupportsInformalParameters; import org.apache.tapestry5.corelib.base.AbstractField; import org.apache.tapestry5.corelib.mixins.RenderDisabled; import org.apache.tapestry5.internal.util.CaptureResultCallback; import org.apache.tapestry5.ioc.annotations.Inject; import org.apache.tapestry5.ioc.internal.util.InternalUtils; import org.apache.tapestry5.json.JSONArray; import org.apache.tapestry5.json.JSONObject; import org.apache.tapestry5.services.ComponentDefaultProvider; import org.apache.tapestry5.services.FieldValidatorDefaultSource; import org.apache.tapestry5.services.Request; import org.apache.tapestry5.services.ValueEncoderSource; import org.apache.tapestry5.services.javascript.JavaScriptSupport; import org.apache.tapestry5.util.EnumSelectModel; /** * @tapestrydoc */ @SupportsInformalParameters @Import(library="editableselect/EditableSelect.js",stylesheet="editableselect/EditableSelect.css") public class EditableSelectBox extends AbstractField { public static final String CHANGE_EVENT = "change"; /** * Allows a specific implementation of {@link ValueEncoder} to be supplied. This is used to create client-side * string values for the different options. * * @see ValueEncoderSource */ @Parameter private ValueEncoder encoder; @Inject private ComponentDefaultProvider defaultProvider; /** * The model used to identify the option groups and options to be presented to the user. This can be generated * automatically for Enum types. */ @Parameter(required = true, allowNull = false) private List<String> items; // additional class name(s) @Parameter(required = false, defaultPrefix=BindingConstants.LITERAL) private String className; @Inject private Request request; @Inject private ComponentResources resources; @Environmental private ValidationTracker tracker; /** * Performs input validation on the value supplied by the user in the form submission. */ @Parameter(defaultPrefix = BindingConstants.VALIDATE) private FieldValidator<Object> validate; /** * The value to read or update. */ @Parameter(required = true, principal = true, autoconnect = true) private Object value; /** * Binding the zone parameter will cause any change of Select's value to be handled as an Ajax request that updates * the * indicated zone. The component will trigger the event {@link EventConstants#VALUE_CHANGED} to inform its * container that Select's value has changed. * * @since 5.2.0 */ @Parameter(defaultPrefix = BindingConstants.LITERAL) private String zone; @Inject private FieldValidationSupport fieldValidationSupport; @Inject private JavaScriptSupport javascriptSupport; @SuppressWarnings("unused") @Mixin private RenderDisabled renderDisabled; private String selectedClientValue; @SuppressWarnings( { "unchecked" }) @Override protected void processSubmission(String elementName) { final String submittedValue = request.getParameter(elementName); tracker.recordInput(this, submittedValue); Object selectedValue = toValue(submittedValue); putPropertyNameIntoBeanValidationContext("value"); try { fieldValidationSupport.validate(selectedValue, resources, validate); value = selectedValue; } catch (ValidationException ex) { tracker.recordError(this, ex.getMessage()); } removePropertyNameFromBeanValidationContext(); } void afterRender(MarkupWriter writer) { writer.end(); } void beginRender(MarkupWriter writer) { selectedClientValue = tracker.getInput(this); // Use the value passed up in the form submission, if available. // Failing that, see if there is a current value (via the value parameter), and // convert that to a client value for later comparison. if (selectedClientValue == null) selectedClientValue = value == null ? null : encoder.toClient(value); String computedClassName = "select-input"; if (StringUtils.isNotBlank(className)) { computedClassName = computedClassName + " " + className; } writer.element("input","name",getControlName(),"id",getClientId(),"type","text","class",computedClassName,"autocomplete","off","value",selectedClientValue); putPropertyNameIntoBeanValidationContext("value"); validate.render(writer); removePropertyNameFromBeanValidationContext(); resources.renderInformalParameters(writer); decorateInsideField(); // Disabled is via a mixin if (this.zone != null) { Link link = resources.createEventLink(CHANGE_EVENT); JSONObject spec = new JSONObject("selectId", getClientId(), "zoneId", zone, "url", link.toURI()); javascriptSupport.addInitializerCall("linkSelectToZone", spec); } } Object onChange(@RequestParameter(value = "t:selectvalue", allowBlank = true) final String selectValue) { final Object newValue = toValue(selectValue); CaptureResultCallback<Object> callback = new CaptureResultCallback<Object>(); this.resources.triggerEvent(EventConstants.VALUE_CHANGED, new Object[] { newValue }, callback); this.value = newValue; return callback.getResult(); } protected Object toValue(String submittedValue) { return InternalUtils.isBlank(submittedValue) ? null : this.encoder.toValue(submittedValue); } @SuppressWarnings("unchecked") ValueEncoder defaultEncoder() { return defaultProvider.defaultValueEncoder("value", resources); } @SuppressWarnings("unchecked") SelectModel defaultModel() { Class valueType = resources.getBoundType("value"); if (valueType == null) return null; if (Enum.class.isAssignableFrom(valueType)) return new EnumSelectModel(valueType, resources.getContainerMessages()); return null; } /** * Computes a default value for the "validate" parameter using {@link FieldValidatorDefaultSource}. */ Binding defaultValidate() { return defaultProvider.defaultValidatorBinding("value", resources); } /** * Renders the options, including the blank option. */ @BeforeRenderTemplate void options(MarkupWriter writer) { JSONObject obj = new JSONObject(); obj.put("data", new JSONArray(items.toArray())); javascriptSupport.addScript("var "+getClientId()+"Options="+ obj.toString()+";", ""); } @AfterRenderTemplate void initializeBox() { javascriptSupport.addScript("EditableSelect.init('"+getClientId()+"',"+getClientId()+"Options);", ""); } @Override public boolean isRequired() { return validate.isRequired(); } }