All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.sakaiproject.wicket.component.SakaiDateTimeField Maven / Gradle / Ivy

The newest version!
/**
 * Copyright (c) 2006-2020 The Apereo Foundation
 *
 * Licensed under the Educational Community 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://opensource.org/licenses/ecl2
 *
 * 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.sakaiproject.wicket.component;

import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
import java.util.Objects;
import org.apache.wicket.AttributeModifier;
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.TextField;
import org.apache.wicket.model.IModel;
import org.apache.wicket.util.convert.ConversionException;
import org.apache.wicket.util.convert.IConverter;
import org.sakaiproject.portal.util.PortalUtils;
import org.sakaiproject.util.DateFormatterUtil;

/**
 * A TextField equipped with a standard Sakai datepicker.
 * 
 * This component adds the jquery-ui and lang-datepicker scripts to the page by default,
 * so they do not need to be added separately. If you know JQuery UI is already loaded on the page,
 * call setLoadJQueryUI(false).
 *
 * JQuery is also required but is assumed to already be loaded on the page.
 *
 * @author plukasew
 */
public class SakaiDateTimeField extends TextField
{
	private static final String DATEPICKERSCRIPT = "/library/js/lang-datepicker/lang-datepicker.js%s";
	private static final String JQUERYUISCRIPT = "/library/webjars/jquery-ui/1.12.1/jquery-ui.min.js%s";

	private SakaiIsoDateConverter dateConverter;
	private static final String DATEPICKER_FORMAT = "yyyy-MM-dd HH:mm:ss";
	private static final String DATEPICKER_FORMAT_DATE_ONLY = "yyyy-MM-dd";

	private boolean useTime = true, allowEmptyDate = true, loadJQueryUI = true;
	private final ZoneId zoneId;

	/**
	 * @param id wicket id
	 * @param model a ZonedDateTime. Can be null if no time is set.
	 * @param timeZoneId timezone for this time, cannot be null. Should match the model's timezone, if model is non-null.
	 */
	public SakaiDateTimeField(String id, IModel model, ZoneId timeZoneId)
	{
		super(id, model);
		Objects.requireNonNull(timeZoneId);
		zoneId = timeZoneId;
	}

	@Override
	protected void onInitialize()
	{
		super.onInitialize();

		setOutputMarkupId(true);
		dateConverter = new SakaiIsoDateConverter(getMarkupId());

		add(AttributeModifier.append("class", "sakai-datetimefield"));
	}

	@Override
	public  IConverter getConverter(Class type)
	{
		return ZonedDateTime.class.isAssignableFrom(type) ? (IConverter) dateConverter : super.getConverter(type);
	}

	@Override
	public void renderHead(IHeaderResponse response)
	{
		super.renderHead(response);

		final String version = PortalUtils.getCDNQuery();
		if (loadJQueryUI)
		{
			response.render(JavaScriptHeaderItem.forUrl(String.format(JQUERYUISCRIPT, version)));
		}
		response.render(JavaScriptHeaderItem.forUrl(String.format(DATEPICKERSCRIPT, version)));
		String pattern = useTime ? DATEPICKER_FORMAT : DATEPICKER_FORMAT_DATE_ONLY;
		String formattedDate = getModelObject() == null ? "" : getModelObject().format(DateTimeFormatter.ofPattern(pattern, getSession().getLocale()));
		String script = String.format("SDP.initSakaiDatePicker('%s','%s', %b, %b);", getMarkupId(), formattedDate, useTime, allowEmptyDate);
		response.render(OnDomReadyHeaderItem.forScript(script));
	}

	/**
	 * Sets whether to include a time component in the display of the text field and date picker. Defaults to true.
	 * @param value true if using time
	 * @return this, for method chaining
	 */
	public SakaiDateTimeField setUseTime(boolean value)
	{
		useTime = value;
		return this;
	}

	/**
	 * Sets whether to allow fields to have no date set. Defaults to true.
	 * @param value true if empty dates are allowed
	 * @return this, for method chaining
	 */
	public SakaiDateTimeField setAllowEmptyDate(boolean value)
	{
		allowEmptyDate = value;
		return this;
	}

	/**
	 * Sets whether to have the component load the JQueryUI dependency on the page. Defaults to true.
	 * @param value true if JQueryUI should be loaded by this component
	 * @return this, for method chaining
	 */
	public SakaiDateTimeField setLoadJQueryUI(boolean value)
	{
		loadJQueryUI = value;
		return this;
	}

	/**
	 * Custom converter to parse the hidden ISO date string from lang-datepicker.js and return a ZonedDateTime
	 */
	private class SakaiIsoDateConverter implements IConverter
	{
		private final String componentMarkupId;

		public SakaiIsoDateConverter(String componentMarkupId)
		{
			this.componentMarkupId = componentMarkupId;
		}

		@Override
		public ZonedDateTime convertToObject(String string, Locale locale) throws ConversionException
		{
			// ignore actual input string and read in iso date from hidden input instead
			String isoParam = componentMarkupId + "ISO8601";
			String isoDateStr = getRequest().getRequestParameters().getParameterValue(isoParam).toString("");
			// format is like '2017-12-03T10:15:30-04:00'
			if (!DateFormatterUtil.isValidISODate(isoDateStr))
			{
				throw new ConversionException("Invalid ISO date: " + isoDateStr).setResourceKey("sakaidatetimefield.error.dateformat");
			}

			// The string we get back from the datepicker has an offset which may or may not match the desired timezone.
			// Since the user cannot change the timezone using the datepicker, we just ignore the datepicker-provided offset
			// and set the timezone from the stored zone.
			LocalDateTime local = LocalDateTime.parse(isoDateStr, DateTimeFormatter.ISO_OFFSET_DATE_TIME);
			return ZonedDateTime.of(local, zoneId);
		}

		@Override
		public String convertToString(ZonedDateTime c, Locale locale)
		{
			// ignore model date, it will be set via javascript on page load
			return "";
		}

	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy