com.vaadin.client.ui.VAbstractCalendarPanel Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of vaadin-client Show documentation
Show all versions of vaadin-client Show documentation
Vaadin is a web application framework for Rich Internet Applications (RIA).
Vaadin enables easy development and maintenance of fast and
secure rich web
applications with a stunning look and feel and a wide browser support.
It features a server-side architecture with the majority of the logic
running
on the server. Ajax technology is used at the browser-side to ensure a
rich
and interactive user experience.
/*
* Copyright 2000-2016 Vaadin Ltd.
*
* Licensed 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 com.vaadin.client.ui;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.google.gwt.aria.client.Roles;
import com.google.gwt.aria.client.SelectedValue;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.event.dom.client.BlurEvent;
import com.google.gwt.event.dom.client.BlurHandler;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.DomEvent;
import com.google.gwt.event.dom.client.FocusEvent;
import com.google.gwt.event.dom.client.FocusHandler;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyDownEvent;
import com.google.gwt.event.dom.client.KeyDownHandler;
import com.google.gwt.event.dom.client.KeyPressEvent;
import com.google.gwt.event.dom.client.KeyPressHandler;
import com.google.gwt.event.dom.client.MouseDownEvent;
import com.google.gwt.event.dom.client.MouseDownHandler;
import com.google.gwt.event.dom.client.MouseOutEvent;
import com.google.gwt.event.dom.client.MouseOutHandler;
import com.google.gwt.event.dom.client.MouseUpEvent;
import com.google.gwt.event.dom.client.MouseUpHandler;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.FlexTable;
import com.google.gwt.user.client.ui.InlineHTML;
import com.google.gwt.user.client.ui.Widget;
import com.vaadin.client.BrowserInfo;
import com.vaadin.client.DateTimeService;
import com.vaadin.client.VConsole;
import com.vaadin.client.WidgetUtil;
import com.vaadin.shared.util.SharedUtil;
/**
* Abstract calendar panel to show and select a date using a resolution. The
* class is parameterized by the date resolution enumeration type.
*
* @author Vaadin Ltd
*
* @param
* the resolution type which this field is based on (day, month, ...)
* @since 8.0
*/
@SuppressWarnings("deprecation")
public abstract class VAbstractCalendarPanel>
extends FocusableFlexTable implements KeyDownHandler, KeyPressHandler,
MouseOutHandler, MouseDownHandler, MouseUpHandler, BlurHandler,
FocusHandler, SubPartAware {
public interface SubmitListener {
/**
* Called when calendar user triggers a submitting operation in calendar
* panel. Eg. clicking on day or hitting enter.
*/
void onSubmit();
/**
* On eg. ESC key.
*/
void onCancel();
}
/**
* Blur listener that listens to blur event from the panel
*/
public interface FocusOutListener {
/**
* @return true if the calendar panel is not used after focus moves out
*/
boolean onFocusOut(DomEvent> event);
}
/**
* FocusChangeListener is notified when the panel changes its _focused_
* value.
*/
public interface FocusChangeListener {
void focusChanged(Date focusedDate);
}
/**
* Represents a Date button in the calendar
*/
private class VEventButton extends Button {
public VEventButton() {
addMouseDownHandler(VAbstractCalendarPanel.this);
addMouseOutHandler(VAbstractCalendarPanel.this);
addMouseUpHandler(VAbstractCalendarPanel.this);
}
}
private static final String CN_FOCUSED = "focused";
private static final String CN_TODAY = "today";
private static final String CN_SELECTED = "selected";
private static final String CN_OFFMONTH = "offmonth";
private static final String CN_OUTSIDE_RANGE = "outside-range";
/**
* Represents a click handler for when a user selects a value by using the
* mouse
*/
private ClickHandler dayClickHandler = new ClickHandler() {
/*
* (non-Javadoc)
*
* @see
* com.google.gwt.event.dom.client.ClickHandler#onClick(com.google.gwt
* .event.dom.client.ClickEvent)
*/
@Override
public void onClick(ClickEvent event) {
if (!isEnabled() || isReadonly()) {
return;
}
Date newDate = ((Day) event.getSource()).getDate();
if (!isDateInsideRange(newDate,
getResolution(VAbstractCalendarPanel.this::isDay))) {
return;
}
if (newDate.getMonth() != displayedMonth.getMonth()
|| newDate.getYear() != displayedMonth.getYear()) {
// If an off-month date was clicked, we must change the
// displayed month and re-render the calendar (#8931)
displayedMonth.setMonth(newDate.getMonth());
displayedMonth.setYear(newDate.getYear());
renderCalendar();
}
focusDay(newDate);
selectFocused();
onSubmit();
}
};
private VEventButton prevYear;
private VEventButton nextYear;
private VEventButton prevMonth;
private VEventButton nextMonth;
private FlexTable days = new FlexTable();
private R resolution;
private Timer mouseTimer;
private Date value;
private DateTimeService dateTimeService;
private boolean showISOWeekNumbers;
private FocusedDate displayedMonth;
private FocusedDate focusedDate;
private Day selectedDay;
private Day focusedDay;
private FocusOutListener focusOutListener;
private SubmitListener submitListener;
private FocusChangeListener focusChangeListener;
private boolean hasFocus = false;
private VDateField parent;
private boolean initialRenderDone = false;
public VAbstractCalendarPanel() {
getElement().setId(DOM.createUniqueId());
setStyleName(VDateField.CLASSNAME + "-calendarpanel");
Roles.getGridRole().set(getElement());
/*
* Firefox auto-repeat works correctly only if we use a key press
* handler, other browsers handle it correctly when using a key down
* handler
*/
if (BrowserInfo.get().isGecko()) {
addKeyPressHandler(this);
} else {
addKeyDownHandler(this);
}
addFocusHandler(this);
addBlurHandler(this);
}
public void setParentField(VDateField parent) {
this.parent = parent;
}
/**
* Sets the focus to given date in the current view. Used when moving in the
* calendar with the keyboard.
*
* @param date
* A Date representing the day of month to be focused. Must be
* one of the days currently visible.
*/
private void focusDay(Date date) {
// Only used when calender body is present
if (acceptDayFocus()) {
if (focusedDay != null) {
focusedDay.removeStyleDependentName(CN_FOCUSED);
}
if (date != null && focusedDate != null) {
focusedDate.setTime(date.getTime());
int rowCount = days.getRowCount();
for (int i = 0; i < rowCount; i++) {
int cellCount = days.getCellCount(i);
for (int j = 0; j < cellCount; j++) {
Widget widget = days.getWidget(i, j);
if (widget != null
&& widget instanceof VAbstractCalendarPanel.Day) {
Day curday = (Day) widget;
if (curday.getDate().equals(date)) {
curday.addStyleDependentName(CN_FOCUSED);
focusedDay = curday;
return;
}
}
}
}
}
}
}
/**
* Returns {@code true} if current resolution assumes handling focus event
* for day UI component.
*
* @return {@code true} if day focus events should be handled, {@code false}
* otherwise
*/
protected abstract boolean acceptDayFocus();
/**
* Returns {@code true} if the provided {@code resolution} represents a day.
*
* @param resolution
* the given resolution
* @return {@code true} if the {@code resolution} represents a day
*/
protected abstract boolean isDay(R resolution);
/**
* Returns {@code true} if the provided {@code resolution} represents a
* month.
*
* @param resolution
* the given resolution
* @return {@code true} if the {@code resolution} represents a month
*/
protected abstract boolean isMonth(R resolution);
/**
* Returns {@code true} if the provided {@code resolution} represents an
* year.
*
* @param resolution
* the given resolution
* @return {@code true} if the {@code resolution} represents a year
*/
protected boolean isYear(R resolution) {
return parent.isYear(resolution);
}
/**
* Returns {@code true} if the {@code resolution} representation is strictly
* below month (day, hour, etc..).
*
* @param resolution
* the given resolution
* @return whether the {@code resolution} is below the month resolution
*/
protected abstract boolean isBelowMonth(R resolution);
/**
* Returns all available resolutions for the widget.
*
* @return all available resolutions
*/
protected Stream getResolutions() {
return parent.getResolutions();
}
/**
* Finds the resolution by the {@code filter}.
*
* @param filter
* predicate to filter resolutions
* @return the resolution accepted by the {@code filter}
*/
protected R getResolution(Predicate filter) {
List resolutions = getResolutions().filter(filter)
.collect(Collectors.toList());
assert resolutions
.size() == 1 : "The result of filtering by the predicate "
+ "contains unexpected number of resolution items :"
+ resolutions.size();
return resolutions.get(0);
}
/**
* Sets the selection highlight to a given day in the current view
*
* @param date
* A Date representing the day of month to be selected. Must be
* one of the days currently visible.
*
*/
private void selectDate(Date date) {
if (selectedDay != null) {
selectedDay.removeStyleDependentName(CN_SELECTED);
Roles.getGridcellRole()
.removeAriaSelectedState(selectedDay.getElement());
}
int rowCount = days.getRowCount();
for (int i = 0; i < rowCount; i++) {
int cellCount = days.getCellCount(i);
for (int j = 0; j < cellCount; j++) {
Widget widget = days.getWidget(i, j);
if (widget != null
&& widget instanceof VAbstractCalendarPanel.Day) {
Day curday = (Day) widget;
if (curday.getDate().equals(date)) {
curday.addStyleDependentName(CN_SELECTED);
selectedDay = curday;
Roles.getGridcellRole().setAriaSelectedState(
selectedDay.getElement(), SelectedValue.TRUE);
return;
}
}
}
}
}
/**
* Updates year, month, day from focusedDate to value
*/
private void selectFocused() {
if (focusedDate != null
&& isDateInsideRange(focusedDate, getResolution())) {
if (value == null) {
// No previously selected value (set to null on server side).
// Create a new date using current date and time
value = new Date();
}
/*
* #5594 set Date (day) to 1 in order to prevent any kind of
* wrapping of months when later setting the month. (e.g. 31 ->
* month with 30 days -> wraps to the 1st of the following month,
* e.g. 31st of May -> 31st of April = 1st of May)
*/
value.setDate(1);
if (value.getYear() != focusedDate.getYear()) {
value.setYear(focusedDate.getYear());
}
if (value.getMonth() != focusedDate.getMonth()) {
value.setMonth(focusedDate.getMonth());
}
if (value.getDate() != focusedDate.getDate()) {
}
// We always need to set the date, even if it hasn't changed, since
// it was forced to 1 above.
value.setDate(focusedDate.getDate());
selectDate(focusedDate);
} else {
VConsole.log("Trying to select a the focused date which is NULL!");
}
}
protected boolean onValueChange() {
return false;
}
public R getResolution() {
return resolution;
}
public void setResolution(R resolution) {
this.resolution = resolution;
}
/**
* Checks whether the widget is not editable (read-only).
*
* @return {@code true} if the widget is read-only
*/
protected boolean isReadonly() {
return parent.isReadonly();
}
/**
* Checks whether the widget is enabled.
*
* @return {@code true} is the widget is enabled
*/
protected boolean isEnabled() {
return parent.isEnabled();
}
@Override
public void setStyleName(String style) {
super.setStyleName(style);
if (initialRenderDone) {
// Dynamic updates to the stylename needs to render the calendar to
// update the inner element stylenames
renderCalendar();
}
}
@Override
public void setStylePrimaryName(String style) {
super.setStylePrimaryName(style);
if (initialRenderDone) {
// Dynamic updates to the stylename needs to render the calendar to
// update the inner element stylenames
renderCalendar();
}
}
private void clearCalendarBody(boolean remove) {
if (!remove) {
// Leave the cells in place but clear their contents
// This has the side effect of ensuring that the calendar always
// contain 7 rows.
for (int row = 1; row < 7; row++) {
for (int col = 0; col < 8; col++) {
days.setHTML(row, col, " ");
}
}
} else if (getRowCount() > 1) {
removeRow(1);
days.clear();
}
}
/**
* Builds the top buttons and current month and year header.
*
* @param needsMonth
* Should the month buttons be visible?
*/
private void buildCalendarHeader(boolean needsMonth) {
getRowFormatter().addStyleName(0,
parent.getStylePrimaryName() + "-calendarpanel-header");
if (prevMonth == null && needsMonth) {
prevMonth = new VEventButton();
prevMonth.setHTML("‹");
prevMonth.setStyleName("v-button-prevmonth");
prevMonth.setTabIndex(-1);
nextMonth = new VEventButton();
nextMonth.setHTML("›");
nextMonth.setStyleName("v-button-nextmonth");
nextMonth.setTabIndex(-1);
setWidget(0, 3, nextMonth);
setWidget(0, 1, prevMonth);
} else if (prevMonth != null && !needsMonth) {
// Remove month traverse buttons
remove(prevMonth);
remove(nextMonth);
prevMonth = null;
nextMonth = null;
}
if (prevYear == null) {
prevYear = new VEventButton();
prevYear.setHTML("«");
prevYear.setStyleName("v-button-prevyear");
prevYear.setTabIndex(-1);
nextYear = new VEventButton();
nextYear.setHTML("»");
nextYear.setStyleName("v-button-nextyear");
nextYear.setTabIndex(-1);
setWidget(0, 0, prevYear);
setWidget(0, 4, nextYear);
}
updateControlButtonRangeStyles(needsMonth);
final String monthName = needsMonth
? getDateTimeService().getMonth(displayedMonth.getMonth()) : "";
final int year = displayedMonth.getYear() + 1900;
getFlexCellFormatter().setStyleName(0, 2,
parent.getStylePrimaryName() + "-calendarpanel-month");
getFlexCellFormatter().setStyleName(0, 0,
parent.getStylePrimaryName() + "-calendarpanel-prevyear");
getFlexCellFormatter().setStyleName(0, 4,
parent.getStylePrimaryName() + "-calendarpanel-nextyear");
getFlexCellFormatter().setStyleName(0, 3,
parent.getStylePrimaryName() + "-calendarpanel-nextmonth");
getFlexCellFormatter().setStyleName(0, 1,
parent.getStylePrimaryName() + "-calendarpanel-prevmonth");
setHTML(0, 2,
"" + monthName + " " + year
+ "");
}
private void updateControlButtonRangeStyles(boolean needsMonth) {
if (focusedDate == null) {
return;
}
if (needsMonth) {
Date prevMonthDate = (Date) focusedDate.clone();
removeOneMonth(prevMonthDate);
R month = getResolution(VAbstractCalendarPanel.this::isMonth);
if (!isDateInsideRange(prevMonthDate, month)) {
prevMonth.addStyleName(CN_OUTSIDE_RANGE);
} else {
prevMonth.removeStyleName(CN_OUTSIDE_RANGE);
}
Date nextMonthDate = (Date) focusedDate.clone();
addOneMonth(nextMonthDate);
if (!isDateInsideRange(nextMonthDate, month)) {
nextMonth.addStyleName(CN_OUTSIDE_RANGE);
} else {
nextMonth.removeStyleName(CN_OUTSIDE_RANGE);
}
}
Date prevYearDate = (Date) focusedDate.clone();
prevYearDate.setYear(prevYearDate.getYear() - 1);
R year = getResolution(VAbstractCalendarPanel.this::isYear);
if (!isDateInsideRange(prevYearDate, year)) {
prevYear.addStyleName(CN_OUTSIDE_RANGE);
} else {
prevYear.removeStyleName(CN_OUTSIDE_RANGE);
}
Date nextYearDate = (Date) focusedDate.clone();
nextYearDate.setYear(nextYearDate.getYear() + 1);
if (!isDateInsideRange(nextYearDate, year)) {
nextYear.addStyleName(CN_OUTSIDE_RANGE);
} else {
nextYear.removeStyleName(CN_OUTSIDE_RANGE);
}
}
/**
* Returns date time service for the widget.
*
* @see #setDateTimeService(DateTimeService)
*
* @return date time service
*/
protected DateTimeService getDateTimeService() {
return dateTimeService;
}
/**
* Returns the date field which this panel is attached to.
*
* @return the "parent" date field
*/
protected VDateField getDateField() {
return parent;
}
public void setDateTimeService(DateTimeService dateTimeService) {
this.dateTimeService = dateTimeService;
}
/**
* Returns whether ISO 8601 week numbers should be shown in the value
* selector or not. ISO 8601 defines that a week always starts with a Monday
* so the week numbers are only shown if this is the case.
*
* @return true if week number should be shown, false otherwise
*/
public boolean isShowISOWeekNumbers() {
return showISOWeekNumbers;
}
public void setShowISOWeekNumbers(boolean showISOWeekNumbers) {
this.showISOWeekNumbers = showISOWeekNumbers;
if (initialRenderDone && isBelowMonth(resolution)) {
clearCalendarBody(false);
buildCalendarBody();
}
}
/**
* Checks inclusively whether a date is inside a range of dates or not.
*
* @param date
* @return
*/
private boolean isDateInsideRange(Date date, R minResolution) {
assert (date != null);
return isAcceptedByRangeEnd(date, minResolution)
&& isAcceptedByRangeStart(date, minResolution);
}
/**
* Accepts dates greater than or equal to rangeStart, depending on the
* resolution. If the resolution is set to DAY, the range will compare on a
* day-basis. If the resolution is set to YEAR, only years are compared. So
* even if the range is set to one millisecond in next year, also next year
* will be included.
*
* @param date
* @param minResolution
* @return
*/
private boolean isAcceptedByRangeStart(Date date, R minResolution) {
assert (date != null);
// rangeStart == null means that we accept all values below rangeEnd
if (rangeStart == null) {
return true;
}
Date valueDuplicate = (Date) date.clone();
Date rangeStartDuplicate = (Date) rangeStart.clone();
if (isYear(minResolution)) {
return valueDuplicate.getYear() >= rangeStartDuplicate.getYear();
}
if (isMonth(minResolution)) {
valueDuplicate = clearDateBelowMonth(valueDuplicate);
rangeStartDuplicate = clearDateBelowMonth(rangeStartDuplicate);
} else {
valueDuplicate = clearDateBelowDay(valueDuplicate);
rangeStartDuplicate = clearDateBelowDay(rangeStartDuplicate);
}
return !rangeStartDuplicate.after(valueDuplicate);
}
/**
* Accepts dates earlier than or equal to rangeStart, depending on the
* resolution. If the resolution is set to DAY, the range will compare on a
* day-basis. If the resolution is set to YEAR, only years are compared. So
* even if the range is set to one millisecond in next year, also next year
* will be included.
*
* @param date
* @param minResolution
* @return
*/
private boolean isAcceptedByRangeEnd(Date date, R minResolution) {
assert (date != null);
// rangeEnd == null means that we accept all values above rangeStart
if (rangeEnd == null) {
return true;
}
Date valueDuplicate = (Date) date.clone();
Date rangeEndDuplicate = (Date) rangeEnd.clone();
if (isYear(minResolution)) {
return valueDuplicate.getYear() <= rangeEndDuplicate.getYear();
}
if (isMonth(minResolution)) {
valueDuplicate = clearDateBelowMonth(valueDuplicate);
rangeEndDuplicate = clearDateBelowMonth(rangeEndDuplicate);
} else {
valueDuplicate = clearDateBelowDay(valueDuplicate);
rangeEndDuplicate = clearDateBelowDay(rangeEndDuplicate);
}
return !rangeEndDuplicate.before(valueDuplicate);
}
private static Date clearDateBelowMonth(Date date) {
date.setDate(1);
return clearDateBelowDay(date);
}
private static Date clearDateBelowDay(Date date) {
date.setHours(0);
date.setMinutes(0);
date.setSeconds(0);
// Clearing milliseconds
long time = date.getTime() / 1000;
date = new Date(time * 1000);
return date;
}
/**
* Builds the day and time selectors of the calendar.
*/
private void buildCalendarBody() {
final int weekColumn = 0;
final int firstWeekdayColumn = 1;
final int headerRow = 0;
setWidget(1, 0, days);
setCellPadding(0);
setCellSpacing(0);
getFlexCellFormatter().setColSpan(1, 0, 5);
getFlexCellFormatter().setStyleName(1, 0,
getDateField().getStylePrimaryName() + "-calendarpanel-body");
days.getFlexCellFormatter().setStyleName(headerRow, weekColumn,
"v-week");
days.setHTML(headerRow, weekColumn, "");
// Hide the week column if week numbers are not to be displayed.
days.getFlexCellFormatter().setVisible(headerRow, weekColumn,
isShowISOWeekNumbers());
days.getRowFormatter().setStyleName(headerRow,
getDateField().getStylePrimaryName()
+ "-calendarpanel-weekdays");
if (isShowISOWeekNumbers()) {
days.getFlexCellFormatter().setStyleName(headerRow, weekColumn,
"v-first");
days.getFlexCellFormatter().setStyleName(headerRow,
firstWeekdayColumn, "");
days.getRowFormatter().addStyleName(headerRow,
getDateField().getStylePrimaryName()
+ "-calendarpanel-weeknumbers");
} else {
days.getFlexCellFormatter().setStyleName(headerRow, weekColumn, "");
days.getFlexCellFormatter().setStyleName(headerRow,
firstWeekdayColumn, "v-first");
}
days.getFlexCellFormatter().setStyleName(headerRow,
firstWeekdayColumn + 6, "v-last");
// Print weekday names
final int firstDay = getDateTimeService().getFirstDayOfWeek();
for (int i = 0; i < 7; i++) {
int day = i + firstDay;
if (day > 6) {
day = 0;
}
if (isBelowMonth(getResolution())) {
days.setHTML(headerRow, firstWeekdayColumn + i, ""
+ getDateTimeService().getShortDay(day) + "");
} else {
days.setHTML(headerRow, firstWeekdayColumn + i, "");
}
Roles.getColumnheaderRole().set(days.getCellFormatter()
.getElement(headerRow, firstWeekdayColumn + i));
}
// Zero out hours, minutes, seconds, and milliseconds to compare dates
// without time part
final Date tmp = new Date();
final Date today = new Date(tmp.getYear(), tmp.getMonth(),
tmp.getDate());
final Date selectedDate = value == null ? null
: new Date(value.getYear(), value.getMonth(), value.getDate());
final int startWeekDay = getDateTimeService()
.getStartWeekDay(displayedMonth);
final Date curr = (Date) displayedMonth.clone();
// Start from the first day of the week that at least partially belongs
// to the current month
curr.setDate(1 - startWeekDay);
// No month has more than 6 weeks so 6 is a safe maximum for rows.
for (int weekOfMonth = 1; weekOfMonth < 7; weekOfMonth++) {
for (int dayOfWeek = 0; dayOfWeek < 7; dayOfWeek++) {
// Actually write the day of month
Date dayDate = (Date) curr.clone();
Day day = new Day(dayDate);
day.setStyleName(getDateField().getStylePrimaryName()
+ "-calendarpanel-day");
if (!isDateInsideRange(dayDate, getResolution(this::isDay))) {
day.addStyleDependentName(CN_OUTSIDE_RANGE);
}
if (curr.equals(selectedDate)) {
day.addStyleDependentName(CN_SELECTED);
Roles.getGridcellRole().setAriaSelectedState(
day.getElement(), SelectedValue.TRUE);
selectedDay = day;
}
if (curr.equals(today)) {
day.addStyleDependentName(CN_TODAY);
}
if (curr.equals(focusedDate)) {
focusedDay = day;
if (hasFocus) {
day.addStyleDependentName(CN_FOCUSED);
}
}
if (curr.getMonth() != displayedMonth.getMonth()) {
day.addStyleDependentName(CN_OFFMONTH);
}
days.setWidget(weekOfMonth, firstWeekdayColumn + dayOfWeek,
day);
Roles.getGridcellRole().set(days.getCellFormatter().getElement(
weekOfMonth, firstWeekdayColumn + dayOfWeek));
// ISO week numbers if requested
days.getCellFormatter().setVisible(weekOfMonth, weekColumn,
isShowISOWeekNumbers());
if (isShowISOWeekNumbers()) {
final String baseCssClass = getDateField()
.getStylePrimaryName()
+ "-calendarpanel-weeknumber";
String weekCssClass = baseCssClass;
int weekNumber = DateTimeService.getISOWeekNumber(curr);
days.setHTML(weekOfMonth, 0, "" + weekNumber + "");
}
curr.setDate(curr.getDate() + 1);
}
}
}
/**
* Updates the calendar and text field with the selected dates.
*/
public void renderCalendar() {
renderCalendar(true);
}
/**
* For internal use only. May be removed or replaced in the future.
*
* Updates the calendar and text field with the selected dates.
*
* @param updateDate
* The value false prevents setting the selected date of the
* calendar based on focusedDate. That can be used when only the
* resolution of the calendar is changed and no date has been
* selected.
*/
public void renderCalendar(boolean updateDate) {
doRenderCalendar(updateDate);
initialRenderDone = true;
}
/**
* Performs the rendering required by the {@link #renderCalendar(boolean)}.
* Subclasses may override this method to provide a custom implementation
* avoiding {@link #renderCalendar(boolean)} override. The latter method
* contains a common logic which should not be overriden.
*
* @param updateDate
* The value false prevents setting the selected date of the
* calendar based on focusedDate. That can be used when only the
* resolution of the calendar is changed and no date has been
* selected.
*/
protected void doRenderCalendar(boolean updateDate) {
super.setStylePrimaryName(
getDateField().getStylePrimaryName() + "-calendarpanel");
if (focusedDate == null) {
Date now = new Date();
// focusedDate must have zero hours, mins, secs, millisecs
focusedDate = new FocusedDate(now.getYear(), now.getMonth(),
now.getDate());
displayedMonth = new FocusedDate(now.getYear(), now.getMonth(), 1);
}
if (updateDate && !isDay(getResolution())
&& focusChangeListener != null) {
focusChangeListener.focusChanged(new Date(focusedDate.getTime()));
}
final boolean needsMonth = !isYear(getResolution());
boolean needsBody = isBelowMonth(resolution);
buildCalendarHeader(needsMonth);
clearCalendarBody(!needsBody);
if (needsBody) {
buildCalendarBody();
}
}
/**
* Moves the focus forward the given number of days.
*/
private void focusNextDay(int days) {
if (focusedDate == null) {
return;
}
Date focusCopy = ((Date) focusedDate.clone());
focusCopy.setDate(focusedDate.getDate() + days);
if (!isDateInsideRange(focusCopy, getResolution())) {
// If not inside allowed range, then do not move anything
return;
}
int oldMonth = focusedDate.getMonth();
int oldYear = focusedDate.getYear();
focusedDate.setDate(focusedDate.getDate() + days);
if (focusedDate.getMonth() == oldMonth
&& focusedDate.getYear() == oldYear) {
// Month did not change, only move the selection
focusDay(focusedDate);
} else {
// If the month changed we need to re-render the calendar
displayedMonth.setMonth(focusedDate.getMonth());
displayedMonth.setYear(focusedDate.getYear());
renderCalendar();
}
}
/**
* Moves the focus backward the given number of days.
*/
private void focusPreviousDay(int days) {
focusNextDay(-days);
}
/**
* Selects the next month
*/
private void focusNextMonth() {
if (focusedDate == null) {
return;
}
// Trying to request next month
Date requestedNextMonthDate = (Date) focusedDate.clone();
addOneMonth(requestedNextMonthDate);
if (!isDateInsideRange(requestedNextMonthDate,
getResolution(this::isMonth))) {
return;
}
// Now also checking whether the day is inside the range or not. If not
// inside,
// correct it
if (!isDateInsideRange(requestedNextMonthDate,
getResolution(this::isDay))) {
requestedNextMonthDate = adjustDateToFitInsideRange(
requestedNextMonthDate);
}
focusedDate.setTime(requestedNextMonthDate.getTime());
displayedMonth.setMonth(displayedMonth.getMonth() + 1);
renderCalendar();
}
private static void addOneMonth(Date date) {
int currentMonth = date.getMonth();
int requestedMonth = (currentMonth + 1) % 12;
date.setMonth(date.getMonth() + 1);
/*
* If the selected value was e.g. 31.3 the new value would be 31.4 but
* this value is invalid so the new value will be 1.5. This is taken
* care of by decreasing the value until we have the correct month.
*/
while (date.getMonth() != requestedMonth) {
date.setDate(date.getDate() - 1);
}
}
private static void removeOneMonth(Date date) {
int currentMonth = date.getMonth();
date.setMonth(date.getMonth() - 1);
/*
* If the selected value was e.g. 31.12 the new value would be 31.11 but
* this value is invalid so the new value will be 1.12. This is taken
* care of by decreasing the value until we have the correct month.
*/
while (date.getMonth() == currentMonth) {
date.setDate(date.getDate() - 1);
}
}
/**
* Selects the previous month
*/
private void focusPreviousMonth() {
if (focusedDate == null) {
return;
}
Date requestedPreviousMonthDate = (Date) focusedDate.clone();
removeOneMonth(requestedPreviousMonthDate);
if (!isDateInsideRange(requestedPreviousMonthDate,
getResolution(this::isMonth))) {
return;
}
if (!isDateInsideRange(requestedPreviousMonthDate,
getResolution(this::isDay))) {
requestedPreviousMonthDate = adjustDateToFitInsideRange(
requestedPreviousMonthDate);
}
focusedDate.setTime(requestedPreviousMonthDate.getTime());
displayedMonth.setMonth(displayedMonth.getMonth() - 1);
renderCalendar();
}
/**
* Selects the previous year
*/
private void focusPreviousYear(int years) {
if (focusedDate == null) {
return;
}
Date previousYearDate = (Date) focusedDate.clone();
previousYearDate.setYear(previousYearDate.getYear() - years);
// Do not focus if not inside range
if (!isDateInsideRange(previousYearDate, getResolution(this::isYear))) {
return;
}
// If we remove one year, but have to roll back a bit, fit it
// into the calendar. Also the months have to be changed
if (!isDateInsideRange(previousYearDate, getResolution(this::isDay))) {
previousYearDate = adjustDateToFitInsideRange(previousYearDate);
focusedDate.setYear(previousYearDate.getYear());
focusedDate.setMonth(previousYearDate.getMonth());
focusedDate.setDate(previousYearDate.getDate());
displayedMonth.setYear(previousYearDate.getYear());
displayedMonth.setMonth(previousYearDate.getMonth());
} else {
int currentMonth = focusedDate.getMonth();
focusedDate.setYear(focusedDate.getYear() - years);
displayedMonth.setYear(displayedMonth.getYear() - years);
/*
* If the focused date was a leap day (Feb 29), the new date becomes
* Mar 1 if the new year is not also a leap year. Set it to Feb 28
* instead.
*/
if (focusedDate.getMonth() != currentMonth) {
focusedDate.setDate(0);
}
}
renderCalendar();
}
/**
* Selects the next year
*/
private void focusNextYear(int years) {
if (focusedDate == null) {
return;
}
Date nextYearDate = (Date) focusedDate.clone();
nextYearDate.setYear(nextYearDate.getYear() + years);
// Do not focus if not inside range
if (!isDateInsideRange(nextYearDate, getResolution(this::isYear))) {
return;
}
// If we add one year, but have to roll back a bit, fit it
// into the calendar. Also the months have to be changed
if (!isDateInsideRange(nextYearDate, getResolution(this::isDay))) {
nextYearDate = adjustDateToFitInsideRange(nextYearDate);
focusedDate.setYear(nextYearDate.getYear());
focusedDate.setMonth(nextYearDate.getMonth());
focusedDate.setDate(nextYearDate.getDate());
displayedMonth.setYear(nextYearDate.getYear());
displayedMonth.setMonth(nextYearDate.getMonth());
} else {
int currentMonth = focusedDate.getMonth();
focusedDate.setYear(focusedDate.getYear() + years);
displayedMonth.setYear(displayedMonth.getYear() + years);
/*
* If the focused date was a leap day (Feb 29), the new date becomes
* Mar 1 if the new year is not also a leap year. Set it to Feb 28
* instead.
*/
if (focusedDate.getMonth() != currentMonth) {
focusedDate.setDate(0);
}
}
renderCalendar();
}
/**
* Handles a user click on the component
*
* @param sender
* The component that was clicked
* @param updateVariable
* Should the value field be updated
*
*/
private void processClickEvent(Widget sender) {
if (!isEnabled() || isReadonly()) {
return;
}
if (sender == prevYear) {
focusPreviousYear(1);
} else if (sender == nextYear) {
focusNextYear(1);
} else if (sender == prevMonth) {
focusPreviousMonth();
} else if (sender == nextMonth) {
focusNextMonth();
}
}
/*
* (non-Javadoc)
*
* @see
* com.google.gwt.event.dom.client.KeyDownHandler#onKeyDown(com.google.gwt
* .event.dom.client.KeyDownEvent)
*/
@Override
public void onKeyDown(KeyDownEvent event) {
handleKeyPress(event);
}
/*
* (non-Javadoc)
*
* @see
* com.google.gwt.event.dom.client.KeyPressHandler#onKeyPress(com.google
* .gwt.event.dom.client.KeyPressEvent)
*/
@Override
public void onKeyPress(KeyPressEvent event) {
handleKeyPress(event);
}
/**
* Handles the keypress from both the onKeyPress event and the onKeyDown
* event
*
* @param event
* The keydown/keypress event
*/
private void handleKeyPress(DomEvent> event) {
// Check tabs
int keycode = event.getNativeEvent().getKeyCode();
if (keycode == KeyCodes.KEY_TAB
&& event.getNativeEvent().getShiftKey()) {
if (onTabOut(event)) {
return;
}
}
// Handle the navigation
if (handleNavigation(keycode,
event.getNativeEvent().getCtrlKey()
|| event.getNativeEvent().getMetaKey(),
event.getNativeEvent().getShiftKey())) {
event.preventDefault();
}
}
/**
* Notifies submit-listeners of a submit event
*/
private void onSubmit() {
if (getSubmitListener() != null) {
getSubmitListener().onSubmit();
}
}
/**
* Notifies submit-listeners of a cancel event
*/
private void onCancel() {
if (getSubmitListener() != null) {
getSubmitListener().onCancel();
}
}
/**
* Handles the keyboard navigation when the resolution is set to years.
*
* @param keycode
* The keycode to process
* @param ctrl
* Is ctrl pressed?
* @param shift
* is shift pressed
* @return Returns true if the keycode was processed, else false
*/
protected boolean handleNavigationYearMode(int keycode, boolean ctrl,
boolean shift) {
// Ctrl and Shift selection not supported
if (ctrl || shift) {
return false;
}
else if (keycode == getPreviousKey()) {
focusNextYear(10); // Add 10 years
return true;
}
else if (keycode == getForwardKey()) {
focusNextYear(1); // Add 1 year
return true;
}
else if (keycode == getNextKey()) {
focusPreviousYear(10); // Subtract 10 years
return true;
}
else if (keycode == getBackwardKey()) {
focusPreviousYear(1); // Subtract 1 year
return true;
} else if (keycode == getSelectKey()) {
value = (Date) focusedDate.clone();
onSubmit();
return true;
} else if (keycode == getResetKey()) {
// Restore showing value the selected value
focusedDate.setTime(value.getTime());
renderCalendar();
return true;
} else if (keycode == getCloseKey()) {
// TODO fire listener, on users responsibility??
onCancel();
return true;
}
return false;
}
/**
* Handle the keyboard navigation when the resolution is set to MONTH
*
* @param keycode
* The keycode to handle
* @param ctrl
* Was the ctrl key pressed?
* @param shift
* Was the shift key pressed?
* @return
*/
protected boolean handleNavigationMonthMode(int keycode, boolean ctrl,
boolean shift) {
// Ctrl selection not supported
if (ctrl) {
return false;
} else if (keycode == getPreviousKey()) {
focusNextYear(1); // Add 1 year
return true;
} else if (keycode == getForwardKey()) {
focusNextMonth(); // Add 1 month
return true;
} else if (keycode == getNextKey()) {
focusPreviousYear(1); // Subtract 1 year
return true;
} else if (keycode == getBackwardKey()) {
focusPreviousMonth(); // Subtract 1 month
return true;
} else if (keycode == getSelectKey()) {
value = (Date) focusedDate.clone();
onSubmit();
return true;
} else if (keycode == getResetKey()) {
// Restore showing value the selected value
focusedDate.setTime(value.getTime());
renderCalendar();
return true;
} else if (keycode == getCloseKey() || keycode == KeyCodes.KEY_TAB) {
onCancel();
// TODO fire close event
return true;
}
return false;
}
/**
* Handle keyboard navigation what the resolution is set to DAY
*
* @param keycode
* The keycode to handle
* @param ctrl
* Was the ctrl key pressed?
* @param shift
* Was the shift key pressed?
* @return Return true if the key press was handled by the method, else
* return false.
*/
protected boolean handleNavigationDayMode(int keycode, boolean ctrl,
boolean shift) {
// Ctrl key is not in use
if (ctrl) {
return false;
}
/*
* Jumps to the next day.
*/
if (keycode == getForwardKey() && !shift) {
focusNextDay(1);
return true;
/*
* Jumps to the previous day
*/
} else if (keycode == getBackwardKey() && !shift) {
focusPreviousDay(1);
return true;
/*
* Jumps one week forward in the calendar
*/
} else if (keycode == getNextKey() && !shift) {
focusNextDay(7);
return true;
/*
* Jumps one week back in the calendar
*/
} else if (keycode == getPreviousKey() && !shift) {
focusPreviousDay(7);
return true;
/*
* Selects the value that is chosen
*/
} else if (keycode == getSelectKey() && !shift) {
selectFocused();
onSubmit(); // submit
return true;
} else if (keycode == getCloseKey()) {
onCancel();
// TODO close event
return true;
/*
* Jumps to the next month
*/
} else if (shift && keycode == getForwardKey()) {
focusNextMonth();
return true;
/*
* Jumps to the previous month
*/
} else if (shift && keycode == getBackwardKey()) {
focusPreviousMonth();
return true;
/*
* Jumps to the next year
*/
} else if (shift && keycode == getPreviousKey()) {
focusNextYear(1);
return true;
/*
* Jumps to the previous year
*/
} else if (shift && keycode == getNextKey()) {
focusPreviousYear(1);
return true;
/*
* Resets the selection
*/
} else if (keycode == getResetKey() && !shift) {
// Restore showing value the selected value
focusedDate = new FocusedDate(value.getYear(), value.getMonth(),
value.getDate());
displayedMonth = new FocusedDate(value.getYear(), value.getMonth(),
1);
renderCalendar();
return true;
}
return false;
}
/**
* Handles the keyboard navigation
*
* @param keycode
* The key code that was pressed
* @param ctrl
* Was the ctrl key pressed
* @param shift
* Was the shift key pressed
* @return Return true if key press was handled by the component, else
* return false
*/
protected boolean handleNavigation(int keycode, boolean ctrl,
boolean shift) {
if (!isEnabled() || isReadonly()) {
return false;
}
else if (isYear(getResolution())) {
return handleNavigationYearMode(keycode, ctrl, shift);
}
else if (isMonth(getResolution())) {
return handleNavigationMonthMode(keycode, ctrl, shift);
}
else if (isDay(getResolution())) {
return handleNavigationDayMode(keycode, ctrl, shift);
}
else {
return handleNavigationDayMode(keycode, ctrl, shift);
}
}
/**
* Returns the reset key which will reset the calendar to the previous
* selection. By default this is backspace but it can be overriden to change
* the key to whatever you want.
*
* @return
*/
protected int getResetKey() {
return KeyCodes.KEY_BACKSPACE;
}
/**
* Returns the select key which selects the value. By default this is the
* enter key but it can be changed to whatever you like by overriding this
* method.
*
* @return
*/
protected int getSelectKey() {
return KeyCodes.KEY_ENTER;
}
/**
* Returns the key that closes the popup window if this is a VPopopCalendar.
* Else this does nothing. By default this is the Escape key but you can
* change the key to whatever you want by overriding this method.
*
* @return
*/
protected int getCloseKey() {
return KeyCodes.KEY_ESCAPE;
}
/**
* The key that selects the next day in the calendar. By default this is the
* right arrow key but by overriding this method it can be changed to
* whatever you like.
*
* @return
*/
protected int getForwardKey() {
return KeyCodes.KEY_RIGHT;
}
/**
* The key that selects the previous day in the calendar. By default this is
* the left arrow key but by overriding this method it can be changed to
* whatever you like.
*
* @return
*/
protected int getBackwardKey() {
return KeyCodes.KEY_LEFT;
}
/**
* The key that selects the next week in the calendar. By default this is
* the down arrow key but by overriding this method it can be changed to
* whatever you like.
*
* @return
*/
protected int getNextKey() {
return KeyCodes.KEY_DOWN;
}
/**
* The key that selects the previous week in the calendar. By default this
* is the up arrow key but by overriding this method it can be changed to
* whatever you like.
*
* @return
*/
protected int getPreviousKey() {
return KeyCodes.KEY_UP;
}
/*
* (non-Javadoc)
*
* @see
* com.google.gwt.event.dom.client.MouseOutHandler#onMouseOut(com.google
* .gwt.event.dom.client.MouseOutEvent)
*/
@Override
public void onMouseOut(MouseOutEvent event) {
if (mouseTimer != null) {
mouseTimer.cancel();
}
}
/*
* (non-Javadoc)
*
* @see
* com.google.gwt.event.dom.client.MouseDownHandler#onMouseDown(com.google
* .gwt.event.dom.client.MouseDownEvent)
*/
@Override
public void onMouseDown(MouseDownEvent event) {
// Click-n-hold the left mouse button for fast-forward or fast-rewind.
// Timer is first used for a 500ms delay after mousedown. After that has
// elapsed, another timer is triggered to go off every 150ms. Both
// timers are cancelled on mouseup or mouseout.
if (event.getNativeButton() == NativeEvent.BUTTON_LEFT && event
.getSource() instanceof VAbstractCalendarPanel.VEventButton) {
final VEventButton sender = (VEventButton) event.getSource();
processClickEvent(sender);
mouseTimer = new Timer() {
@Override
public void run() {
mouseTimer = new Timer() {
@Override
public void run() {
processClickEvent(sender);
}
};
mouseTimer.scheduleRepeating(150);
}
};
mouseTimer.schedule(500);
}
}
/*
* (non-Javadoc)
*
* @see
* com.google.gwt.event.dom.client.MouseUpHandler#onMouseUp(com.google.gwt
* .event.dom.client.MouseUpEvent)
*/
@Override
public void onMouseUp(MouseUpEvent event) {
if (mouseTimer != null) {
mouseTimer.cancel();
}
}
/**
* Adjusts a date to fit inside the range, only if outside
*
* @param date
*/
private Date adjustDateToFitInsideRange(Date date) {
if (rangeStart != null && rangeStart.after(date)) {
date = (Date) rangeStart.clone();
} else if (rangeEnd != null && rangeEnd.before(date)) {
date = (Date) rangeEnd.clone();
}
return date;
}
/**
* Sets the data of the Panel.
*
* @param currentDate
* The date to set
*/
public void setDate(Date currentDate) {
doSetDate(currentDate, false, () -> {
});
}
/**
* The actual implementation of the logic which sets the data of the Panel.
* The method {@link #setDate(Date)} just delegate a call to this method
* providing additional config parameters.
*
* @param currentDate
* currentDate The date to set
* @param needRerender
* if {@code true} then calendar will be rerendered regardless of
* internal logic, otherwise the decision will be made on the
* internal state inside the method
* @param focusAction
* an additional action which will be executed in case
* rerendering is not required
*/
protected void doSetDate(Date currentDate, boolean needRerender,
Runnable focusAction) {
// Check that we are not re-rendering an already active date
if (currentDate == value && currentDate != null) {
return;
}
boolean currentDateWasAdjusted = false;
// Check that selected date is inside the allowed range
if (currentDate != null
&& !isDateInsideRange(currentDate, getResolution())) {
currentDate = adjustDateToFitInsideRange(currentDate);
currentDateWasAdjusted = true;
}
Date oldDisplayedMonth = displayedMonth;
value = currentDate;
// If current date was adjusted, we will not select any date,
// since that will look like a date is selected. Instead we
// only focus on the adjusted value
if (value == null || currentDateWasAdjusted) {
// If ranges enabled, we may need to focus on a different view to
// potentially not get stuck
if (rangeStart != null || rangeEnd != null) {
Date dateThatFitsInsideRange = adjustDateToFitInsideRange(
new Date());
focusedDate = new FocusedDate(dateThatFitsInsideRange.getYear(),
dateThatFitsInsideRange.getMonth(),
dateThatFitsInsideRange.getDate());
displayedMonth = new FocusedDate(
dateThatFitsInsideRange.getYear(),
dateThatFitsInsideRange.getMonth(), 1);
// value was adjusted. Set selected to null to not cause
// confusion, but this is only needed (and allowed) when we have
// a day
// resolution
if (isDay(getResolution())) {
value = null;
}
} else {
focusedDate = displayedMonth = null;
}
} else {
focusedDate = new FocusedDate(value.getYear(), value.getMonth(),
value.getDate());
displayedMonth = new FocusedDate(value.getYear(), value.getMonth(),
1);
}
// Re-render calendar if the displayed month is changed.
if (needRerender || oldDisplayedMonth == null || value == null
|| oldDisplayedMonth.getYear() != value.getYear()
|| oldDisplayedMonth.getMonth() != value.getMonth()) {
renderCalendar();
} else {
focusDay(focusedDate);
selectFocused();
focusAction.run();
}
if (!hasFocus) {
focusDay(null);
}
}
/**
* A widget representing a single day in the calendar panel.
*/
private class Day extends InlineHTML {
private final Date date;
Day(Date date) {
super("" + date.getDate());
this.date = date;
addClickHandler(dayClickHandler);
}
public Date getDate() {
return date;
}
}
public Date getDate() {
return value;
}
/**
* If true should be returned if the panel will not be used after this
* event.
*
* @param event
* @return
*/
protected boolean onTabOut(DomEvent> event) {
if (focusOutListener != null) {
return focusOutListener.onFocusOut(event);
}
return false;
}
/**
* A focus out listener is triggered when the panel loosed focus. This can
* happen either after a user clicks outside the panel or tabs out.
*
* @param listener
* The listener to trigger
*/
public void setFocusOutListener(FocusOutListener listener) {
focusOutListener = listener;
}
/**
* The submit listener is called when the user selects a value from the
* calender either by clicking the day or selects it by keyboard.
*
* @param submitListener
* The listener to trigger
*/
public void setSubmitListener(SubmitListener submitListener) {
this.submitListener = submitListener;
}
/**
* The given FocusChangeListener is notified when the focused date changes
* by user either clicking on a new date or by using the keyboard.
*
* @param listener
* The FocusChangeListener to be notified
*/
public void setFocusChangeListener(FocusChangeListener listener) {
focusChangeListener = listener;
}
/**
* Returns the submit listener that listens to selection made from the panel
*
* @return The listener or NULL if no listener has been set
*/
public SubmitListener getSubmitListener() {
return submitListener;
}
/*
* (non-Javadoc)
*
* @see
* com.google.gwt.event.dom.client.BlurHandler#onBlur(com.google.gwt.event
* .dom.client.BlurEvent)
*/
@Override
public void onBlur(final BlurEvent event) {
if (event.getSource() instanceof VAbstractCalendarPanel) {
hasFocus = false;
focusDay(null);
}
}
/*
* (non-Javadoc)
*
* @see
* com.google.gwt.event.dom.client.FocusHandler#onFocus(com.google.gwt.event
* .dom.client.FocusEvent)
*/
@Override
public void onFocus(FocusEvent event) {
if (event.getSource() instanceof VAbstractCalendarPanel) {
hasFocus = true;
// Focuses the current day if the calendar shows the days
if (focusedDay != null) {
focusDay(focusedDate);
}
}
}
private static final String SUBPART_NEXT_MONTH = "nextmon";
private static final String SUBPART_PREV_MONTH = "prevmon";
private static final String SUBPART_NEXT_YEAR = "nexty";
private static final String SUBPART_PREV_YEAR = "prevy";
private static final String SUBPART_HOUR_SELECT = "h";
private static final String SUBPART_MINUTE_SELECT = "m";
private static final String SUBPART_SECS_SELECT = "s";
private static final String SUBPART_AMPM_SELECT = "ampm";
private static final String SUBPART_DAY = "day";
private static final String SUBPART_MONTH_YEAR_HEADER = "header";
private Date rangeStart;
private Date rangeEnd;
@Override
public String getSubPartName(
com.google.gwt.user.client.Element subElement) {
if (contains(nextMonth, subElement)) {
return SUBPART_NEXT_MONTH;
} else if (contains(prevMonth, subElement)) {
return SUBPART_PREV_MONTH;
} else if (contains(nextYear, subElement)) {
return SUBPART_NEXT_YEAR;
} else if (contains(prevYear, subElement)) {
return SUBPART_PREV_YEAR;
} else if (contains(days, subElement)) {
// Day, find out which dayOfMonth and use that as the identifier
Day day = WidgetUtil.findWidget(subElement, Day.class);
if (day != null) {
Date date = day.getDate();
int id = date.getDate();
// Zero or negative ids map to days of the preceding month,
// past-the-end-of-month ids to days of the following month
if (date.getMonth() < displayedMonth.getMonth()) {
id -= DateTimeService.getNumberOfDaysInMonth(date);
} else if (date.getMonth() > displayedMonth.getMonth()) {
id += DateTimeService
.getNumberOfDaysInMonth(displayedMonth);
}
return SUBPART_DAY + id;
}
} else if (getCellFormatter().getElement(0, 2)
.isOrHasChild(subElement)) {
return SUBPART_MONTH_YEAR_HEADER;
}
return null;
}
/**
* Checks if subElement is inside the widget DOM hierarchy.
*
* @param w
* @param subElement
* @return true if {@code w} is a parent of subElement, false otherwise.
*/
protected boolean contains(Widget w, Element subElement) {
if (w == null || w.getElement() == null) {
return false;
}
return w.getElement().isOrHasChild(subElement);
}
@Override
public com.google.gwt.user.client.Element getSubPartElement(
String subPart) {
if (SUBPART_NEXT_MONTH.equals(subPart)) {
return nextMonth.getElement();
}
if (SUBPART_PREV_MONTH.equals(subPart)) {
return prevMonth.getElement();
}
if (SUBPART_NEXT_YEAR.equals(subPart)) {
return nextYear.getElement();
}
if (SUBPART_PREV_YEAR.equals(subPart)) {
return prevYear.getElement();
}
if (subPart.startsWith(SUBPART_DAY)) {
// Zero or negative ids map to days in the preceding month,
// past-the-end-of-month ids to days in the following month
int dayOfMonth = Integer
.parseInt(subPart.substring(SUBPART_DAY.length()));
Date date = new Date(displayedMonth.getYear(),
displayedMonth.getMonth(), dayOfMonth);
Iterator iter = days.iterator();
while (iter.hasNext()) {
Widget w = iter.next();
if (w instanceof VAbstractCalendarPanel.Day) {
Day day = (Day) w;
if (day.getDate().equals(date)) {
return day.getElement();
}
}
}
}
if (SUBPART_MONTH_YEAR_HEADER.equals(subPart)) {
return DOM.asOld(
(Element) getCellFormatter().getElement(0, 2).getChild(0));
}
return null;
}
@Override
protected void onDetach() {
super.onDetach();
if (mouseTimer != null) {
mouseTimer.cancel();
}
}
/**
* Helper class to inform the screen reader that the user changed the
* selected date. It sets the value of a field that is outside the view, and
* is defined as a live area. That way the screen reader recognizes the
* change and reads it to the user.
*/
public class FocusedDate extends Date {
public FocusedDate(int year, int month, int date) {
super(year, month, date);
}
@Override
public void setTime(long time) {
super.setTime(time);
setLabel();
}
@Override
@Deprecated
public void setDate(int date) {
super.setDate(date);
setLabel();
}
@Override
@Deprecated
public void setMonth(int month) {
super.setMonth(month);
setLabel();
}
@Override
@Deprecated
public void setYear(int year) {
super.setYear(year);
setLabel();
}
private void setLabel() {
if (getDateField() instanceof VAbstractPopupCalendar) {
((VAbstractPopupCalendar) getDateField()).setFocusedDate(this);
}
}
}
/**
* Sets the start range for this component. The start range is inclusive,
* and it depends on the current resolution, what is considered inside the
* range.
*
* @param startDate
* - the allowed range's start date
*/
public void setRangeStart(Date newRangeStart) {
if (!SharedUtil.equals(rangeStart, newRangeStart)) {
rangeStart = newRangeStart;
if (initialRenderDone) {
// Dynamic updates to the range needs to render the calendar to
// update the element stylenames
renderCalendar();
}
}
}
/**
* Sets the end range for this component. The end range is inclusive, and it
* depends on the current resolution, what is considered inside the range.
*
* @param endDate
* - the allowed range's end date
*/
public void setRangeEnd(Date newRangeEnd) {
if (!SharedUtil.equals(rangeEnd, newRangeEnd)) {
rangeEnd = newRangeEnd;
if (initialRenderDone) {
// Dynamic updates to the range needs to render the calendar to
// update the element stylenames
renderCalendar();
}
}
}
}