From 7be8ef7e6cd9724a552dd5ae35912e7eb3cf73dd Mon Sep 17 00:00:00 2001 From: stroomdev66 Date: Tue, 27 Feb 2024 22:42:01 +0000 Subject: [PATCH] #4041 Improve date picker --- ... => V07_04_00_001__execution_schedule.sql} | 0 .../src/main/resources/stroom/app/App.gwt.xml | 1 + .../resources/ui/css/CustomDatePicker.css | 147 ++ stroom-app/src/main/resources/ui/css/app.css | 1 + .../src/main/resources/ui/images/calendar.svg | 1 + .../main/resources/ui/raw-images/calendar.svg | 1 + .../datepicker/client/AbstractCell.java | 23 + .../datepicker/client/CalendarModel.java | 365 +++ .../datepicker/client/CalendarUtil.java | 170 ++ .../datepicker/client/CalendarView.java | 88 + .../datepicker/client/CustomDatePicker.java | 759 ++++++ .../widget/datepicker/client/DateCell.java | 134 + .../datepicker/client/DateChangeEvent.java | 59 + .../widget/datepicker/client/DateGrid.java | 128 + .../client/DatePickerComponent.java | 69 + .../widget/datepicker/client/DateRecord.java | 353 +++ .../datepicker/client/DateTimeConstants.java | 70 + .../datepicker/client/DateTimeFormat.java | 19 + .../datepicker/client/DateTimeFormatImpl.java | 2176 +++++++++++++++++ .../widget/datepicker/client/DayLabel.java | 8 + .../client/DefaultCalendarView.java | 197 ++ .../client/DefaultMonthSelector.java | 271 ++ .../datepicker/client/IntlDateTimeFormat.java | 780 ++++++ .../widget/datepicker/client/JsDate.java | 689 ++++++ .../datepicker/client/MonthSelector.java | 24 + .../widget/datepicker/client/TimeZone.java | 275 +++ .../widget/datepicker/DatePicker.gwt.xml | 7 + .../ExecutionScheduleEditPresenter.java | 92 +- .../presenter/ExecutionScheduleEditView.java | 6 +- .../view/ExecutionScheduleEditViewImpl.java | 21 +- .../job/client/presenter/DateTimeBox.java | 178 ++ .../job/client/presenter/DateTimePopup.java | 71 + .../presenter/JobNodeListPresenter.java | 4 +- .../job/client/presenter/ScheduleBox.java | 12 +- ...edulePresenter.java => SchedulePopup.java} | 12 +- .../job/client/view/DateTimeViewImpl.java | 333 +++ .../job/client/view/ScheduleViewImpl.java | 2 +- .../client/gin/MonitoringModule.java | 13 +- .../preferences/client/DateTimeFormatter.java | 21 +- .../view/ExecutionScheduleEditViewImpl.ui.xml | 8 +- .../job/client/view/DateTimeViewImpl.ui.xml | 36 + .../main/java/stroom/svg/shared/SvgImage.java | 28 + .../resources/SvgImageTools/themedIcons.html | 9 + .../stroom/dashboard/DashboardApp.gwt.xml | 1 + .../20240227_224134_784__4041.md | 24 + 45 files changed, 7609 insertions(+), 77 deletions(-) rename stroom-analytics/stroom-analytics-impl-db/src/main/resources/stroom/analytics/impl/db/migration/{V07_03_00_001__execution_schedule.sql => V07_04_00_001__execution_schedule.sql} (100%) create mode 100644 stroom-app/src/main/resources/ui/css/CustomDatePicker.css create mode 100644 stroom-app/src/main/resources/ui/images/calendar.svg create mode 100644 stroom-app/src/main/resources/ui/raw-images/calendar.svg create mode 100644 stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/AbstractCell.java create mode 100644 stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/CalendarModel.java create mode 100644 stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/CalendarUtil.java create mode 100644 stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/CalendarView.java create mode 100644 stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/CustomDatePicker.java create mode 100644 stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/DateCell.java create mode 100644 stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/DateChangeEvent.java create mode 100644 stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/DateGrid.java create mode 100644 stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/DatePickerComponent.java create mode 100644 stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/DateRecord.java create mode 100644 stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/DateTimeConstants.java create mode 100644 stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/DateTimeFormat.java create mode 100644 stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/DateTimeFormatImpl.java create mode 100644 stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/DayLabel.java create mode 100644 stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/DefaultCalendarView.java create mode 100644 stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/DefaultMonthSelector.java create mode 100644 stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/IntlDateTimeFormat.java create mode 100644 stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/JsDate.java create mode 100644 stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/MonthSelector.java create mode 100644 stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/TimeZone.java create mode 100644 stroom-core-client-widget/src/main/resources/stroom/widget/datepicker/DatePicker.gwt.xml create mode 100644 stroom-core-client/src/main/java/stroom/job/client/presenter/DateTimeBox.java create mode 100644 stroom-core-client/src/main/java/stroom/job/client/presenter/DateTimePopup.java rename stroom-core-client/src/main/java/stroom/job/client/presenter/{SchedulePresenter.java => SchedulePopup.java} (95%) create mode 100644 stroom-core-client/src/main/java/stroom/job/client/view/DateTimeViewImpl.java create mode 100644 stroom-core-client/src/main/resources/stroom/job/client/view/DateTimeViewImpl.ui.xml create mode 100644 unreleased_changes/20240227_224134_784__4041.md diff --git a/stroom-analytics/stroom-analytics-impl-db/src/main/resources/stroom/analytics/impl/db/migration/V07_03_00_001__execution_schedule.sql b/stroom-analytics/stroom-analytics-impl-db/src/main/resources/stroom/analytics/impl/db/migration/V07_04_00_001__execution_schedule.sql similarity index 100% rename from stroom-analytics/stroom-analytics-impl-db/src/main/resources/stroom/analytics/impl/db/migration/V07_03_00_001__execution_schedule.sql rename to stroom-analytics/stroom-analytics-impl-db/src/main/resources/stroom/analytics/impl/db/migration/V07_04_00_001__execution_schedule.sql diff --git a/stroom-app-gwt/src/main/resources/stroom/app/App.gwt.xml b/stroom-app-gwt/src/main/resources/stroom/app/App.gwt.xml index 1e4c3fe48af..378eb5957ab 100644 --- a/stroom-app-gwt/src/main/resources/stroom/app/App.gwt.xml +++ b/stroom-app-gwt/src/main/resources/stroom/app/App.gwt.xml @@ -40,6 +40,7 @@ + diff --git a/stroom-app/src/main/resources/ui/css/CustomDatePicker.css b/stroom-app/src/main/resources/ui/css/CustomDatePicker.css new file mode 100644 index 00000000000..397ee904e0a --- /dev/null +++ b/stroom-app/src/main/resources/ui/css/CustomDatePicker.css @@ -0,0 +1,147 @@ +.CustomDatePicker { +} +.CustomDatePicker .gwt-ListBox { + border: none; + padding: 2px; +} +.CustomDatePicker .form-label { + margin-top: 0.25rem; +} +.CustomDatePicker td, +.customDatePickerMonthSelector td:focus { + outline: none +} + +.CustomDatePicker .cellOuter { + width: 30px; + height: 30px; + display: flex; + align-items: center; +} +.CustomDatePicker .cellInner { + display: flex; + align-items: center; + flex-direction: column; + width: 100%; +} + +.customDatePickerDay, +.customDatePickerWeekdayLabel, +.customDatePickerWeekendLabel { + outline: none; + color: var(--text-color); +} +.customDatePickerWeekdayLabel, +.customDatePickerWeekendLabel { + cursor: default; +} +.customDatePickerDay { + padding: 4px; + cursor: pointer; + border-radius: 2rem; + border: 1px solid transparent; +} +.customDatePickerDayIsToday { + color: var(--text-color); + border: 1px solid var(--row__background-color--selected); +} +.customDatePickerDayIsWeekend { +} +.customDatePickerDayIsFiller { + color: var(--text-color--disabled); + /*color: transparent;*/ +} +.customDatePickerDayIsValue { + background: var(--row__background-color--selected); + border: 1px solid var(--row__background-color--selected); + color: var(--row__text-color--selected); +} +.customDatePickerDayIsDisabled { + color: var(--text-color--disabled); +} + +td:hover .customDatePickerDay { + background: var(--row__background-color--hovered); +} +td:hover .customDatePickerDayIsValue { + background: var(--row__background-color--selected--hovered) !important; + border: 1px solid var(--row__background-color--selected--hovered) !important; + color: var(--row__text-color--selected) !important; +} + +/* +.customDatePickerDayIsHighlighted { + background: var(--row__background-color--hovered); +} +.customDatePickerDayIsValueAndHighlighted { + background: var(--row__background-color--selected--hovered) !important; + border: 1px solid var(--row__background-color--selected--hovered) !important; + color: var(--row__text-color--selected) !important; +} +.customDatePickerDayIsValue.customDatePickerDayIsHighlighted { + background: var(--row__background-color--hovered); + color: var(--text-color); +} +*/ + +.customDatePickerMonthSelector { +} +.customDatePickerYear, +.customDatePickerMonth { + display: flex; + align-items: center; + + gap: 4px; + padding: 0 2px; +} +.customDatePickerYear .icon-button, +.customDatePickerMonth .icon-button { + width: 26px; + height: 26px; +} +.customDatePickerPreviousButton svg, +.customDatePickerNextButton svg, +.customDatePickerPreviousYearButton svg, +.customDatePickerNextYearButton svg { + color: var(--navigation__text-color); + width: 10px; + height: 10px; +} + + + + + + +.customDatePickerDay,.customDatePickerWeekdayLabel,.customDatePickerWeekendLabel { + +} + +.DateTimeView-heading { + color: var(--heading-color); + font-weight: bold; + font-size: 125%; + padding-bottom: 0.3em; +} + +.DateTimeView-right { + padding-left: 1rem; +} + +.customDatePickerMonth .SelectionBox, +.customDatePickerYear .SelectionBox { + flex-grow: 1; + width: 5px; +} + +.customDatePickerMonth .SelectionBox input, +.customDatePickerYear .SelectionBox input { + min-width: 5px; +} + +.DateTimeView-date, +.DateTimeView-time { + padding: 5px 0 10px 0; +} + + diff --git a/stroom-app/src/main/resources/ui/css/app.css b/stroom-app/src/main/resources/ui/css/app.css index ae48860d0c6..1ff7750d3ba 100644 --- a/stroom-app/src/main/resources/ui/css/app.css +++ b/stroom-app/src/main/resources/ui/css/app.css @@ -46,6 +46,7 @@ @import url("celltable/ExplorerTree.css"); @import url("celltable/Menu.css"); @import url("celltable/DataGrid.css"); +@import url("CustomDatePicker.css"); @import url("ScheduleBox.css"); @import url("SelectionBox.css"); @import url("QuickFilter.css"); diff --git a/stroom-app/src/main/resources/ui/images/calendar.svg b/stroom-app/src/main/resources/ui/images/calendar.svg new file mode 100644 index 00000000000..217bdeda8b8 --- /dev/null +++ b/stroom-app/src/main/resources/ui/images/calendar.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/stroom-app/src/main/resources/ui/raw-images/calendar.svg b/stroom-app/src/main/resources/ui/raw-images/calendar.svg new file mode 100644 index 00000000000..0aca8124438 --- /dev/null +++ b/stroom-app/src/main/resources/ui/raw-images/calendar.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/AbstractCell.java b/stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/AbstractCell.java new file mode 100644 index 00000000000..e6935aa2f58 --- /dev/null +++ b/stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/AbstractCell.java @@ -0,0 +1,23 @@ +package stroom.widget.datepicker.client; + +import com.google.gwt.dom.client.Element; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.ui.Widget; + +public abstract class AbstractCell extends Widget { + + private final Element cellInner; + + public AbstractCell() { + final Element cellOuter = DOM.createDiv(); + cellOuter.setClassName("cellOuter"); + cellInner = DOM.createDiv(); + cellInner.setClassName("cellInner"); + DOM.appendChild(cellOuter, cellInner); + setElement(cellOuter); + } + + protected void setText(String value) { + cellInner.setInnerText(value); + } +} diff --git a/stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/CalendarModel.java b/stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/CalendarModel.java new file mode 100644 index 00000000000..1be66779511 --- /dev/null +++ b/stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/CalendarModel.java @@ -0,0 +1,365 @@ +/* + * Copyright 2008 Google Inc. + * + * 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 stroom.widget.datepicker.client; + +/** + * Model used to get calendar information for {@link CustomDatePicker} and its + * subclasses. + */ +public class CalendarModel { + + private static final DateTimeConstants DATE_TIME_CONSTANTS = new DateTimeConstants(); + + /** + * The number of weeks normally displayed in a month. + */ + public static final int WEEKS_IN_MONTH = 6; + + /** + * Number of days normally displayed in a week. + */ + public static final int DAYS_IN_WEEK = 7; + + public static final int MONTHS_IN_YEAR = 12; + + private static final int MAX_DAYS_IN_MONTH = 31; + + private final String[] dayOfWeekNames = DATE_TIME_CONSTANTS.getDayOfWeekNames(); + private final String[] dayOfMonthNames = DATE_TIME_CONSTANTS.getDayOfMonthNames(); + private final String[] monthOfYearNames = DATE_TIME_CONSTANTS.getMonthNames(); + private final JsDate currentMonth; + + /** + * Constructor. + */ + public CalendarModel() { + currentMonth = getFirstDayOfMonth(JsDate.create()); + +// // Finding day of week names +// dayOfWeekNames = new String[DAYS_IN_WEEK]; +// for (int i = 1; i <= DAYS_IN_WEEK; i++) { +// final JsDate date = JsDate.create(currentMonth.getFullYear(), currentMonth.getMonth(), i); +// final int dayOfWeek = date.getDay(); +// dayOfWeekNames[dayOfWeek] = getDayOfWeekFormatter().format(date); +// } +// +// // Finding day of month names +// dayOfMonthNames = new String[32]; +// for (int i = 1; i <= MAX_DAYS_IN_MONTH; i++) { +// final JsDate date = JsDate.create(currentMonth.getFullYear(), 0, i); +// dayOfMonthNames[i] = getDayOfMonthFormatter().format(date); +// } +// +// // Finding month names +// monthOfYearNames = new String[MONTHS_IN_YEAR]; +// for (int i = 0; i < MONTHS_IN_YEAR; i++) { +// final JsDate date = JsDate.create(currentMonth.getFullYear(), i); +// monthOfYearNames[i] = getMonthFormatter().format(date); +// } + } + + public static JsDate getFirstDayOfMonth(JsDate date) { + return JsDate.create(date.getFullYear(), date.getMonth()); + } + +// /** +// * Format the current month and year in the current locale. For example, "Jan 2013" in English. +// * +// * @return the formatted month and year +// */ +// public String formatCurrentMonthAndYear() { +// return getMonthAndYearFormatter().format(getCurrentMonth()); +// } +// +// /** +// * Formats the current specified year. For example "2012". +// * +// * @return the formatted year +// */ +// public String formatCurrentYear() { +// return getYearFormatter().format(getCurrentMonth()); +// } + + /** + * Formats a date's day of month. For example "1". + * + * @param date the date + * @return the formated day of month + */ + public String formatDayOfMonth(JsDate date) { + return dayOfMonthNames[date.getDate()]; + } + + /** + * Format a day in the week. So, for example "Monday". + * + * @param dayInWeek the day in week to format + * @return the formatted day in week + */ + public String formatDayOfWeek(int dayInWeek) { + return dayOfWeekNames[dayInWeek]; + } + + /** + * Format a month in the year. So, for example "January". + * + * @param month A number from 0 (for January) to 11 (for December) identifying the month wanted. + * @return the formatted month + */ + public String formatMonth(int month) { + return monthOfYearNames[month]; + } + + /** + * Gets the first day of the first week in the currently specified month. + * + * @return the first day + */ + public JsDate getCurrentFirstDayOfFirstWeek() { + int wkDayOfMonth1st = currentMonth.getDay(); + int start = CalendarUtil.getStartingDayOfWeek(); + if (wkDayOfMonth1st == start) { + // always return a copy to allow SimpleCalendarView to adjust first + // display date + return JsDate.create(currentMonth.getTime()); + } else { + JsDate d = JsDate.create(currentMonth.getTime()); + int offset = wkDayOfMonth1st - start > 0 + ? wkDayOfMonth1st - start + : DAYS_IN_WEEK - (start - wkDayOfMonth1st); + CalendarUtil.addDaysToDate(d, -offset); + return d; + } + } + + /** + * Gets the date representation of the currently specified month. Used to + * access both the month and year information. + * + * @return the month and year + */ + public JsDate getCurrentMonth() { + return currentMonth; + } + + /** + * Is a date in the currently specified month? + * + * @param date the date + * @return date + */ + public boolean isInCurrentMonth(JsDate date) { + return currentMonth.getMonth() == date.getMonth(); + } + + /** + * Sets the currently specified date. + * + * @param currentDate the currently specified date + */ + public void setCurrentMonth(JsDate currentDate) { + this.currentMonth.setFullYear(currentDate.getFullYear()); + this.currentMonth.setMonth(currentDate.getMonth()); + } + + /** + * Shifts the currently specified date by the given number of months. The day + * of the month will be pinned to the original value as far as possible. + * + * @param deltaMonths - number of months to be added to the current date + */ + public void shiftCurrentMonth(int deltaMonths) { + CalendarUtil.addMonthsToDate(currentMonth, deltaMonths); + refresh(); + } + +// /** +// * Gets the date of month formatter. +// * +// * @return the day of month formatter +// */ +// protected DateTimeFormat getDayOfMonthFormatter() { +// return DateTimeFormatImpl.getFormat("d"); +// } +// +// /** +// * Gets the day of week formatter. +// * +// * @return the day of week formatter +// */ +// protected DateTimeFormat getDayOfWeekFormatter() { +// return DateTimeFormatImpl.getFormat("ccccc"); +// } +// +// /** +// * Gets the month and year formatter. +// * +// * @return the month and year formatter +// */ +// protected DateTimeFormat getMonthAndYearFormatter() { +// return DateTimeFormatImpl.getFormat(DateTimeFormatImpl.PredefinedFormat.YEAR_MONTH_ABBR); +// } +// +// /** +// * Gets the month formatter. +// * +// * @return the month formatter +// */ +// protected DateTimeFormat getMonthFormatter() { +// return DateTimeFormatImpl.getFormat(DateTimeFormatImpl.PredefinedFormat.MONTH_ABBR); +// } +// +// /** +// * Gets the year formatter. +// * +// * @return the year formatter +// */ +// protected DateTimeFormat getYearFormatter() { +// return DateTimeFormatImpl.getFormat(DateTimeFormatImpl.PredefinedFormat.YEAR); +// } +// +// /** +// * Returns {@code true} if the month is before year in the date formatter in current locale. +// */ +// protected boolean isMonthBeforeYear() { +// String monthAndYearPattern = getMonthAndYearFormatter().getPattern(); +// +// for (int i = 0; i < monthAndYearPattern.length(); ++i) { +// switch (monthAndYearPattern.charAt(i)) { +// case 'y': +// return false; +// case 'M': +// case 'L': +// return true; +// } +// } +// +// return true; +// } +// +// +// +// +// /** +// * Formats a date's day of month. For example "1". +// * +// * @param date the date +// * @return the formated day of month +// */ +// public String formatDayOfMonth(JsDate date) { +// return dayOfMonthNames[date.getDate()]; +// } +// +// /** +// * Format a day in the week. So, for example "Monday". +// * +// * @param dayInWeek the day in week to format +// * @return the formatted day in week +// */ +// public String formatDayOfWeek(int dayInWeek) { +// return dayOfWeekNames[dayInWeek]; +// } +// +// /** +// * Gets the first day of the first week in the currently specified month. +// * +// * @return the first day +// */ +// public JsDate getCurrentFirstDayOfFirstWeek() { +// int wkDayOfMonth1st = currentMonth.getDay(); +// int start = CalendarUtil.getStartingDayOfWeek(); +// if (wkDayOfMonth1st == start) { +// // Always return a copy to allow SimpleCalendarView to adjust first +// // display date +// return JsDate.create(currentMonth.getTime()); +// } else { +// JsDate d = JsDate.create(currentMonth.getTime()); +// int offset = wkDayOfMonth1st - start > 0 +// ? wkDayOfMonth1st - start +// : DAYS_IN_WEEK - (start - wkDayOfMonth1st); +// CalendarUtil.addDaysToDate(d, -offset); +// return d; +// } +// } +// +// /** +// * Gets the date representation of the currently specified month. Used to +// * access both the month and year information. +// * +// * @return the month and year +// */ +// public JsDate getCurrentMonth() { +// return currentMonth; +// } +// +// /** +// * Sets the currently specified date. +// * +// * @param currentDate the currently specified date +// */ +// public void setCurrentMonth(JsDate currentDate) { +// this.currentMonth.setFullYear(currentDate.getFullYear()); +// this.currentMonth.setMonth(currentDate.getMonth()); +// } +// +// /** +// * Is a date in the currently specified month? +// * +// * @param date the date +// * @return date +// */ +// public boolean isInCurrentMonth(JsDate date) { +// return currentMonth.getMonth() == date.getMonth(); +// } +// +// /** +// * Shifts the currently specified date by the given number of months. The +// * day of the month will be pinned to the original value as far as possible. +// * +// * @param deltaMonths - number of months to be added to the current date +// */ +// public void shiftCurrentMonth(int deltaMonths) { +// CalendarUtil.addMonthsToDate(currentMonth, deltaMonths); +// refresh(); +// } +// +// /** +// * Gets the date of month formatter. +// * +// * @return the day of month formatter +// */ +// protected DateTimeFormat getDayOfMonthFormatter() { +// return DateTimeFormat.getFormat("d"); +// } +// +// /** +// * Gets the day of week formatter. +// * +// * @return the day of week formatter +// */ +// protected DateTimeFormat getDayOfWeekFormatter() { +// return DateTimeFormat.getFormat("ccccc"); +// } +// + + + /** + * Refresh the current model as needed. + */ + protected void refresh() { + } +} diff --git a/stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/CalendarUtil.java b/stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/CalendarUtil.java new file mode 100644 index 00000000000..3510ab8a0af --- /dev/null +++ b/stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/CalendarUtil.java @@ -0,0 +1,170 @@ +/* + * Copyright 2008 Google Inc. + * + * 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 stroom.widget.datepicker.client; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.i18n.client.DateTimeFormatInfo; +import com.google.gwt.i18n.client.LocaleInfo; + +/** + * Useful utilities for creating views of a calendar. + */ +public class CalendarUtil { + + private static int firstDayOfWeekend; + private static int lastDayOfWeekend; + private static int startingDay; + + static { + if (GWT.isClient()) { + DateTimeFormatInfo dateTimeFormatInfo = LocaleInfo.getCurrentLocale().getDateTimeFormatInfo(); + // Finding the start and end of weekend + firstDayOfWeekend = dateTimeFormatInfo.weekendStart(); + lastDayOfWeekend = dateTimeFormatInfo.weekendEnd(); + startingDay = dateTimeFormatInfo.firstDayOfTheWeek(); + } + } + + /** + * Adds the given number of days to a date. + * + * @param date the date + * @param days number of days + */ + public static void addDaysToDate(JsDate date, int days) { + date.setDate(date.getDate() + days); + } + + /** + * Adds the given number of months to a date. + * + * @param date the date + * @param months number of months + */ + public static void addMonthsToDate(JsDate date, int months) { + if (months != 0) { + date.setMonth(date.getMonth() + months); + + +// int month = date.getMonth(); +// int year = date.getFullYear(); +// +// int resultMonthCount = year * 12 + month + months; +// int resultYear = (int) Math.floor(resultMonthCount / 12.0); +// int resultMonth = resultMonthCount - resultYear * 12; +// +// date.setMonth(resultMonth); +// date.setFullYear(resultYear); + } + } + + /** + * Copies a date. + * + * @param date the date + * @return the copy + */ + public static JsDate copyDate(JsDate date) { + if (date == null) { + return null; + } + return JsDate.create(date.getTime()); + } + + /** + * Returns the number of days between the two dates. Time is ignored. + * + * @param start starting date + * @param finish ending date + * @return the different + */ + public static int getDaysBetween(JsDate start, JsDate finish) { + // Convert the dates to the same time + start = copyDate(start); + resetTime(start); + finish = copyDate(finish); + resetTime(finish); + + double aTime = start.getTime(); + double bTime = finish.getTime(); + + long adjust = 60 * 60 * 1000; + adjust = (bTime > aTime) ? adjust : -adjust; + + return (int) ((bTime - aTime + adjust) / (24 * 60 * 60 * 1000)); + } + + /** + * Returns the day of the week on which week starts in the current locale. The + * range between 0 for Sunday and 6 for Saturday. + * + * @return the day of the week + */ + public static int getStartingDayOfWeek() { + return startingDay; + } + + /** + * Check if two dates represent the same date of the same year, even if they + * have different times. + * + * @param date0 a date + * @param date1 a second date + * @return true if the dates are the same + */ + public static boolean isSameDate(JsDate date0, JsDate date1) { + assert date0 != null : "date0 cannot be null"; + assert date1 != null : "date1 cannot be null"; + return date0.getFullYear() == date1.getFullYear() + && date0.getMonth() == date1.getMonth() + && date0.getDate() == date1.getDate(); + } + + /** + * Sets a date object to be at the beginning of the month and no time + * specified. + * + * @param date the date + */ + public static void setToFirstDayOfMonth(JsDate date) { + resetTime(date); + date.setDate(1); + } + + /** + * Is a day in the week a weekend? + * + * @param dayOfWeek day of week + * @return is the day of week a weekend? + */ + static boolean isWeekend(int dayOfWeek) { + return dayOfWeek == firstDayOfWeekend || dayOfWeek == lastDayOfWeekend; + } + + /** + * Resets the date to have no time modifiers. Note that the hour might not be zero if the time + * hits a DST transition date. + * + * @param date the date + */ + public static void resetTime(JsDate date) { + date.setHours(0); + date.setMinutes(0); + date.setSeconds(0); + date.setMilliseconds(0); + } +} diff --git a/stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/CalendarView.java b/stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/CalendarView.java new file mode 100644 index 00000000000..7ca7e293013 --- /dev/null +++ b/stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/CalendarView.java @@ -0,0 +1,88 @@ +/* + * Copyright 2008 Google Inc. + * + * 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 stroom.widget.datepicker.client; + +/** + * The CalendarView is a calendar grid that represents the current view of a + * {@link CustomDatePicker}. Note, the calendar view only deals with the currently + * visible dates and all state is flushed when the calendar view is refreshed. + */ +public abstract class CalendarView extends DatePickerComponent { + + /** + * Constructor. + */ + public CalendarView() { + } + + /** + * Adds a style name to the cell of the supplied date. This style is only set + * until the next time the {@link CalendarView} is refreshed. + * + * @param styleName style name to add + * @param date date that will have the supplied style added + */ + public abstract void addStyleToDate(String styleName, JsDate date); + + /** + * Returns the first date that is currently shown by the calendar. + * + * @return the first date. + */ + public abstract JsDate getFirstDate(); + + /** + * Returns the last date that is currently shown by the calendar. + * + * @return the last date. + */ + public abstract JsDate getLastDate(); + + /** + * Is the cell representing the given date enabled? + * + * @param date the date + * @return is the date enabled + */ + public abstract boolean isDateEnabled(JsDate date); + + /** + * Removes a visible style name from the cell of the supplied date. + * + * @param styleName style name to remove + * @param date date that will have the supplied style added + */ + public abstract void removeStyleFromDate(String styleName, JsDate date); + + /** + * Sets aria-selected in the given date's cell and clears the other cells. + * + * @param date the date of the cell where aria-selected should be set, + * or null to clear aria-selected. + */ + public void setAriaSelectedCell(JsDate date) { + } + + /** + * Enables or Disables a particular date. by default all valid dates are + * enabled after a rendering event. Disabled dates cannot be selected. + * + * @param enabled true for enabled, false for disabled + * @param date date to enable or disable + */ + public abstract void setEnabledOnDate(boolean enabled, JsDate date); +} diff --git a/stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/CustomDatePicker.java b/stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/CustomDatePicker.java new file mode 100644 index 00000000000..cbc4b0b14ae --- /dev/null +++ b/stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/CustomDatePicker.java @@ -0,0 +1,759 @@ +/* + * Copyright 2008 Google Inc. + * + * 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 stroom.widget.datepicker.client; + +import com.google.gwt.editor.client.IsEditor; +import com.google.gwt.editor.client.LeafValueEditor; +import com.google.gwt.editor.client.adapters.TakesValueEditor; +import com.google.gwt.event.logical.shared.HasHighlightHandlers; +import com.google.gwt.event.logical.shared.HasShowRangeHandlers; +import com.google.gwt.event.logical.shared.HighlightEvent; +import com.google.gwt.event.logical.shared.HighlightHandler; +import com.google.gwt.event.logical.shared.ShowRangeEvent; +import com.google.gwt.event.logical.shared.ShowRangeHandler; +import com.google.gwt.event.logical.shared.ValueChangeEvent; +import com.google.gwt.event.logical.shared.ValueChangeHandler; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.user.client.ui.Composite; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.HasValue; + +import java.util.HashMap; +import java.util.Map; + +/** + * Standard GWT date picker. + * + *

CSS Style Rules

+ * + *
    + * + *
  • .gwt-DatePicker { }
  • + * + *
  • .datePickerMonthSelector { the month selector widget }
  • + * + *
  • .datePickerMonth { the month in the month selector widget }
  • + * + *
  • .datePickerYear { the year in the month selector widget }
  • + * + *
  • .datePickerPreviousButton { the previous month button }
  • + * + *
  • .datePickerNextButton { the next month button }
  • + * + *
  • .datePickerPreviousYearButton { the previous year button }
  • + * + *
  • .datePickerNextYearButton { the next year button }
  • + * + *
  • .datePickerDays { the portion of the picker that shows the days }
  • + * + *
  • .datePickerWeekdayLabel { the label over weekdays }
  • + * + *
  • .datePickerWeekendLabel { the label over weekends }
  • + * + *
  • .datePickerDay { a single day }
  • + * + *
  • .datePickerDayIsToday { today's date }
  • + * + *
  • .datePickerDayIsWeekend { a weekend day }
  • + * + *
  • .datePickerDayIsFiller { a day in another month }
  • + * + *
  • .datePickerDayIsValue { the selected day }
  • + * + *
  • .datePickerDayIsDisabled { a disabled day }
  • + * + *
  • .datePickerDayIsHighlighted { the currently highlighted day }
  • + * + *
  • .datePickerDayIsValueAndHighlighted { the highlighted day if it is also + * selected }
  • + * + *
+ * + *

+ *

Example

+ * {@example com.google.gwt.examples.DatePickerExample} + *

+ */ +public class CustomDatePicker extends Composite implements + HasHighlightHandlers, HasShowRangeHandlers, HasValue, + IsEditor> { + + /** + * Convenience class to group css style names. + */ + static class StandardCss { + + static StandardCss DEFAULT = new StandardCss("CustomDatePicker", "customDatePicker"); + + private String baseName; + private String widgetName; + + public StandardCss(String widgetName, String baseName) { + this.widgetName = widgetName; + this.baseName = baseName; + } + + public String datePicker() { + return getWidgetStyleName(); + } + + public String day() { + return wrap("Day"); + } + + public String day(String dayModifier) { + return day() + "Is" + dayModifier; + } + + public String dayIsDisabled() { + return day("Disabled"); + } + + public String dayIsFiller() { + return day("Filler"); + } + + public String dayIsHighlighted() { + return day("Highlighted"); + } + + public String dayIsToday() { + return day("Today"); + } + + public String dayIsValue() { + return day("Value"); + } + + public String dayIsValueAndHighlighted() { + return dayIsValue() + "AndHighlighted"; + } + + public String dayIsWeekend() { + return day("Weekend"); + } + + public String days() { + return wrap("Days"); + } + + public String daysLabel() { + return wrap("DaysLabel"); + } + + public String getBaseStyleName() { + return baseName; + } + + public String getWidgetStyleName() { + return widgetName; + } + + public String month() { + return wrap("Month"); + } + + public String monthSelector() { + return wrap("MonthSelector"); + } + + public String nextButton() { + return wrap("NextButton"); + } + + public String nextYearButton() { + return wrap("NextYearButton"); + } + + public String previousButton() { + return wrap("PreviousButton"); + } + + public String previousYearButton() { + return wrap("PreviousYearButton"); + } + + public String weekdayLabel() { + return wrap("WeekdayLabel"); + } + + public String weekendLabel() { + return wrap("WeekendLabel"); + } + + public String year() { + return wrap("Year"); + } + + /** + * Prepends the base name to the given style. + * + * @param style style name + * @return style name + */ + protected String wrap(String style) { + return baseName + style; + } + } + + /** + * A date highlighted event that copied on read. + */ + private static class DateHighlightEvent extends HighlightEvent { + + protected DateHighlightEvent(JsDate highlighted) { + super(highlighted); + } + + @Override + public JsDate getHighlighted() { + return CalendarUtil.copyDate(super.getHighlighted()); + } + } + + private static class DateStyler { + + private Map info = new HashMap(); + + public String getStyleName(JsDate d) { + return info.get(genKey(d)); + } + + public void setStyleName(JsDate d, String styleName, boolean add) { + // Code is easier to maintain if surrounded by " ", and on all browsers + // this is a no-op. + styleName = " " + styleName + " "; + String key = genKey(d); + String current = info.get(key); + + if (add) { + if (current == null) { + info.put(key, styleName); + } else if (current.indexOf(styleName) == -1) { + info.put(key, current + styleName); + } + } else { + if (current != null) { + String newValue = current.replaceAll(styleName, ""); + if (newValue.trim().length() == 0) { + info.remove(key); + } else { + info.put(key, newValue); + } + } + } + } + + private String genKey(JsDate d) { + return d.getFullYear() + "/" + d.getMonth() + "/" + d.getDate(); + } + } + + private static final int DEFAULT_VISIBLE_YEAR_COUNT = 21; + + private final DateStyler styler = new DateStyler(); + + private final MonthSelector monthAndYearSelector; + private final CalendarView view; + private final CalendarModel model; + private JsDate value; + private JsDate highlighted; + private StandardCss css = StandardCss.DEFAULT; + private LeafValueEditor editor; + private int visibleYearCount = DEFAULT_VISIBLE_YEAR_COUNT; + private boolean yearArrowsVisible; + private boolean yearAndMonthDropdownVisible; + + /** + * Create a new date picker. + */ + public CustomDatePicker() { + this(new DefaultMonthSelector(), + new DefaultCalendarView(), + new CalendarModel()); + } + + /** + * Creates a new date picker. + * + * @param monthAndYearSelector the month selector + * @param view the view + * @param model the model + */ + + protected CustomDatePicker(MonthSelector monthAndYearSelector, + CalendarView view, + CalendarModel model) { + + this.model = model; + this.monthAndYearSelector = monthAndYearSelector; + monthAndYearSelector.setDatePicker(this); + this.view = view; + view.setDatePicker(this); + + view.setup(); + monthAndYearSelector.setup(); + this.setup(); + + setCurrentMonth(JsDate.today()); + addStyleToDates(css().dayIsToday(), JsDate.today()); + } + + public HandlerRegistration addHighlightHandler(HighlightHandler handler) { + return addHandler(handler, HighlightEvent.getType()); + } + + public HandlerRegistration addShowRangeHandler(ShowRangeHandler handler) { + return addHandler(handler, ShowRangeEvent.getType()); + } + + /** + * Adds a show range handler and immediately activate the handler on the + * current view. + * + * @param handler the handler + * @return the handler registration + */ + public HandlerRegistration addShowRangeHandlerAndFire( + ShowRangeHandler handler) { + ShowRangeEvent event = new ShowRangeEvent( + getView().getFirstDate(), getView().getLastDate()) { + }; + handler.onShowRange(event); + return addShowRangeHandler(handler); + } + + /** + * Add a style name to the given dates. + */ + public void addStyleToDates(String styleName, JsDate date) { + styler.setStyleName(date, styleName, true); + if (isDateVisible(date)) { + getView().addStyleToDate(styleName, date); + } + } + + /** + * Add a style name to the given dates. + */ + public void addStyleToDates(String styleName, JsDate date, JsDate... moreDates) { + addStyleToDates(styleName, date); + for (JsDate d : moreDates) { + addStyleToDates(styleName, d); + } + } + + /** + * Add a style name to the given dates. + */ + public void addStyleToDates(String styleName, Iterable dates) { + for (JsDate d : dates) { + addStyleToDates(styleName, d); + } + } + + /** + * Adds the given style name to the specified dates, which must be visible. + * This is only set until the next time the DatePicker is refreshed. + */ + public void addTransientStyleToDates(String styleName, JsDate date) { + assert isDateVisible(date) : date + " must be visible"; + getView().addStyleToDate(styleName, date); + } + + /** + * Adds the given style name to the specified dates, which must be visible. + * This is only set until the next time the DatePicker is refreshed. + */ + public final void addTransientStyleToDates(String styleName, JsDate date, + JsDate... moreDates) { + addTransientStyleToDates(styleName, date); + for (JsDate d : moreDates) { + addTransientStyleToDates(styleName, d); + } + } + + /** + * Adds the given style name to the specified dates, which must be visible. + * This is only set until the next time the DatePicker is refreshed. + */ + public final void addTransientStyleToDates(String styleName, + Iterable dates) { + for (JsDate d : dates) { + addTransientStyleToDates(styleName, d); + } + } + + public HandlerRegistration addValueChangeHandler( + ValueChangeHandler handler) { + return addHandler(handler, ValueChangeEvent.getType()); + } + + /** + * Returns a {@link TakesValueEditor} backed by the DatePicker. + */ + public LeafValueEditor asEditor() { + if (editor == null) { + editor = TakesValueEditor.of(this); + } + return editor; + } + + /** + * Gets the current month the date picker is showing. + * + *

+ * A datepicker may show days not in the current month. It + * must show all days in the current month. + *

+ * + * @return the current month + */ + public JsDate getCurrentMonth() { + return getModel().getCurrentMonth(); + } + + /** + * Returns the first shown date. + * + * @return the first date. + */ + // Final because the view should always control the value of the first date. + public final JsDate getFirstDate() { + return view.getFirstDate(); + } + + /** + * Gets the highlighted date (the one the mouse is hovering over), if any. + * + * @return the highlighted date + */ + public final JsDate getHighlightedDate() { + return CalendarUtil.copyDate(highlighted); + } + + /** + * Returns the last shown date. + * + * @return the last date. + */ + // Final because the view should always control the value of the last date. + public final JsDate getLastDate() { + return view.getLastDate(); + } + + /** + * Returns the number of year to display in the years selection dropdown. + */ + public int getVisibleYearCount() { + return visibleYearCount; + } + + /** + * Gets the style associated with a date (does not include styles set via + * {@link #addTransientStyleToDates}). + * + * @param date the date + * @return the styles associated with this date + */ + public String getStyleOfDate(JsDate date) { + return styler.getStyleName(date); + } + + /** + * Returns the selected date, or null if none is selected. + * + * @return the selected date, or null + */ + public final JsDate getValue() { + return CalendarUtil.copyDate(value); + } + + /** + * Is the visible date enabled? + * + * @param date the date, which must be visible + * @return is the date enabled? + */ + public boolean isDateEnabled(JsDate date) { + assert isDateVisible(date) : date + " is not visible"; + return getView().isDateEnabled(date); + } + + /** + * Is the date currently shown in the date picker? + * + * @param date + * @return is the date currently shown + */ + public boolean isDateVisible(JsDate date) { + CalendarView r = getView(); + JsDate first = r.getFirstDate(); + JsDate last = r.getLastDate(); + return (date != null && (CalendarUtil.isSameDate(first, date) + || CalendarUtil.isSameDate(last, date) || + (first.getTime() < date.getTime()) && last.getTime() > date.getTime())); + } + + /** + * Can the user navigate through the years? + * + * @return is the year navigation is enabled + */ + public boolean isYearArrowsVisible() { + return yearArrowsVisible; + } + + /** + * Is the year and month selectable via a dropdown? + */ + public boolean isYearAndMonthDropdownVisible() { + return yearAndMonthDropdownVisible; + } + + @Override + public void onLoad() { + ShowRangeEvent.fire(this, getFirstDate(), getLastDate()); + } + + /** + * Removes the styleName from the given dates (even if it is transient). + */ + public void removeStyleFromDates(String styleName, JsDate date) { + styler.setStyleName(date, styleName, false); + if (isDateVisible(date)) { + getView().removeStyleFromDate(styleName, date); + } + } + + /** + * Removes the styleName from the given dates (even if it is transient). + */ + public void removeStyleFromDates(String styleName, JsDate date, + JsDate... moreDates) { + removeStyleFromDates(styleName, date); + for (JsDate d : moreDates) { + removeStyleFromDates(styleName, d); + } + } + + /** + * Removes the styleName from the given dates (even if it is transient). + */ + public void removeStyleFromDates(String styleName, Iterable dates) { + for (JsDate d : dates) { + removeStyleFromDates(styleName, d); + } + } + + /** + * Sets the date picker to show the given month, use {@link #getFirstDate()} + * and {@link #getLastDate()} to access the exact date range the date picker + * chose to display. + *

+ * A datepicker may show days not in the current month. It + * must show all days in the current month. + *

+ * + * @param month the month to show + */ + public void setCurrentMonth(JsDate month) { + getModel().setCurrentMonth(month); + refreshAll(); + } + + /** + * Set the number of years to display in the years selection dropdown. The range of years will be + * centered on the selected date. + */ + public void setVisibleYearCount(int numberOfYears) { + if (numberOfYears <= 0) { + throw new IllegalArgumentException("The number of years to display must be positive"); + } + + visibleYearCount = numberOfYears; + getMonthSelector().refresh(); + } + + /** + * Set if the user can navigate through the years via a set of backward and forward buttons. + */ + public void setYearArrowsVisible(boolean yearArrowsVisible) { + this.yearArrowsVisible = yearArrowsVisible; + getMonthSelector().refresh(); + } + + /** + * If the dropdownVisible is equal to true, the user will be able to change the current month and + * the current year of the date picker via two dropdown lists. + */ + public void setYearAndMonthDropdownVisible(boolean dropdownVisible) { + this.yearAndMonthDropdownVisible = dropdownVisible; + getMonthSelector().refresh(); + } + + /** + * Sets the date picker style name. + */ + @Override + public void setStyleName(String styleName) { + css = new StandardCss(styleName, "customDatePicker"); + super.setStyleName(styleName); + } + + /** + * Sets a visible date to be enabled or disabled. This is only set until the + * next time the DatePicker is refreshed. + */ + public final void setTransientEnabledOnDates(boolean enabled, JsDate date) { + assert isDateVisible(date) : date + " must be visible"; + getView().setEnabledOnDate(enabled, date); + } + + /** + * Sets a visible date to be enabled or disabled. This is only set until the + * next time the DatePicker is refreshed. + */ + public final void setTransientEnabledOnDates(boolean enabled, JsDate date, + JsDate... moreDates) { + setTransientEnabledOnDates(enabled, date); + for (JsDate d : moreDates) { + setTransientEnabledOnDates(enabled, d); + } + } + + /** + * Sets a group of visible dates to be enabled or disabled. This is only set + * until the next time the DatePicker is refreshed. + */ + public final void setTransientEnabledOnDates(boolean enabled, + Iterable dates) { + for (JsDate d : dates) { + setTransientEnabledOnDates(enabled, d); + } + } + + /** + * Sets the {@link CustomDatePicker}'s value. + * + * @param newValue the new value + */ + public final void setValue(JsDate newValue) { + setValue(newValue, false); + } + + /** + * Sets the {@link CustomDatePicker}'s value. + * + * @param newValue the new value for this date picker + * @param fireEvents should events be fired. + */ + public final void setValue(JsDate newValue, boolean fireEvents) { + JsDate oldValue = value; + + if (oldValue != null) { + removeStyleFromDates(css().dayIsValue(), oldValue); + } + + value = CalendarUtil.copyDate(newValue); + if (value != null) { + addStyleToDates(css().dayIsValue(), value); + } + getView().setAriaSelectedCell(newValue); + + if (fireEvents) { + DateChangeEvent.fireIfNotEqualDates(this, oldValue, newValue); + } + } + + /** + * Gets the {@link CalendarModel} associated with this date picker. + * + * @return the model + */ + protected final CalendarModel getModel() { + return model; + } + + /** + * Gets the {@link MonthSelector} associated with this date picker. + * + * @return the month selector + */ + protected final MonthSelector getMonthSelector() { + return monthAndYearSelector; + } + + /** + * Gets the {@link CalendarView} associated with this date picker. + * + * @return the view + */ + protected final CalendarView getView() { + return view; + } + + /** + * Refreshes all components of this date picker. + */ + protected final void refreshAll() { + highlighted = null; + getModel().refresh(); + + getView().refresh(); + getMonthSelector().refresh(); + if (isAttached()) { + ShowRangeEvent.fire(this, getFirstDate(), getLastDate()); + } + getView().setAriaSelectedCell(value); + } + + /** + * Sets up the date picker. + */ + protected void setup() { + /* + * Use a table (VerticalPanel) to get shrink-to-fit behavior. Divs expand to + * fill the available width, so we'd need to give it a size. + */ + FlowPanel panel = new FlowPanel(); + panel.setStyleName(css.datePicker()); + panel.add(this.getMonthSelector()); + panel.add(this.getView()); + initWidget(panel); + } + + /** + * Gets the css associated with this date picker for use by extended month and + * cell grids. + * + * @return the css. + */ + final StandardCss css() { + return css; + } + + /** + * Sets the highlighted date. + * + * @param highlighted highlighted date + */ + void setHighlightedDate(JsDate highlighted) { + this.highlighted = highlighted; + fireEvent(new DateHighlightEvent(highlighted)); + } +} diff --git a/stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/DateCell.java b/stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/DateCell.java new file mode 100644 index 00000000000..d14ca320439 --- /dev/null +++ b/stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/DateCell.java @@ -0,0 +1,134 @@ +package stroom.widget.datepicker.client; + +import com.google.gwt.aria.client.Roles; +import com.google.gwt.aria.client.SelectedValue; + +public final class DateCell extends AbstractCell { + + private boolean enabled = true; + private final int index; + + private final DefaultCalendarView defaultCalendarView; + private final CustomDatePicker.StandardCss css; + private String cellStyle; + private String dateStyle; + private JsDate value = JsDate.create(); + + + public DateCell(final DefaultCalendarView defaultCalendarView, + final CustomDatePicker.StandardCss css, + final boolean isWeekend, + final int index) { + this.defaultCalendarView = defaultCalendarView; + this.css = css; + this.index = index; + cellStyle = css.day(); + + if (isWeekend) { + cellStyle += " " + css.dayIsWeekend(); + } + getElement().setTabIndex(isFiller() + ? -1 + : 0); + setAriaSelected(false); + } + + public boolean isEnabled() { + return enabled; + } + + public boolean isSelected() { + final JsDate selected = defaultCalendarView.getDatePicker().getValue(); + return selected != null && value != null && value.getTime() == selected.getTime(); + } + + public boolean isHighlighted() { + final JsDate highlighted = defaultCalendarView.getDatePicker().getHighlightedDate(); + return highlighted != null && value != null && value.getTime() == highlighted.getTime(); + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + onEnabled(enabled); + } + + private void onEnabled(boolean enabled) { + updateStyle(); + } + + private void updateStyle() { + String accum = dateStyle; + if (isHighlighted()) { + accum += " " + css.dayIsHighlighted(); + if (isSelected()) { + accum += " " + css.dayIsValueAndHighlighted(); + } + } + if (!isEnabled()) { + accum += " " + css.dayIsDisabled(); + } + setStyleName("cellOuter " + accum); + } + + @Override + public void addStyleName(String styleName) { + if (dateStyle.indexOf(" " + styleName + " ") == -1) { + dateStyle += styleName + " "; + } + updateStyle(); + } + + public boolean isFiller() { + return !defaultCalendarView.getModel().isInCurrentMonth(value); + } + + public void onSelected(boolean selected) { + if (selected) { + defaultCalendarView.getDatePicker().setValue(value, true); + if (isFiller()) { + defaultCalendarView.getDatePicker().setCurrentMonth(value); + } + } + updateStyle(); + } + + @Override + public void removeStyleName(String styleName) { + dateStyle = dateStyle.replace(" " + styleName + " ", " "); + updateStyle(); + } + + public void setAriaSelected(boolean value) { + Roles.getGridcellRole().setAriaSelectedState(getElement(), SelectedValue.of(value)); + } + + public JsDate getValue() { + return value; + } + + void update(final JsDate current) { + setEnabled(true); + value = CalendarUtil.copyDate(current); + String text = defaultCalendarView.getModel().formatDayOfMonth(value); + setText(text); + dateStyle = cellStyle; + if (isFiller()) { + getElement().setTabIndex(-1); + dateStyle += " " + css.dayIsFiller(); + } else { + getElement().setTabIndex(0); + String extraStyle = defaultCalendarView.getDatePicker().getStyleOfDate(value); + if (extraStyle != null) { + dateStyle += " " + extraStyle; + } + } + // We want to certify that all date styles have " " before and after + // them for ease of adding to and replacing them. + dateStyle += " "; + updateStyle(); + } + + public int getIndex() { + return index; + } +} diff --git a/stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/DateChangeEvent.java b/stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/DateChangeEvent.java new file mode 100644 index 00000000000..8d7c88519e4 --- /dev/null +++ b/stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/DateChangeEvent.java @@ -0,0 +1,59 @@ +/* + * Copyright 2008 Google Inc. + * + * 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 stroom.widget.datepicker.client; + +import com.google.gwt.event.logical.shared.HasValueChangeHandlers; +import com.google.gwt.event.logical.shared.ValueChangeEvent; +import com.google.gwt.event.shared.HasHandlers; + +/** + * Creates a new value every time a date is accessed. + */ +class DateChangeEvent extends ValueChangeEvent { + + /** + * Fires value change event if the old value is not equal to the new value. + * Use this call rather than making the decision to short circuit yourself for + * safe handling of null. + * + * @param The event source + * @param source the source of the handlers + * @param oldValue the oldValue, may be null + * @param newValue the newValue, may be null + */ + public static & HasHandlers> void fireIfNotEqualDates( + S source, JsDate oldValue, JsDate newValue) { + if (ValueChangeEvent.shouldFire(source, oldValue, newValue)) { + source.fireEvent(new DateChangeEvent(newValue)); + } + } + + /** + * Creates a new date value change event. + * + * @param value the value + */ + protected DateChangeEvent(JsDate value) { + // The date must be copied in case one handler causes it to change. + super(CalendarUtil.copyDate(value)); + } + + @Override + public JsDate getValue() { + return CalendarUtil.copyDate(super.getValue()); + } +} diff --git a/stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/DateGrid.java b/stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/DateGrid.java new file mode 100644 index 00000000000..78db8298210 --- /dev/null +++ b/stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/DateGrid.java @@ -0,0 +1,128 @@ +/* + * Copyright 2008 Google Inc. + * + * 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 stroom.widget.datepicker.client; + +import stroom.widget.util.client.ElementUtil; + +import com.google.gwt.dom.client.Element; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.impl.ElementMapperImpl; +import com.google.gwt.user.client.ui.Grid; + +import java.util.ArrayList; + +/** + * Highlighting, selectable cell grid. Used to help construct the default + * calendar view. + */ +public class DateGrid extends Grid { + + private final ElementMapperImpl elementToCell = new ElementMapperImpl<>(); + private final ArrayList cellList = new ArrayList<>(); + private DateCell selectedCell; + + protected DateGrid() { + sinkEvents(Event.ONCLICK); + setCellPadding(0); + setCellSpacing(0); + setBorderWidth(0); + resize(CalendarModel.WEEKS_IN_MONTH + 1, CalendarModel.DAYS_IN_WEEK); + } + + public void addElement(final AbstractCell abstractCell) { + elementToCell.put(abstractCell); + } + + public void addCell(final DateCell cell) { + cellList.add(cell); + } + + public DateCell getCell(int i) { + return cellList.get(i); + } + + public int getNumCells() { + return cellList.size(); + } + + @Override + public void onBrowserEvent(Event event) { + final Element element = event.getEventTarget().cast(); + if (element != null) { + final Element outer = ElementUtil.findMatching(element, "cellOuter", 0, 5); + if (outer != null) { + final AbstractCell cell = elementToCell.get(outer); + if (cell instanceof DateCell) { + final DateCell dateCell = (DateCell) cell; + if (DOM.eventGetType(event) == Event.ONCLICK) { + if (isActive(dateCell)) { + setSelected(dateCell); + } + } + } + } + } + } + + public final void setSelected(DateCell cell) { + DateCell last = selectedCell; + selectedCell = cell; + + if (last != null) { + last.onSelected(false); + } + if (selectedCell != null) { + selectedCell.onSelected(true); + } + } + + boolean isActive(DateCell cell) { + return cell != null && cell.isEnabled(); + } + + +// public void verticalNavigation(int keyCode) { +// switch (keyCode) { +// case KeyCodes.KEY_UP: +// setHighlighted(previousItem()); +// break; +// case KeyCodes.KEY_DOWN: +// setHighlighted(nextItem()); +// break; +// case KeyCodes.KEY_ENTER: +// setSelected(this); +// break; +// } +// } +// +// private DateCell nextItem() { +// if (index == getLastIndex()) { +// return getCell(0); +// } else { +// return getCell(index + 1); +// } +// } +// +// private DateCell previousItem() { +// if (index != 0) { +// return getCell(index - 1); +// } else { +// return getCell(getLastIndex()); +// } +// } +} diff --git a/stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/DatePickerComponent.java b/stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/DatePickerComponent.java new file mode 100644 index 00000000000..a14034c8b7d --- /dev/null +++ b/stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/DatePickerComponent.java @@ -0,0 +1,69 @@ +/* + * Copyright 2008 Google Inc. + * + * 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 stroom.widget.datepicker.client; + +import com.google.gwt.user.client.ui.Composite; + +/** + * Package protected class used to combine functionality for the + * {@link MonthSelector} and {@link CalendarView} components. + */ +abstract class DatePickerComponent extends Composite { + + private CustomDatePicker datePicker; + + public CalendarModel getModel() { + return datePicker.getModel(); + } + + protected void addMonths(int numMonths) { + getModel().shiftCurrentMonth(numMonths); + getDatePicker().refreshAll(); + } + + protected CustomDatePicker getDatePicker() { + return datePicker; + } + + /** + * Refresh the component. Usually called because the model's current date has + * changed. In general, only should be called by {@link CustomDatePicker}. Use + * refreshAll() if you need to refresh all components. + */ + protected abstract void refresh(); + + /** + * Refreshes the {@link CustomDatePicker}, {@link CalendarView}, and + * {@link CalendarModel}. + */ + protected void refreshAll() { + getDatePicker().refreshAll(); + } + + /** + * Set up the component. + */ + protected abstract void setup(); + + CustomDatePicker.StandardCss css() { + return datePicker.css(); + } + + void setDatePicker(CustomDatePicker me) { + this.datePicker = me; + } +} diff --git a/stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/DateRecord.java b/stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/DateRecord.java new file mode 100644 index 00000000000..3c78dc406a6 --- /dev/null +++ b/stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/DateRecord.java @@ -0,0 +1,353 @@ +/* + * Copyright 2008 Google Inc. + * + * 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 stroom.widget.datepicker.client; + +/** + * Implementation detail of DateTimeFormat -- not a public API and subject to + * change. + *

+ * DateRecord class exposes almost the same set of interface as Date class with + * only a few exceptions. The main purpose is the record all the information + * during parsing phase and resolve them in a later time when all information + * can be processed together. + */ +public class DateRecord { + + public static final int AM = 0; + public static final int PM = 1; + + private static final int JS_START_YEAR = 1900; + + private int era; + private int year; + private int month; + private int dayOfMonth; + private int ampm; + private boolean midnightIs24; + private int hours; + private int minutes; + private int seconds; + private int milliseconds; + + private int tzOffset; + private int dayOfWeek; + private boolean ambiguousYear; + + /** + * Initialize DateExt object with default value. Here we use -1 for most of + * the field to indicate that field is not set. + */ + public DateRecord() { + era = -1; + ambiguousYear = false; + year = Integer.MIN_VALUE; + month = -1; + dayOfMonth = -1; + ampm = -1; + midnightIs24 = false; + hours = -1; + minutes = -1; + seconds = -1; + milliseconds = -1; + dayOfWeek = -1; + tzOffset = Integer.MIN_VALUE; + } + + /** + * calcDate uses all the field available so far to fill a Date object. For + * those information that is not provided, the existing value in 'date' will + * be kept. Ambiguous year will be resolved after the date/time values are + * resolved. + *

+ * If the strict option is set to true, calcDate will calculate certain + * invalid dates by wrapping around as needed. For example, February 30 will + * wrap to March 2. + * + * @param date The Date object being filled. Its value should be set to an + * acceptable default before pass in to this method + * @param strict true to be strict when parsing + * @return true if successful, otherwise false. + */ + public boolean calcDate(JsDate date, boolean strict) { + // Year 0 is 1 BC, and so on. + if (this.era == 0 && this.year > 0) { + this.year = -(this.year - 1); + } + + if (this.year > Integer.MIN_VALUE) { + date.setFullYear(this.year - JS_START_YEAR); + } + + // "setMonth" and "setDate" is a little bit tricky. Suppose content in + // date is 11/30, switch month to 02 will lead to 03/02 since 02/30 does + // not exist. And you certain won't like 02/12 turn out to be 03/12. So + // here to set date to a smaller number before month, and later setMonth. + // Real date is set after, and that might cause month switch. However, + // that's desired. + int orgDayOfMonth = date.getDate(); + date.setDate(1); + + if (this.month >= 0) { + date.setMonth(this.month); + } + + if (this.dayOfMonth >= 0) { + date.setDate(this.dayOfMonth); + } else if (this.month >= 0) { + // If the month was parsed but dayOfMonth was not, then the current day of + // the month shouldn't affect the parsed month. For example, if "Feb2006" + // is parse on January 31, the resulting date should be in February, not + // March. So, we limit the day of the month to the maximum day within the + // parsed month. + JsDate tmp = JsDate.create(date.getFullYear(), date.getMonth(), 35); + int daysInCurrentMonth = 35 - tmp.getDate(); + date.setDate(Math.min(daysInCurrentMonth, orgDayOfMonth)); + } else { + date.setDate(orgDayOfMonth); + } + + // adjust ampm + if (this.hours < 0) { + this.hours = date.getHours(); + } + + if (this.ampm > 0) { + if (this.hours < 12) { + this.hours += 12; + } + } + date.setHours(this.hours == 24 && this.midnightIs24 + ? 0 + : this.hours); + + if (this.minutes >= 0) { + date.setMinutes(this.minutes); + } + + if (this.seconds >= 0) { + date.setSeconds(this.seconds); + } + + if (this.milliseconds >= 0) { + date.setTime(date.getTime() / 1000 * 1000 + this.milliseconds); + } + + // If strict, verify that the original date fields match the calculated date + // fields. We do this before we set the timezone offset, which will skew all + // of the dates. + // + // We don't need to check the day of week as it is guaranteed to be correct + // or return false below. + if (strict) { + if ((this.year > Integer.MIN_VALUE) + && ((this.year - JS_START_YEAR) != date.getFullYear())) { + return false; + } + if ((this.month >= 0) && (this.month != date.getMonth())) { + return false; + } + if ((this.dayOfMonth >= 0) && (this.dayOfMonth != date.getDate())) { + return false; + } + // Times have well defined maximums + if (this.hours == 24 && this.midnightIs24) { + if (this.ampm > 0) { + return false; + } + } else if (this.hours >= 24) { + return false; + } else if (this.hours == 0 && this.midnightIs24) { + return false; + } + if (this.minutes >= 60) { + return false; + } + if (this.seconds >= 60) { + return false; + } + if (this.milliseconds >= 1000) { + return false; + } + } + + // Resolve ambiguous year if needed. + if (this.ambiguousYear) { // the two-digit year == the default start year + JsDate defaultCenturyStart = JsDate.create(); + defaultCenturyStart.setFullYear(defaultCenturyStart.getFullYear() - 80); + if (date.getTime() < defaultCenturyStart.getTime()) { + date.setFullYear(defaultCenturyStart.getFullYear() + 100); + } + } + + // Date is resolved to the nearest dayOfWeek if date is not explicitly + // specified. There is one exception, if the nearest dayOfWeek falls + // into a different month, the 2nd nearest dayOfWeek, which is on the + // other direction, will be used. + if (this.dayOfWeek >= 0) { + if (this.dayOfMonth == -1) { + // Adjust to the nearest day of the week. + int adjustment = (7 + this.dayOfWeek - date.getDay()) % 7; + if (adjustment > 3) { + adjustment -= 7; + } + int orgMonth = date.getMonth(); + date.setDate(date.getDate() + adjustment); + + // If the nearest weekday fall into a different month, we will use the + // 2nd nearest weekday, which will be on the other direction, and is + // sure fall into the same month. + if (date.getMonth() != orgMonth) { + date.setDate(date.getDate() + (adjustment > 0 + ? -7 + : 7)); + } + } else { + if (date.getDay() != this.dayOfWeek) { + return false; + } + } + } + + // Adjust time zone. + if (this.tzOffset > Integer.MIN_VALUE) { + int offset = date.getTimezoneOffset(); + date.setTime(date.getTime() + (this.tzOffset - offset) * 60 * 1000); + // HBJ date.setTime(date.getTime() + this.tzOffset * 60 * 1000); + } + + return true; + } + + /** + * Set ambiguous year field. This flag indicates that a 2 digit years's + * century need to be determined by its date/time value. This can only be + * resolved after its date/time is known. + * + * @param ambiguousYear true if it is ambiguous year. + */ + public void setAmbiguousYear(boolean ambiguousYear) { + this.ambiguousYear = ambiguousYear; + } + + /** + * Set morning/afternoon field. + * + * @param ampm ampm value. + */ + public void setAmpm(int ampm) { + this.ampm = ampm; + } + + /** + * Set dayOfMonth field. + * + * @param day dayOfMonth value + */ + public void setDayOfMonth(int day) { + this.dayOfMonth = day; + } + + /** + * Set dayOfWeek field. + * + * @param dayOfWeek day of the week. + */ + public void setDayOfWeek(int dayOfWeek) { + this.dayOfWeek = dayOfWeek; + } + + /** + * Set Era field. + * + * @param era era value being set. + */ + public void setEra(int era) { + this.era = era; + } + + /** + * Set hour field. + * + * @param hours hour value. + */ + public void setHours(int hours) { + this.hours = hours; + } + + /** + * Set midnightIs24 field. + * + * @param midnightIs24 whether an hour value of 24 signifies midnight. + */ + public void setMidnightIs24(boolean midnightIs24) { + this.midnightIs24 = midnightIs24; + } + + /** + * Set milliseconds field. + * + * @param milliseconds milliseconds value. + */ + public void setMilliseconds(int milliseconds) { + this.milliseconds = milliseconds; + } + + /** + * Set minute field. + * + * @param minutes minute value. + */ + public void setMinutes(int minutes) { + this.minutes = minutes; + } + + /** + * Set month field. + * + * @param month month value. + */ + public void setMonth(int month) { + this.month = month; + } + + /** + * Set seconds field. + * + * @param seconds second value. + */ + public void setSeconds(int seconds) { + this.seconds = seconds; + } + + /** + * Set timezone offset, in minutes. + * + * @param tzOffset timezone offset. + */ + public void setTzOffset(int tzOffset) { + this.tzOffset = tzOffset; + } + + /** + * Set year field. + * + * @param value year value. + */ + public void setFullYear(int value) { + this.year = value; + } +} diff --git a/stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/DateTimeConstants.java b/stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/DateTimeConstants.java new file mode 100644 index 00000000000..c76474d5692 --- /dev/null +++ b/stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/DateTimeConstants.java @@ -0,0 +1,70 @@ +package stroom.widget.datepicker.client; + +import stroom.widget.datepicker.client.IntlDateTimeFormat.FormatOptions; +import stroom.widget.datepicker.client.IntlDateTimeFormat.FormatOptions.Day; +import stroom.widget.datepicker.client.IntlDateTimeFormat.FormatOptions.Month; +import stroom.widget.datepicker.client.IntlDateTimeFormat.FormatOptions.Weekday; + +import com.google.gwt.core.client.GWT; + +public class DateTimeConstants { + + public static final int DAYS_IN_WEEK = 7; + + public static final int MONTHS_IN_YEAR = 12; + + private static final int MAX_DAYS_IN_MONTH = 31; + + private final String[] languages; + private final String[] dayOfWeekNames; + private final String[] dayOfMonthNames; + private final String[] monthNames; + + public DateTimeConstants() { + languages = new String[IntlDateTimeFormat.getLanguageCount()]; + for (int i = 0; i < languages.length; i++) { + languages[i] = IntlDateTimeFormat.getLanguage(i); + GWT.log(languages[i]); + } + + dayOfWeekNames = new String[DAYS_IN_WEEK]; + for (int i = 0; i < dayOfWeekNames.length; i++) { + final JsDate date = JsDate.create(2000, 0, i + 1); + final String dayOfWeekName = IntlDateTimeFormat.format(date, languages, + FormatOptions.builder().weekday(Weekday.SHORT).build()); + dayOfWeekNames[date.getDay()] = dayOfWeekName; + } + + dayOfMonthNames = new String[MAX_DAYS_IN_MONTH + 1]; + for (int i = 1; i < dayOfMonthNames.length; i++) { + final JsDate date = JsDate.create(2000, 0, i); + final String dayOfMonthName = IntlDateTimeFormat.format(date, languages, + FormatOptions.builder().day(Day.NUMERIC).build()); + dayOfMonthNames[i] = dayOfMonthName; + } + + monthNames = new String[MONTHS_IN_YEAR]; + for (int i = 0; i < monthNames.length; i++) { + final JsDate date = JsDate.create(2000, i, 1); + final String monthName = IntlDateTimeFormat.format(date, languages, + FormatOptions.builder().month(Month.LONG).build()); + monthNames[i] = monthName; + } + } + + public String[] getLanguages() { + return languages; + } + + public String[] getDayOfWeekNames() { + return dayOfWeekNames; + } + + public String[] getDayOfMonthNames() { + return dayOfMonthNames; + } + + public String[] getMonthNames() { + return monthNames; + } +} diff --git a/stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/DateTimeFormat.java b/stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/DateTimeFormat.java new file mode 100644 index 00000000000..731d5146d19 --- /dev/null +++ b/stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/DateTimeFormat.java @@ -0,0 +1,19 @@ +package stroom.widget.datepicker.client; + +public interface DateTimeFormat { + + /** + * Format a date object. + * + * @param date the date object being formatted + * @return string representation for this date in desired format + */ + String format(JsDate date); + + /** + * Retrieve the pattern used in this DateTimeFormat object. + * + * @return pattern string + */ + String getPattern(); +} diff --git a/stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/DateTimeFormatImpl.java b/stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/DateTimeFormatImpl.java new file mode 100644 index 00000000000..aded2d0a7db --- /dev/null +++ b/stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/DateTimeFormatImpl.java @@ -0,0 +1,2176 @@ +/* + * Copyright 2008 Google Inc. + * + * 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 stroom.widget.datepicker.client; + +import com.google.gwt.i18n.client.LocaleInfo; +import com.google.gwt.i18n.shared.DateTimeFormatInfo; +import com.google.gwt.i18n.shared.DefaultDateTimeFormatInfo; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +/** + * Formats and parses dates and times using locale-sensitive patterns. + * + *

Patterns

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
SymbolMeaningPresentationExample
Gera designatorTextAD
yyearNumber1996
Lstandalone month in yearText or NumberJuly (or) 07
Mmonth in yearText or NumberJuly (or) 07
dday in monthNumber10
hhour in am/pm (1-12)Number12
Hhour in day (0-23)Number0
mminute in hourNumber30
ssecond in minuteNumber55
Sfractional secondNumber978
Eday of weekTextTuesday
cstandalone day of weekTextTuesday
aam/pm markerTextPM
khour in day (1-24)Number24
Khour in am/pm (0-11)Number0
ztime zoneTextPacific Standard Time(see comment)
Ztime zone (RFC 822)Text-0800(See comment)
vtime zone idTextAmerica/Los_Angeles(See comment)
'escape for textDelimiter'Date='
''single quoteLiteral'o''clock'
+ * + *

+ * The number of pattern letters influences the format, as follows: + *

+ * + *
+ *
Text
+ *
if 4 or more, then use the full form; if less than 4, use short or + * abbreviated form if it exists (e.g., "EEEE" produces + * "Monday", "EEE" produces "Mon")
+ * + *
Number
+ *
the minimum number of digits. Shorter numbers are zero-padded to this + * amount (e.g. if "m" produces "6", "mm" + * produces "06"). Year is handled specially; that is, if the count + * of 'y' is 2, the Year will be truncated to 2 digits. (e.g., if + * "yyyy" produces "1997", "yy" produces + * "97".) Unlike other fields, fractional seconds are padded on the + * right with zero.
+ * + *
Text or Number
+ *
3 or more, use text, otherwise use number. (e.g. "M" + * produces "1", "MM" produces "01", + * "MMM" produces "Jan", and "MMMM" + * produces "January". Some pattern letters also treat a count + * of 5 specially, meaning a single-letter abbreviation: L, + * M, E, and c.
+ *
+ * + *

+ * Any characters in the pattern that are not in the ranges of ['a + * '..'z'] and ['A'..'Z'] will be treated + * as quoted text. For instance, characters like ':', ' + * .', ' ' (space), '#' and ' + * @' will appear in the resulting time text even they are not + * embraced within single quotes. + *

+ * + *

+ * [Time Zone Handling] Web browsers don't provide all the information we need + * for proper time zone formating -- so GWT has a copy of the required data, for + * your convenience. For simpler cases, one can also use a fallback + * implementation that only keeps track of the current timezone offset. These + * two approaches are called, respectively, Common TimeZones and Simple + * TimeZones, although both are implemented with the same TimeZone class. + *

+ * "TimeZone createTimeZone(String timezoneData)" returns a Common TimeZone + * object, and "TimeZone createTimeZone(int timeZoneOffsetInMinutes)" returns a + * Simple TimeZone object. The one provided by OS fall into to Simple TimeZone + * category. For formatting purpose, following table shows the behavior of GWT + * DateTimeFormat. + *

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
PatternCommon TimeZoneSimple TimeZone
z, zz, zzzPDTUTC-7
zzzzPacific Daylight TimeUTC-7
Z, ZZ-0700-0700
ZZZ-07:00-07:00
ZZZZGMT-07:00GMT-07:00
v, vv, vvv, vvvvAmerica/Los_AngelesEtc/GMT+7
+ * + *

Parsing Dates and Times

+ *

+ * The pattern does not need to specify every field. If the year, month, or + * day is missing from the pattern, the corresponding value will be taken from + * the current date. If the month is specified but the day is not, the day will + * be constrained to the last day within the specified month. If the hour, + * minute, or second is missing, the value defaults to zero. + *

+ * + *

+ * As with formatting (described above), the count of pattern letters determines + * the parsing behavior. + *

+ * + *
+ *
Text
+ *
4 or more pattern letters--use full form, less than 4--use short or + * abbreviated form if one exists. In parsing, we will always try long format, + * then short.
+ * + *
Number
+ *
the minimum number of digits.
+ * + *
Text or Number
+ *
3 or more characters means use text, otherwise use number
+ *
+ * + *

+ * Although the current pattern specification doesn't not specify behavior for + * all letters, it may in the future. It is strongly discouraged to use + * unspecified letters as literal text without quoting them. + *

+ *

+ * [Note on TimeZone] The time zone support for parsing is limited. Only + * standard GMT and RFC format are supported. Time zone specification using time + * zone id (like America/Los_Angeles), time zone names (like PST, Pacific + * Standard Time) are not supported. Normally, it is too much a burden for a + * client application to load all the time zone symbols. And in almost all those + * cases, it is a better choice to do such parsing on server side through + * certain RPC mechanism. This decision is based on particular use cases we have + * studied; in principle, it could be changed in future versions. + *

+ * + *

Examples

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
PatternFormatted Text
"yyyy.MM.dd G 'at' HH:mm:ss vvvv"1996.07.10 AD at 15:08:56 America/Los_Angeles
"EEE, MMM d, ''yy"Wed, July 10, '96
"h:mm a"12:08 PM
"hh 'o''clock' a, zzzz" 12 o'clock PM, Pacific Daylight Time
"K:mm a, vvvv" 0:00 PM, America/Los_Angeles
"yyyyy.MMMMM.dd GGG hh:mm aaa"01996.July.10 AD 12:08 PM
+ * + *

Additional Parsing Considerations

+ *

+ * When parsing a date string using the abbreviated year pattern ( + * "yy"), the parser must interpret the abbreviated year relative + * to some century. It does this by adjusting dates to be within 80 years before + * and 20 years after the time the parser instance is created. For example, + * using a pattern of "MM/dd/yy" and a DateTimeFormat + * object created on Jan 1, 1997, the string "01/11/12" would be + * interpreted as Jan 11, 2012 while the string "05/04/64" would be + * interpreted as May 4, 1964. During parsing, only strings consisting of + * exactly two digits, as defined by {@link Character#isDigit(char)}, + * will be parsed into the default century. If the year pattern does not have + * exactly two 'y' characters, the year is interpreted literally, regardless of + * the number of digits. For example, using the pattern + * "MM/dd/yyyy", "01/11/12" parses to Jan 11, 12 A.D. + *

+ * + *

+ * When numeric fields abut one another directly, with no intervening delimiter + * characters, they constitute a run of abutting numeric fields. Such runs are + * parsed specially. For example, the format "HHmmss" parses the input text + * "123456" to 12:34:56, parses the input text "12345" to 1:23:45, and fails to + * parse "1234". In other words, the leftmost field of the run is flexible, + * while the others keep a fixed width. If the parse fails anywhere in the run, + * then the leftmost field is shortened by one character, and the entire run is + * parsed again. This is repeated until either the parse succeeds or the + * leftmost field is one character in length. If the parse still fails at that + * point, the parse of the run fails. + *

+ * + *

+ * In the current implementation, timezone parsing only supports + * GMT:hhmm, GMT:+hhmm, and GMT:-hhmm. + *

+ * + *

Example

{@example com.google.gwt.examples.DateTimeFormatExample} + */ +public class DateTimeFormatImpl implements DateTimeFormat { + + /** + * Predefined date/time formats -- see {@link CustomDateTimeFormat} if you + * need some format that isn't supplied here. + */ + public enum PredefinedFormat { + // TODO(jat): Javadoc to explain these formats + + /** + * ISO 8601 date format, fixed across all locales. + *

Example: {@code 2008-10-03T10:29:40.046-04:00} + *

http://code.google.com/p/google-web-toolkit/issues/detail?id=3068 + *

http://www.iso.org/iso/support/faqs/faqs_widely_used_standards/widely_used_standards_other/date_and_time_format.htm + */ + ISO_8601, + + /** + * RFC 2822 date format, fixed across all locales. + *

Example: {@code Thu, 20 May 2010 17:54:50 -0700} + *

http://tools.ietf.org/html/rfc2822#section-3.3 + */ + RFC_2822, + + DATE_FULL, + DATE_LONG, + DATE_MEDIUM, + DATE_SHORT, + + TIME_FULL, + TIME_LONG, + TIME_MEDIUM, + TIME_SHORT, + + DATE_TIME_FULL, + DATE_TIME_LONG, + DATE_TIME_MEDIUM, + DATE_TIME_SHORT, + + DAY, + HOUR_MINUTE, + HOUR_MINUTE_SECOND, + HOUR24_MINUTE, + HOUR24_MINUTE_SECOND, + MINUTE_SECOND, + MONTH, + MONTH_ABBR, + MONTH_ABBR_DAY, + MONTH_DAY, + MONTH_NUM_DAY, + MONTH_WEEKDAY_DAY, + YEAR, + YEAR_MONTH, + YEAR_MONTH_ABBR, + YEAR_MONTH_ABBR_DAY, + YEAR_MONTH_DAY, + YEAR_MONTH_NUM, + YEAR_MONTH_NUM_DAY, + YEAR_MONTH_WEEKDAY_DAY, + YEAR_QUARTER, + YEAR_QUARTER_ABBR, + } + + /** + * Class PatternPart holds a "compiled" pattern part. + */ + private static class PatternPart { + + public String text; + public int count; // 0 has a special meaning, it stands for literal + public boolean abutStart; + + public PatternPart(String txt, int cnt) { + text = txt; + count = cnt; + abutStart = false; + } + } + + protected static final String RFC2822_PATTERN = "EEE, d MMM yyyy HH:mm:ss Z"; + protected static final String ISO8601_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZ"; + + private static final int NUMBER_BASE = 10; + private static final int JS_START_YEAR = 1900; + + private static final Map cache; + + private static final int NUM_MILLISECONDS_IN_DAY = 24 * 60 * 60000; + + private static final String PATTERN_CHARS = "GyMLdkHmsSEcDahKzZv"; + + // Note: M & L must be the first two characters + private static final String NUMERIC_FORMAT_CHARS = "MLydhHmsSDkK"; + + private static final String WHITE_SPACE = " \t\r\n"; + + private static final String GMT = "GMT"; + private static final String UTC = "UTC"; + + private static final int MINUTES_PER_HOUR = 60; + + static { + cache = new HashMap(); + } + + /** + * Get a DateTimeFormat instance for a predefined format. + * + *

See {@link CustomDateTimeFormat} if you need a localized format that is + * not supported here. + * + * @param predef {@link PredefinedFormat} describing desired format + * @return a DateTimeFormat instance for the specified format + */ + public static DateTimeFormatImpl getFormat(PredefinedFormat predef) { + if (usesFixedEnglishStrings(predef)) { + String pattern; + switch (predef) { + case RFC_2822: + pattern = RFC2822_PATTERN; + break; + case ISO_8601: + pattern = ISO8601_PATTERN; + break; + default: + throw new IllegalStateException("Unexpected predef type " + predef); + } + return getFormat(pattern, new DefaultDateTimeFormatInfo()); + } + DateTimeFormatInfo dtfi = getDefaultDateTimeFormatInfo(); + String pattern; + switch (predef) { + case DATE_FULL: + pattern = dtfi.dateFormatFull(); + break; + case DATE_LONG: + pattern = dtfi.dateFormatLong(); + break; + case DATE_MEDIUM: + pattern = dtfi.dateFormatMedium(); + break; + case DATE_SHORT: + pattern = dtfi.dateFormatShort(); + break; + case DATE_TIME_FULL: + pattern = dtfi.dateTimeFull(dtfi.timeFormatFull(), + dtfi.dateFormatFull()); + break; + case DATE_TIME_LONG: + pattern = dtfi.dateTimeLong(dtfi.timeFormatLong(), + dtfi.dateFormatLong()); + break; + case DATE_TIME_MEDIUM: + pattern = dtfi.dateTimeMedium(dtfi.timeFormatMedium(), + dtfi.dateFormatMedium()); + break; + case DATE_TIME_SHORT: + pattern = dtfi.dateTimeShort(dtfi.timeFormatShort(), + dtfi.dateFormatShort()); + break; + case DAY: + pattern = dtfi.formatDay(); + break; + case HOUR24_MINUTE: + pattern = dtfi.formatHour24Minute(); + break; + case HOUR24_MINUTE_SECOND: + pattern = dtfi.formatHour24MinuteSecond(); + break; + case HOUR_MINUTE: + pattern = dtfi.formatHour12Minute(); + break; + case HOUR_MINUTE_SECOND: + pattern = dtfi.formatHour12MinuteSecond(); + break; + case MINUTE_SECOND: + pattern = dtfi.formatMinuteSecond(); + break; + case MONTH: + pattern = dtfi.formatMonthFull(); + break; + case MONTH_ABBR: + pattern = dtfi.formatMonthAbbrev(); + break; + case MONTH_ABBR_DAY: + pattern = dtfi.formatMonthAbbrevDay(); + break; + case MONTH_DAY: + pattern = dtfi.formatMonthFullDay(); + break; + case MONTH_NUM_DAY: + pattern = dtfi.formatMonthNumDay(); + break; + case MONTH_WEEKDAY_DAY: + pattern = dtfi.formatMonthFullWeekdayDay(); + break; + case TIME_FULL: + pattern = dtfi.timeFormatFull(); + break; + case TIME_LONG: + pattern = dtfi.timeFormatLong(); + break; + case TIME_MEDIUM: + pattern = dtfi.timeFormatMedium(); + break; + case TIME_SHORT: + pattern = dtfi.timeFormatShort(); + break; + case YEAR: + pattern = dtfi.formatYear(); + break; + case YEAR_MONTH: + pattern = dtfi.formatYearMonthFull(); + break; + case YEAR_MONTH_ABBR: + pattern = dtfi.formatYearMonthAbbrev(); + break; + case YEAR_MONTH_ABBR_DAY: + pattern = dtfi.formatYearMonthAbbrevDay(); + break; + case YEAR_MONTH_DAY: + pattern = dtfi.formatYearMonthFullDay(); + break; + case YEAR_MONTH_NUM: + pattern = dtfi.formatYearMonthNum(); + break; + case YEAR_MONTH_NUM_DAY: + pattern = dtfi.formatYearMonthNumDay(); + break; + case YEAR_MONTH_WEEKDAY_DAY: + pattern = dtfi.formatYearMonthWeekdayDay(); + break; + case YEAR_QUARTER: + pattern = dtfi.formatYearQuarterFull(); + break; + case YEAR_QUARTER_ABBR: + pattern = dtfi.formatYearQuarterShort(); + break; + default: + throw new IllegalArgumentException("Unexpected predefined format " + + predef); + } + return getFormat(pattern, dtfi); + } + + /** + * Returns a DateTimeFormat object using the specified pattern. If you need to + * format or parse repeatedly using the same pattern, it is highly recommended + * that you cache the returned DateTimeFormat object and reuse it + * rather than calling this method repeatedly. + * + *

Note that the pattern supplied is used as-is -- for example, if you + * supply "MM/dd/yyyy" as the pattern, that is the order you will get the + * fields, even in locales where the order is different. It is recommended to + * use {@link #getFormat(PredefinedFormat)} instead -- if you use this method, + * you are taking responsibility for localizing the patterns yourself. + * + * @param pattern string to specify how the date should be formatted + * @return a DateTimeFormat object that can be used for format or + * parse date/time values matching the specified pattern + * @throws IllegalArgumentException if the specified pattern could not be + * parsed + */ + public static DateTimeFormatImpl getFormat(String pattern) { + return getFormat(pattern, getDefaultDateTimeFormatInfo()); + } + + /** + * Internal factory method that provides caching. + * + * @param pattern + * @param dtfi + * @return DateTimeFormat instance + */ + protected static DateTimeFormatImpl getFormat(String pattern, + DateTimeFormatInfo dtfi) { + DateTimeFormatInfo defaultDtfi = getDefaultDateTimeFormatInfo(); + DateTimeFormatImpl dtf = null; + if (dtfi == defaultDtfi) { + dtf = cache.get(pattern); + } + if (dtf == null) { + dtf = new DateTimeFormatImpl(pattern, dtfi); + if (dtfi == defaultDtfi) { + cache.put(pattern, dtf); + } + } + return dtf; + } + + private static DateTimeFormatInfo getDefaultDateTimeFormatInfo() { + // MUSTFIX(jat): implement + return LocaleInfo.getCurrentLocale().getDateTimeFormatInfo(); + } + + /** + * Returns true if the predefined format is one that specifies always using + * English names/separators. + *

This should be a method on PredefinedFormat, but that would defeat the + * enum optimizations GWT is currently capable of. + * + * @param predef + * @return true if the specified format requires English names/separators + */ + private static boolean usesFixedEnglishStrings(PredefinedFormat predef) { + switch (predef) { + case RFC_2822: + return true; + case ISO_8601: + return true; + default: + return false; + } + } + + private final ArrayList patternParts = new ArrayList(); + + private final DateTimeFormatInfo dateTimeFormatInfo; + + private final String pattern; + + /** + * Constructs a format object using the specified pattern and the date time + * constants for the default locale. + * + * @param pattern string pattern specification + */ + protected DateTimeFormatImpl(String pattern) { + this(pattern, getDefaultDateTimeFormatInfo()); + } + + /** + * Constructs a format object using the specified pattern and user-supplied + * date time constants. + * + * @param pattern string pattern specification + * @param dtfi DateTimeFormatInfo instance to use + */ + protected DateTimeFormatImpl(String pattern, DateTimeFormatInfo dtfi) { + this.pattern = pattern; + this.dateTimeFormatInfo = dtfi; + + /* + * Even though the pattern is only compiled for use in parsing and parsing + * is far less common than formatting, the pattern is still parsed eagerly + * here to fail fast in case the pattern itself is malformed. + */ + parsePattern(pattern); + } + + /** + * Format a date object. + * + * @param date the date object being formatted + * @return string representation for this date in desired format + */ + @Override + public String format(JsDate date) { + return format(date, null); + } + + /** + * Format a date object using specified time zone. + * + * @param date the date object being formatted + * @param timeZone a TimeZone object that holds time zone information, or + * {@code null} to use the default + * @return string representation for this date in the format defined by this + * object + */ + public String format(JsDate date, TimeZone timeZone) { + // We use the Date class to calculate each date/time field in order + // to maximize performance and minimize code size. + // JavaScript only provides an API for rendering local time (in the os time + // zone). Here we want to render time in any timezone. So suppose we try to + // render the date (20:00 GMT0000, or 16:00 GMT-0400, or 12:00 GMT-0800) for + // time zone GMT-0400, and OS has time zone GMT-0800. By adding the + // difference between OS time zone (GMT-0800) and target time zone + // (GMT-0400) to "date", we end up with 16:00 GMT-0800. This date object + // has the same date/time fields (year, month, date, hour, minutes, etc) + // in GMT-0800 as original date in our target time zone (GMT-0400). We + // just need to take care of time zone display, but that's needed anyway. + + // Things get a little bit more tricky when a daylight time transition + // happens. For example, if the OS timezone is America/Los_Angeles, + // it is just impossible to have a Date represent 2006/4/2 02:30, because + // 2:00 to 3:00 on that day does not exist in US Pacific time zone because + // of the daylight time switch. + + // But we can use 2 separate date objects, one to represent 2006/4/2, one + // to represent 02:30. Of course, for the 2nd date object its date can be + // any other day in that year, except 2006/4/2. So we end up have 3 Date + // objects: one for resolving "Year, month, day", one for time within that + // day, and the original date object, which is needed for figuring out + // actual time zone offset. + + if (timeZone == null) { + timeZone = createTimeZone(date.getTimezoneOffset()); + } + int diff = (date.getTimezoneOffset() - timeZone.getOffset(date)) * 60000; + JsDate keepDate = JsDate.create(date.getTime() + diff); + JsDate keepTime = keepDate; + if (keepDate.getTimezoneOffset() != date.getTimezoneOffset()) { + if (diff > 0) { + diff -= NUM_MILLISECONDS_IN_DAY; + } else { + diff += NUM_MILLISECONDS_IN_DAY; + } + keepTime = JsDate.create(date.getTime() + diff); + } + + StringBuilder toAppendTo = new StringBuilder(64); + int j, n = pattern.length(); + for (int i = 0; i < n; ) { + char ch = pattern.charAt(i); + if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')) { + // ch is a date-time pattern character to be interpreted by subFormat(). + // Count the number of times it is repeated. + for (j = i + 1; j < n && pattern.charAt(j) == ch; ++j) { + } + subFormat(toAppendTo, ch, j - i, date, keepDate, keepTime, timeZone); + i = j; + } else if (ch == '\'') { + // Handle an entire quoted string, included embedded + // doubled apostrophes (as in 'o''clock'). + + // i points after '. + ++i; + + // If start with '', just add ' and continue. + if (i < n && pattern.charAt(i) == '\'') { + toAppendTo.append('\''); + ++i; + continue; + } + + // Otherwise add the quoted string. + boolean trailQuote = false; + while (!trailQuote) { + // j points to next ' or EOS. + j = i; + while (j < n && pattern.charAt(j) != '\'') { + ++j; + } + + if (j >= n) { + // Trailing ' (pathological). + throw new IllegalArgumentException("Missing trailing \'"); + } + + // Look ahead to detect '' within quotes. + if (j + 1 < n && pattern.charAt(j + 1) == '\'') { + ++j; + } else { + trailQuote = true; + } + toAppendTo.append(pattern.substring(i, j)); + i = j + 1; + } + } else { + // Append unquoted literal characters. + toAppendTo.append(ch); + ++i; + } + } + + return toAppendTo.toString(); + } + + /** + * Retrieve the pattern used in this DateTimeFormat object. + * + * @return pattern string + */ + @Override + public String getPattern() { + return pattern; + } + + /** + * Parses text to produce a {@link Date} value. An + * {@link IllegalArgumentException} is thrown if either the text is empty or + * if the parse does not consume all characters of the text. + *

+ * Dates are parsed leniently, so invalid dates will be wrapped around as + * needed. For example, February 30 will wrap to March 2. + * + * @param text the string being parsed + * @return a parsed date/time value + * @throws IllegalArgumentException if the entire text could not be converted + * into a number + */ + public JsDate parse(String text) throws IllegalArgumentException { + return parse(text, false); + } + + /** + * This method modifies a {@link Date} object to reflect the date that is + * parsed from an input string. + *

+ * Dates are parsed leniently, so invalid dates will be wrapped around as + * needed. For example, February 30 will wrap to March 2. + * + * @param text the string that need to be parsed + * @param start the character position in "text" where parsing should start + * @param date the date object that will hold parsed value + * @return 0 if parsing failed, otherwise the number of characters advanced + */ + public int parse(String text, int start, JsDate date) { + return parse(text, start, date, false); + } + + /** + * Parses text to produce a {@link Date} value. An + * {@link IllegalArgumentException} is thrown if either the text is empty or + * if the parse does not consume all characters of the text. + *

+ * Dates are parsed strictly, so invalid dates will result in an + * {@link IllegalArgumentException}. + * + * @param text the string being parsed + * @return a parsed date/time value + * @throws IllegalArgumentException if the entire text could not be converted + * into a number + */ + public JsDate parseStrict(String text) throws IllegalArgumentException { + return parse(text, true); + } + + /** + * This method modifies a {@link Date} object to reflect the date that is + * parsed from an input string. + *

+ * Dates are parsed strictly, so invalid dates will return 0. For example, + * February 30 will return 0 because February only has 28 days. + * + * @param text the string that need to be parsed + * @param start the character position in "text" where parsing should start + * @param date the date object that will hold parsed value + * @return 0 if parsing failed, otherwise the number of characters advanced + */ + public int parseStrict(String text, int start, JsDate date) { + return parse(text, start, date, true); + } + + /** + * @param timezoneOffset + * @return {@link TimeZone} instance + */ + protected TimeZone createTimeZone(int timezoneOffset) { + // MUSTFIX(jat): implement + return TimeZone.createTimeZone(timezoneOffset); + } + + /** + * Method append current content in buf as pattern part if there is any, and + * clear buf for next part. + * + * @param buf pattern part text specification + * @param count pattern part repeat count + */ + private void addPart(StringBuilder buf, int count) { + if (buf.length() > 0) { + patternParts.add((new PatternPart(buf.toString(), count))); + buf.setLength(0); + } + } + + /** + * Formats (0..11) Hours field according to pattern specified. + * + * @param buf where formatted string will be appended to + * @param count number of time pattern char repeats; this controls how a field + * should be formatted + * @param date hold the date object to be formatted + */ + private void format0To11Hours(StringBuilder buf, int count, JsDate date) { + int value = date.getHours() % 12; + zeroPaddingNumber(buf, value, count); + } + + /** + * Formats (0..23) Hours field according to pattern specified. + * + * @param buf where formatted string will be appended to + * @param count number of time pattern char repeats; this controls how a field + * should be formatted + * @param date hold the date object to be formatted + */ + private void format0To23Hours(StringBuilder buf, int count, JsDate date) { + int value = date.getHours(); + zeroPaddingNumber(buf, value, count); + } + + /** + * Formats (1..12) Hours field according to pattern specified. + * + * @param buf where formatted string will be appended to + * @param count number of time pattern char repeats; this controls how a field + * should be formatted + * @param date hold the date object to be formatted + */ + private void format1To12Hours(StringBuilder buf, int count, JsDate date) { + int value = date.getHours() % 12; + if (value == 0) { + zeroPaddingNumber(buf, 12, count); + } else { + zeroPaddingNumber(buf, value, count); + } + } + + /** + * Formats (1..24) Hours field according to pattern specified. + * + * @param buf where formatted string will be appended to + * @param count number of time pattern char repeats; this controls how a field + * should be formatted + * @param date hold the date object to be formatted + */ + private void format24Hours(StringBuilder buf, int count, JsDate date) { + int value = date.getHours(); + if (value == 0) { + zeroPaddingNumber(buf, 24, count); + } else { + zeroPaddingNumber(buf, value, count); + } + } + + /** + * Formats AM/PM field according to pattern specified. + * + * @param buf where formatted string will be appended to + * @param date hold the date object to be formatted + */ + private void formatAmPm(StringBuilder buf, JsDate date) { + if (date.getHours() >= 12 && date.getHours() < 24) { + buf.append(dateTimeFormatInfo.ampms()[1]); + } else { + buf.append(dateTimeFormatInfo.ampms()[0]); + } + } + + /** + * Formats Date field according to pattern specified. + * + * @param buf where formatted string will be appended to + * @param count number of time pattern char repeats; this controls how a field + * should be formatted + * @param date hold the date object to be formatted + */ + private void formatDate(StringBuilder buf, int count, JsDate date) { + int value = date.getDate(); + zeroPaddingNumber(buf, value, count); + } + + /** + * Formats Day of week field according to pattern specified. + * + * @param buf where formatted string will be appended to + * @param count number of time pattern char repeats; this controls how a field + * should be formatted + * @param date hold the date object to be formatted + */ + private void formatDayOfWeek(StringBuilder buf, int count, JsDate date) { + int value = date.getDay(); + if (count == 5) { + buf.append(dateTimeFormatInfo.weekdaysNarrow()[value]); + } else if (count == 4) { + buf.append(dateTimeFormatInfo.weekdaysFull()[value]); + } else { + buf.append(dateTimeFormatInfo.weekdaysShort()[value]); + } + } + + /** + * Formats Era field according to pattern specified. + * + * @param buf where formatted string will be appended to + * @param count number of time pattern char repeats; this controls how a field + * should be formatted + * @param date hold the date object to be formatted + */ + private void formatEra(StringBuilder buf, int count, JsDate date) { + int value = date.getFullYear() >= -JS_START_YEAR + ? 1 + : 0; + if (count >= 4) { + buf.append(dateTimeFormatInfo.erasFull()[value]); + } else { + buf.append(dateTimeFormatInfo.erasShort()[value]); + } + } + + /** + * Formats Fractional seconds field according to pattern specified. + * + * @param buf where formatted string will be appended to + * @param count number of time pattern char repeats; this controls how a field + * should be formatted + * @param date hold the date object to be formatted + */ + private void formatFractionalSeconds(StringBuilder buf, int count, JsDate date) { + /* + * Fractional seconds should be left-justified, ie. zero must be padded from + * left. For example, if the value in milliseconds is 5, and the count is 3, + * the output will be "005". + * + * Values with less than three digits are rounded to the desired number of + * places, but the rounded values are truncated at 9 or 99 in order to avoid + * changing the values of seconds. + */ + double time = date.getTime(); + int value; + if (time < 0) { + value = 1000 - (int) (-time % 1000); + if (value == 1000) { + value = 0; + } + } else { + value = (int) (time % 1000); + } + if (count == 1) { + value = Math.min((value + 50) / 100, 9); // Round to 100ms, clamp to 9 + buf.append((char) ('0' + value)); + } else if (count == 2) { + value = Math.min((value + 5) / 10, 99); // Round to 10ms, clamp to 99 + zeroPaddingNumber(buf, value, 2); + } else { + zeroPaddingNumber(buf, value, 3); + + if (count > 3) { + zeroPaddingNumber(buf, 0, count - 3); + } + } + } + + /** + * Formats Minutes field according to pattern specified. + * + * @param buf where formatted string will be appended to + * @param count number of time pattern char repeats; this controls how a field + * should be formatted + * @param date hold the date object to be formatted + */ + private void formatMinutes(StringBuilder buf, int count, JsDate date) { + int value = date.getMinutes(); + zeroPaddingNumber(buf, value, count); + } + + /** + * Formats Month field according to pattern specified. + * + * @param buf where formatted string will be appended to + * @param count number of time pattern char repeats; this controls how a field + * should be formatted + * @param date hold the date object to be formatted + */ + private void formatMonth(StringBuilder buf, int count, JsDate date) { + int value = date.getMonth(); + switch (count) { + case 5: + buf.append(dateTimeFormatInfo.monthsNarrow()[value]); + break; + case 4: + buf.append(dateTimeFormatInfo.monthsFull()[value]); + break; + case 3: + buf.append(dateTimeFormatInfo.monthsShort()[value]); + break; + default: + zeroPaddingNumber(buf, value + 1, count); + } + } + + /** + * Formats Quarter field according to pattern specified. + * + * @param buf where formatted string will be appended to + * @param count number of time pattern char repeats; this controls how a field + * should be formatted + * @param date hold the date object to be formatted + */ + private void formatQuarter(StringBuilder buf, int count, JsDate date) { + int value = date.getMonth() / 3; + if (count < 4) { + buf.append(dateTimeFormatInfo.quartersShort()[value]); + } else { + buf.append(dateTimeFormatInfo.quartersFull()[value]); + } + } + + /** + * Formats Seconds field according to pattern specified. + * + * @param buf where formatted string will be appended to + * @param count number of time pattern char repeats; this controls how a field + * should be formatted + * @param date hold the date object to be formatted + */ + private void formatSeconds(StringBuilder buf, int count, JsDate date) { + int value = date.getSeconds(); + zeroPaddingNumber(buf, value, count); + } + + /** + * Formats Standalone weekday field according to pattern specified. + * + * @param buf where formatted string will be appended to + * @param count number of time pattern char repeats; this controls how a field + * should be formatted + * @param date hold the date object to be formatted + */ + private void formatStandaloneDay(StringBuilder buf, int count, JsDate date) { + int value = date.getDay(); + if (count == 5) { + buf.append(dateTimeFormatInfo.weekdaysNarrowStandalone()[value]); + } else if (count == 4) { + buf.append(dateTimeFormatInfo.weekdaysFullStandalone()[value]); + } else if (count == 3) { + buf.append(dateTimeFormatInfo.weekdaysShortStandalone()[value]); + } else { + zeroPaddingNumber(buf, value, 1); + } + } + + /** + * Formats Standalone Month field according to pattern specified. + * + * @param buf where formatted string will be appended to + * @param count number of time pattern char repeats; this controls how a field + * should be formatted + * @param date hold the date object to be formatted + */ + private void formatStandaloneMonth(StringBuilder buf, int count, JsDate date) { + int value = date.getMonth(); + if (count == 5) { + buf.append(dateTimeFormatInfo.monthsNarrowStandalone()[value]); + } else if (count == 4) { + buf.append(dateTimeFormatInfo.monthsFullStandalone()[value]); + } else if (count == 3) { + buf.append(dateTimeFormatInfo.monthsShortStandalone()[value]); + } else { + zeroPaddingNumber(buf, value + 1, count); + } + } + + /** + * Formats Timezone field. + * + * @param buf where formatted string will be appended to + * @param count number of time pattern char repeats; this controls how a field + * should be formatted + * @param date hold the date object to be formatted + */ + private void formatTimeZone(StringBuilder buf, int count, JsDate date, + TimeZone timeZone) { + if (count < 4) { + buf.append(timeZone.getShortName(date)); + } else { + buf.append(timeZone.getLongName(date)); + } + } + + /** + * Formats Timezone field following RFC. + * + * @param buf where formatted string will be appended to + * @param count number of time pattern char repeats; this controls how a field + * should be formatted + * @param date hold the date object to be formatted + */ + private void formatTimeZoneRFC(StringBuilder buf, int count, JsDate date, + TimeZone timeZone) { + if (count < 3) { + buf.append(timeZone.getRFCTimeZoneString(date)); + } else if (count == 3) { + buf.append(timeZone.getISOTimeZoneString(date)); + } else { + buf.append(timeZone.getGMTString(date)); + } + } + + /** + * Formats Year field according to pattern specified. Javascript Date object + * seems incapable handling 1BC and year before. It can show you year 0 which + * does not exists. following we just keep consistent with javascript's + * toString method. But keep in mind those things should be unsupported. + * + * @param buf where formatted string will be appended to + * @param count number of time pattern char repeats; this controls how a field + * should be formatted; 2 is treated specially with the last two digits of + * the year, while more than 2 digits are zero-padded + * @param date hold the date object to be formatted + */ + private void formatYear(StringBuilder buf, int count, JsDate date) { + int value = date.getFullYear(); + if (value < 0) { + value = -value; + } + switch (count) { + case 1: // no padding + buf.append(value); + break; + case 2: // last 2 digits of year, zero-padded + zeroPaddingNumber(buf, value % 100, 2); + break; + default: // anything else is zero-padded + zeroPaddingNumber(buf, value, count); + break; + } + } + + /** + * Method getNextCharCountInPattern calculate character repeat count in + * pattern. + * + * @param pattern describe the format of date string that need to be parsed + * @param start the position of pattern character + * @return repeat count + */ + private int getNextCharCountInPattern(String pattern, int start) { + char ch = pattern.charAt(start); + int next = start + 1; + while (next < pattern.length() && pattern.charAt(next) == ch) { + ++next; + } + return next - start; + } + + /** + * Method identifies the start of a run of abutting numeric fields. Take the + * pattern "HHmmss" as an example. We will try to parse 2/2/2 characters of + * the input text, then if that fails, 1/2/2. We only adjust the width of the + * leftmost field; the others remain fixed. This allows "123456" => 12:34:56, + * but "12345" => 1:23:45. Likewise, for the pattern "yyyyMMdd" we try 4/2/2, + * 3/2/2, 2/2/2, and finally 1/2/2. The first field of connected numeric + * fields will be marked as abutStart, its width can be reduced to accommodate + * others. + */ + private void identifyAbutStart() { + // 'abut' parts are continuous numeric parts. abutStart is the switch + // point from non-abut to abut. + boolean abut = false; + + int len = patternParts.size(); + for (int i = 0; i < len; i++) { + if (isNumeric(patternParts.get(i))) { + // If next part is not following abut sequence, and isNumeric. + if (!abut && i + 1 < len && isNumeric(patternParts.get(i + 1))) { + abut = true; + patternParts.get(i).abutStart = true; + } + } else { + abut = false; + } + } + } + + /** + * Method checks if the pattern part is a numeric field. + * + * @param part pattern part to be examined + * @return true if the pattern part is numberic field + */ + private boolean isNumeric(PatternPart part) { + if (part.count <= 0) { + return false; + } + int i = NUMERIC_FORMAT_CHARS.indexOf(part.text.charAt(0)); + // M & L (index 0 and 1) are only numeric if there are less than 3 chars + return (i > 1 || (i >= 0 && part.count < 3)); + } + + /** + * Method attempts to match the text at a given position against an array of + * strings. Since multiple strings in the array may match (for example, if the + * array contains "a", "ab", and "abc", all will match the input string + * "abcd") the longest match is returned. + * + * @param text the time text being parsed + * @param start where to start parsing + * @param data the string array to parsed + * @param pos to receive where the match stopped + * @return the new start position if matching succeeded; a negative number + * indicating matching failure + */ + private int matchString(String text, int start, String[] data, int[] pos) { + int count = data.length; + + // There may be multiple strings in the data[] array which begin with + // the same prefix (e.g., Cerven and Cervenec (June and July) in Czech). + // We keep track of the longest match, and return that. Note that this + // unfortunately requires us to test all array elements. + int bestMatchLength = 0, bestMatch = -1; + String textInLowerCase = text.substring(start).toLowerCase(Locale.ROOT); + for (int i = 0; i < count; ++i) { + int length = data[i].length(); + // Always compare if we have no match yet; otherwise only compare + // against potentially better matches (longer strings). + if (length > bestMatchLength + && textInLowerCase.startsWith(data[i].toLowerCase(Locale.ROOT))) { + bestMatch = i; + bestMatchLength = length; + } + } + if (bestMatch >= 0) { + pos[0] = start + bestMatchLength; + } + return bestMatch; + } + + /** + * Parses text to produce a {@link JsDate} value. An + * {@link IllegalArgumentException} is thrown if either the text is empty or + * if the parse does not consume all characters of the text. + *

+ * If using lenient parsing, certain invalid dates and times will be parsed. + * For example, February 32nd would be parsed as March 4th in lenient mode, + * but would throw an exception in non-lenient mode. + * + * @param text the string being parsed + * @param strict true to be strict when parsing, false to be lenient + * @return a parsed date/time value + * @throws IllegalArgumentException if the entire text could not be converted + * into a number + */ + private JsDate parse(String text, boolean strict) { + JsDate curDate = JsDate.create(); + JsDate date = JsDate.create(curDate.getFullYear(), curDate.getMonth(), + curDate.getDate()); + int charsConsumed = parse(text, 0, date, strict); + if (charsConsumed == 0 || charsConsumed < text.length()) { + throw new IllegalArgumentException(text); + } + return date; + } + + /** + * This method parses the input string and fills its value into a {@link JsDate} + * . + *

+ * If using lenient parsing, certain invalid dates and times will be parsed. + * For example, February 32nd would be parsed as March 4th in lenient mode, + * but would return 0 in non-lenient mode. + * + * @param text the string that need to be parsed + * @param start the character position in "text" where parsing should start + * @param date the date object that will hold parsed value + * @param strict true to be strict when parsing, false to be lenient + * @return 0 if parsing failed, otherwise the number of characters advanced + */ + private int parse(String text, int start, JsDate date, boolean strict) { + DateRecord cal = new DateRecord(); + int[] parsePos = {start}; + + // For parsing abutting numeric fields. 'abutPat' is the + // offset into 'pattern' of the first of 2 or more abutting + // numeric fields. 'abutStart' is the offset into 'text' + // where parsing the fields begins. 'abutPass' starts off as 0 + // and increments each time we try to parse the fields. + int abutPat = -1; // If >=0, we are in a run of abutting numeric fields. + int abutStart = 0; + int abutPass = 0; + + for (int i = 0; i < patternParts.size(); ++i) { + PatternPart part = patternParts.get(i); + + if (part.count > 0) { + if (abutPat < 0 && part.abutStart) { + abutPat = i; + abutStart = parsePos[0]; + abutPass = 0; + } + + // Handle fields within a run of abutting numeric fields. Take + // the pattern "HHmmss" as an example. We will try to parse + // 2/2/2 characters of the input text, then if that fails, + // 1/2/2. We only adjust the width of the leftmost field; the + // others remain fixed. This allows "123456" => 12:34:56, but + // "12345" => 1:23:45. Likewise, for the pattern "yyyyMMdd" we + // try 4/2/2, 3/2/2, 2/2/2, and finally 1/2/2. + if (abutPat >= 0) { + // If we are at the start of a run of abutting fields, then + // shorten this field in each pass. If we can't shorten + // this field any more, then the parse of this set of + // abutting numeric fields has failed. + int count = part.count; + if (i == abutPat) { + count -= abutPass++; + if (count == 0) { + return 0; + } + } + + if (!subParse(text, parsePos, part, count, cal)) { + // If the parse fails anywhere in the run, back up to the + // start of the run and retry. + i = abutPat - 1; + parsePos[0] = abutStart; + continue; + } + } else { + // Handle non-numeric fields and non-abutting numeric fields. + abutPat = -1; + if (!subParse(text, parsePos, part, 0, cal)) { + return 0; + } + } + } else { + // Handle literal pattern characters. These are any + // quoted characters and non-alphabetic unquoted characters. + abutPat = -1; + // A run of white space in the pattern matches a run + // of white space in the input text. + if (part.text.charAt(0) == ' ') { + // Advance over run in input text. + int s = parsePos[0]; + skipSpace(text, parsePos); + + // Must see at least one white space char in input. + if (parsePos[0] > s) { + continue; + } + } else if (text.startsWith(part.text, parsePos[0])) { + parsePos[0] += part.text.length(); + continue; + } + + // We fall through to this point if the match fails. + return 0; + } + } + + // Calculate the date from the parts + if (!cal.calcDate(date, strict)) { + return 0; + } + + // Return progress. + return parsePos[0] - start; + } + + /** + * Method parses a integer string and return integer value. + * + * @param text string being parsed + * @param pos parse position + * @return integer value + */ + private int parseInt(String text, int[] pos) { + int ret = 0; + int ind = pos[0]; + if (ind >= text.length()) { + return -1; + } + char ch = text.charAt(ind); + while (ch >= '0' && ch <= '9') { + ret = ret * 10 + (ch - '0'); + ind++; + if (ind >= text.length()) { + break; + } + ch = text.charAt(ind); + } + if (ind > pos[0]) { + pos[0] = ind; + } else { + ret = -1; + } + return ret; + } + + /** + * Method parses the input pattern string a generate a vector of pattern + * parts. + * + * @param pattern describe the format of date string that need to be parsed + */ + private void parsePattern(String pattern) { + StringBuilder buf = new StringBuilder(32); + boolean inQuote = false; + + for (int i = 0; i < pattern.length(); i++) { + char ch = pattern.charAt(i); + + // Handle space, add literal part (if exist), and add space part. + if (ch == ' ') { + addPart(buf, 0); + buf.append(' '); + addPart(buf, 0); + while (i + 1 < pattern.length() && pattern.charAt(i + 1) == ' ') { + i++; + } + continue; + } + + // If inside quote, except two quote connected, just copy or exit. + if (inQuote) { + if (ch == '\'') { + if (i + 1 < pattern.length() && pattern.charAt(i + 1) == '\'') { + // Quote appeared twice continuously, interpret as one quote. + buf.append(ch); + ++i; + } else { + inQuote = false; + } + } else { + // Literal. + buf.append(ch); + } + continue; + } + + // Outside quote now. + if (PATTERN_CHARS.indexOf(ch) > 0) { + addPart(buf, 0); + buf.append(ch); + int count = getNextCharCountInPattern(pattern, i); + addPart(buf, count); + i += count - 1; + continue; + } + + // Two consecutive quotes is a quote literal, inside or outside of quotes. + if (ch == '\'') { + if (i + 1 < pattern.length() && pattern.charAt(i + 1) == '\'') { + buf.append('\''); + i++; + } else { + inQuote = true; + } + } else { + buf.append(ch); + } + } + + addPart(buf, 0); + + identifyAbutStart(); + } + + /** + * Method parses time zone offset. + * + * @param text the time text to be parsed + * @param pos Parse position + * @param cal DateRecord object that holds parsed value + * @return true if parsing successful, otherwise + * false + */ + private boolean parseTimeZoneOffset(String text, int[] pos, DateRecord cal) { + if (pos[0] >= text.length()) { + cal.setTzOffset(0); + return true; + } + + int sign; + switch (text.charAt(pos[0])) { + case '+': + sign = 1; + break; + case '-': + sign = -1; + break; + default: + cal.setTzOffset(0); + return true; + } + ++(pos[0]); + + // Look for hours:minutes or hhmm. + int st = pos[0]; + int value = parseInt(text, pos); + if (value == 0 && pos[0] == st) { + return false; + } + + int offset; + if (pos[0] < text.length() && text.charAt(pos[0]) == ':') { + // This is the hours:minutes case. + offset = value * MINUTES_PER_HOUR; + ++(pos[0]); + st = pos[0]; + value = parseInt(text, pos); + if (value == 0 && pos[0] == st) { + return false; + } + offset += value; + } else { + // This is the hhmm case. + offset = value; + // Assume "-23".."+23" refers to hours. + if (offset < 24 && (pos[0] - st) <= 2) { + offset *= MINUTES_PER_HOUR; + } else { + offset = offset % 100 + offset / 100 * MINUTES_PER_HOUR; + } + } + + offset *= sign; + cal.setTzOffset(-offset); + return true; + } + + /** + * Method skips space in the string as pointed by pos. + * + * @param text input string + * @param pos where skip start, and return back where skip stop + */ + private void skipSpace(String text, int[] pos) { + while (pos[0] < text.length() + && WHITE_SPACE.indexOf(text.charAt(pos[0])) >= 0) { + ++(pos[0]); + } + } + + /** + * Formats a single field according to pattern specified. + * + * @param ch pattern character for this field + * @param count number of time pattern char repeats; this controls how a field + * should be formatted + * @param date the date object to be formatted + * @param adjustedDate holds the time zone adjusted date fields + * @param adjustedTime holds the time zone adjusted time fields + * @return true if pattern valid, otherwise false + */ + private boolean subFormat(StringBuilder buf, char ch, int count, JsDate date, + JsDate adjustedDate, JsDate adjustedTime, TimeZone timezone) { + switch (ch) { + case 'G': + formatEra(buf, count, adjustedDate); + break; + case 'y': + formatYear(buf, count, adjustedDate); + break; + case 'M': + formatMonth(buf, count, adjustedDate); + break; + case 'k': + format24Hours(buf, count, adjustedTime); + break; + case 'S': + formatFractionalSeconds(buf, count, adjustedTime); + break; + case 'E': + formatDayOfWeek(buf, count, adjustedDate); + break; + case 'a': + formatAmPm(buf, adjustedTime); + break; + case 'h': + format1To12Hours(buf, count, adjustedTime); + break; + case 'K': + format0To11Hours(buf, count, adjustedTime); + break; + case 'H': + format0To23Hours(buf, count, adjustedTime); + break; + case 'c': + formatStandaloneDay(buf, count, adjustedDate); + break; + case 'L': + formatStandaloneMonth(buf, count, adjustedDate); + break; + case 'Q': + formatQuarter(buf, count, adjustedDate); + break; + case 'd': + formatDate(buf, count, adjustedDate); + break; + case 'm': + formatMinutes(buf, count, adjustedTime); + break; + case 's': + formatSeconds(buf, count, adjustedTime); + break; + case 'z': + formatTimeZone(buf, count, date, timezone); + break; + case 'v': + buf.append(timezone.getID()); + break; + case 'Z': + formatTimeZoneRFC(buf, count, date, timezone); + break; + default: + return false; + } + return true; + } + + /** + * Converts one field of the input string into a numeric field value. Returns + * false if failed. + * + * @param text the time text to be parsed + * @param pos Parse position + * @param part the pattern part for this field + * @param digitCount when greater than 0, numeric parsing must obey the count + * @param cal DateRecord object that will hold parsed value + * @return true if parsing successful + */ + @SuppressWarnings("fallthrough") + private boolean subParse(String text, int[] pos, PatternPart part, + int digitCount, DateRecord cal) { + + skipSpace(text, pos); + + int start = pos[0]; + char ch = part.text.charAt(0); + + // Parse integer value if it is a numeric field. + int value = -1; // initialize value to be -1, + if (isNumeric(part)) { + if (digitCount > 0) { + if ((start + digitCount) > text.length()) { + return false; + } + value = parseInt(text.substring(0, start + digitCount), pos); + } else { + value = parseInt(text, pos); + } + } + + switch (ch) { + case 'G': // era + value = matchString(text, start, dateTimeFormatInfo.erasFull(), pos); + cal.setEra(value); + return true; + case 'M': // month + return subParseMonth(text, pos, cal, value, start); + case 'L': // standalone month + return subParseStandaloneMonth(text, pos, cal, value, start); + case 'E': // day of week + return subParseDayOfWeek(text, pos, start, cal); + case 'c': // standalone day of week + return subParseStandaloneDay(text, pos, start, cal); + case 'a': // AM/PM + value = matchString(text, start, dateTimeFormatInfo.ampms(), pos); + cal.setAmpm(value); + return true; + case 'y': // year + return subParseYear(text, pos, start, value, part, cal); + case 'd': // day of month + if (value <= 0) { + return false; + } + cal.setDayOfMonth(value); + return true; + case 'S': // fractional seconds + if (value < 0) { + return false; + } + return subParseFractionalSeconds(value, start, pos[0], cal); + case 'h': // hour (1..12) + if (value == 12) { + value = 0; + } + // fall through + case 'K': // hour (0..11) + case 'H': // hour (0..23) + if (value < 0) { + return false; + } + cal.setHours(value); + cal.setMidnightIs24(false); + return true; + case 'k': // hour (1..24) + if (value < 0) { + return false; + } + cal.setHours(value); + cal.setMidnightIs24(true); + return true; + case 'm': // minute + if (value < 0) { + return false; + } + cal.setMinutes(value); + return true; + case 's': // second + if (value < 0) { + return false; + } + cal.setSeconds(value); + return true; + + case 'Z': // time zone RFC + // ISO-8601 times can have a literal Z to indicate GMT+0 + if (start < text.length() && text.charAt(start) == 'Z') { + pos[0]++; + cal.setTzOffset(0); + return true; + } + // $FALL-THROUGH$ + case 'z': // time zone offset + case 'v': // time zone generic + return subParseTimeZoneInGMT(text, start, pos, cal); + default: + return false; + } + } + + /** + * Method subParseDayOfWeek parses day of the week field. + * + * @param text the time text to be parsed + * @param pos Parse position + * @param start from where parse start + * @param cal DateRecord object that holds parsed value + * @return true if parsing successful, otherwise + * false + */ + private boolean subParseDayOfWeek(String text, int[] pos, int start, + DateRecord cal) { + int value; + // 'E' - DAY_OF_WEEK + // Want to be able to parse both short and long forms. + // Try count == 4 (DDDD) first: + value = matchString(text, start, dateTimeFormatInfo.weekdaysFull(), pos); + if (value < 0) { + value = matchString(text, start, dateTimeFormatInfo.weekdaysShort(), pos); + } + if (value < 0) { + return false; + } + cal.setDayOfWeek(value); + return true; + } + + /** + * Method subParseFractionalSeconds parses fractional seconds field. + * + * @param value parsed numberic value + * @param start + * @param end parse position + * @param cal DateRecord object that holds parsed value + * @return true if parsing successful, otherwise + * false + */ + private boolean subParseFractionalSeconds(int value, int start, int end, + DateRecord cal) { + // Fractional seconds left-justify. + int i = end - start; + if (i < 3) { + while (i < 3) { + value *= 10; + i++; + } + } else { + int a = 1; + while (i > 3) { + a *= 10; + i--; + } + value = (value + (a >> 1)) / a; + } + cal.setMilliseconds(value); + return true; + } + + /** + * Parses Month field. + * + * @param text the time text to be parsed + * @param pos Parse position + * @param cal DateRecord object that will hold parsed value + * @param value numeric value if this field is expressed using numberic + * pattern + * @param start from where parse start + * @return true if parsing successful + */ + private boolean subParseMonth(String text, int[] pos, DateRecord cal, + int value, int start) { + // When month is symbols, i.e., MMM or MMMM, value will be -1. + if (value < 0) { + // Want to be able to parse both short and long forms. + // Try count == 4 first: + value = matchString(text, start, dateTimeFormatInfo.monthsFull(), pos); + if (value < 0) { // count == 4 failed, now try count == 3. + value = matchString(text, start, dateTimeFormatInfo.monthsShort(), pos); + } + if (value < 0) { + return false; + } + cal.setMonth(value); + return true; + } else if (value > 0) { + cal.setMonth(value - 1); + return true; + } + return false; + } + + /** + * Parses standalone day of the week field. + * + * @param text the time text to be parsed + * @param pos Parse position + * @param start from where parse start + * @param cal DateRecord object that holds parsed value + * @return true if parsing successful, otherwise + * false + */ + private boolean subParseStandaloneDay(String text, int[] pos, int start, + DateRecord cal) { + int value; + // 'c' - DAY_OF_WEEK + // Want to be able to parse both short and long forms. + // Try count == 4 (cccc) first: + value = matchString(text, start, dateTimeFormatInfo.weekdaysFullStandalone(), + pos); + if (value < 0) { + value = matchString(text, start, + dateTimeFormatInfo.weekdaysShortStandalone(), pos); + } + if (value < 0) { + return false; + } + cal.setDayOfWeek(value); + return true; + } + + /** + * Parses a standalone month field. + * + * @param text the time text to be parsed + * @param pos Parse position + * @param cal DateRecord object that will hold parsed value + * @param value numeric value if this field is expressed using numberic + * pattern + * @param start from where parse start + * @return true if parsing successful + */ + private boolean subParseStandaloneMonth(String text, int[] pos, + DateRecord cal, int value, int start) { + // When month is symbols, i.e., LLL or LLLL, value will be -1. + if (value < 0) { + // Want to be able to parse both short and long forms. + // Try count == 4 first: + value = matchString(text, start, + dateTimeFormatInfo.monthsFullStandalone(), pos); + if (value < 0) { // count == 4 failed, now try count == 3. + value = matchString(text, start, + dateTimeFormatInfo.monthsShortStandalone(), pos); + } + if (value < 0) { + return false; + } + cal.setMonth(value); + return true; + } else if (value > 0) { + cal.setMonth(value - 1); + return true; + } + return false; + } + + /** + * Method parses GMT type timezone. + * + * @param text the time text to be parsed + * @param start from where parse start + * @param pos Parse position + * @param cal DateRecord object that holds parsed value + * @return true if parsing successful, otherwise + * false + */ + private boolean subParseTimeZoneInGMT(String text, int start, int[] pos, + DateRecord cal) { + // First try to parse generic forms such as GMT-07:00. Do this first + // in case localized DateFormatZoneData contains the string "GMT" + // for a zone; in that case, we don't want to match the first three + // characters of GMT+/-HH:MM etc. + + // For time zones that have no known names, look for strings + // of the form: + // GMT[+-]hours:minutes or + // GMT[+-]hhmm or + // GMT. + if (text.startsWith(GMT, start)) { + pos[0] = start + GMT.length(); + return parseTimeZoneOffset(text, pos, cal); + } + // Likewise for UTC. + if (text.startsWith(UTC, start)) { + pos[0] = start + UTC.length(); + return parseTimeZoneOffset(text, pos, cal); + } + + // At this point, check for named time zones by looking through + // the locale data from the DateFormatZoneData strings. + // Want to be able to parse both short and long forms. + /* + * i = subParseZoneString(text, start, cal); if (i != 0) return i; + */ + + // As a last resort, look for numeric timezones of the form + // [+-]hhmm as specified by RFC 822. This code is actually + // a little more permissive than RFC 822. It will try to do + // its best with numbers that aren't strictly 4 digits long. + return parseTimeZoneOffset(text, pos, cal); + } + + /** + * Method subParseYear parse year field. Year field is special because 1, two + * digit year need to be resolved. 2, we allow year to take a sign. 3, year + * field participate in abut processing. In my testing, negative year does not + * seem working due to JDK (or GWT implementation) limitation. It is not a + * big deal so we don't worry about it. But keep the logic here so that we + * might want to replace DateRecord with our a calendar class. + * + * @param text the time text to be parsed + * @param pos parse position + * @param start where this field starts + * @param value integer value of year + * @param part the pattern part for this field + * @param cal DateRecord object that will hold parsed value + * @return true if successful + */ + private boolean subParseYear(String text, int[] pos, int start, int value, + PatternPart part, DateRecord cal) { + char ch = ' '; + if (value < 0) { + if (pos[0] >= text.length()) { + return false; + } + ch = text.charAt(pos[0]); + // Check if it is a sign. + if (ch != '+' && ch != '-') { + return false; + } + ++(pos[0]); + value = parseInt(text, pos); + if (value < 0) { + return false; + } + if (ch == '-') { + value = -value; + } + } + + // no sign, only 2 digit was actually parsed, pattern say it has 2 digit. + if (ch == ' ' && (pos[0] - start) == 2 && part.count == 2) { + // Assume for example that the defaultCenturyStart is 6/18/1903. + // This means that two-digit years will be forced into the range + // 6/18/1903 to 6/17/2003. As a result, years 00, 01, and 02 + // correspond to 2000, 2001, and 2002. Years 04, 05, etc. correspond + // to 1904, 1905, etc. If the year is 03, then it is 2003 if the + // other fields specify a date before 6/18, or 1903 if they specify a + // date afterwards. As a result, 03 is an ambiguous year. All other + // two-digit years are unambiguous. + JsDate date = JsDate.create(); + int defaultCenturyStartYear = date.getFullYear() + 1900 - 80; + int ambiguousTwoDigitYear = defaultCenturyStartYear % 100; + cal.setAmbiguousYear(value == ambiguousTwoDigitYear); + value += (defaultCenturyStartYear / 100) * 100 + + (value < ambiguousTwoDigitYear + ? 100 + : 0); + } + cal.setFullYear(value); + return true; + } + + /** + * Formats a number with the specified minimum number of digits, using zero to + * fill the gap. + * + * @param buf where zero padded string will be written to + * @param value the number value being formatted + * @param minWidth minimum width of the formatted string; zero will be padded + * to reach this width + */ + private void zeroPaddingNumber(StringBuilder buf, int value, int minWidth) { + int b = NUMBER_BASE; + for (int i = 0; i < minWidth - 1; i++) { + if (value < b) { + buf.append('0'); + } + b *= NUMBER_BASE; + } + buf.append(value); + } +} diff --git a/stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/DayLabel.java b/stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/DayLabel.java new file mode 100644 index 00000000000..4240535eaac --- /dev/null +++ b/stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/DayLabel.java @@ -0,0 +1,8 @@ +package stroom.widget.datepicker.client; + +public class DayLabel extends AbstractCell { + + public DayLabel(final String text) { + setText(text); + } +} diff --git a/stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/DefaultCalendarView.java b/stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/DefaultCalendarView.java new file mode 100644 index 00000000000..bc7550f4090 --- /dev/null +++ b/stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/DefaultCalendarView.java @@ -0,0 +1,197 @@ +/* + * Copyright 2008 Google Inc. + * + * 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 stroom.widget.datepicker.client; + +import com.google.gwt.user.client.ui.HTMLTable.CellFormatter; + +/** + * Simple calendar view. Not extensible as we wish to evolve it freely over + * time. + */ +public final class DefaultCalendarView extends CalendarView { + + private final DateGrid grid = new DateGrid(); + + private JsDate firstDisplayed; + + private final JsDate lastDisplayed = JsDate.today(); + + private DateCell ariaSelectedCell; + + /** + * Constructor. + */ + public DefaultCalendarView() { + } + + @Override + public void addStyleToDate(String styleName, JsDate date) { + assert getDatePicker().isDateVisible(date) : "You tried to add style " + styleName + " to " + + date + ". The calendar is currently showing " + getFirstDate() + + " to " + getLastDate(); + final DateCell dateCell = getCell(date); + if (dateCell != null) { + dateCell.addStyleName(styleName); + } + } + + @Override + public JsDate getFirstDate() { + return firstDisplayed; + } + + @Override + public JsDate getLastDate() { + return lastDisplayed; + } + + @Override + public boolean isDateEnabled(JsDate date) { + final DateCell dateCell = getCell(date); + if (dateCell != null) { + return dateCell.isEnabled(); + } + return false; + } + + @Override + public void refresh() { + firstDisplayed = getModel().getCurrentFirstDayOfFirstWeek(); + + if (firstDisplayed.getDate() == 1) { + // show one empty week if date is Monday is the first in month. + addDays(firstDisplayed, -7); + } + + lastDisplayed.setTime(firstDisplayed.getTime()); + + for (int i = 0; i < grid.getNumCells(); i++) { + if (i != 0) { + addDays(lastDisplayed, 1); + } + final DateCell cell = grid.getCell(i); + if (cell != null) { + cell.update(lastDisplayed); + } + } + setAriaSelectedCell(null); + } + + private static void addDays(JsDate date, int days) { + CalendarUtil.addDaysToDate(date, days); + // We might hit DST transition. Try resetting back so follow up days continue showing midnight. + CalendarUtil.resetTime(date); + } + + @Override + public void removeStyleFromDate(String styleName, JsDate date) { + final DateCell dateCell = getCell(date); + if (dateCell != null) { + dateCell.removeStyleName(styleName); + } + } + + @Override + public void setAriaSelectedCell(JsDate date) { + if (ariaSelectedCell != null) { + ariaSelectedCell.setAriaSelected(false); + } + DateCell newSelectedCell = date != null + ? getCell(date) + : null; + if (newSelectedCell != null) { + newSelectedCell.setAriaSelected(true); + } + ariaSelectedCell = newSelectedCell; + } + + @Override + public void setEnabledOnDate(boolean enabled, JsDate date) { + final DateCell dateCell = getCell(date); + if (dateCell != null) { + dateCell.setEnabled(enabled); + } + } + + @Override + public void setup() { + // Preparation + CellFormatter formatter = grid.getCellFormatter(); + int weekendStartColumn = -1; + int weekendEndColumn = -1; + + // Set up the day labels. + for (int i = 0; i < CalendarModel.DAYS_IN_WEEK; i++) { + int shift = CalendarUtil.getStartingDayOfWeek(); + int dayIdx = i + shift < CalendarModel.DAYS_IN_WEEK + ? i + shift + : i + shift - CalendarModel.DAYS_IN_WEEK; + final DayLabel cell = new DayLabel(getModel().formatDayOfWeek(dayIdx)); + grid.addElement(cell); + grid.setWidget(0, i, cell); + + if (CalendarUtil.isWeekend(dayIdx)) { + formatter.setStyleName(0, i, css().weekendLabel()); + if (weekendStartColumn == -1) { + weekendStartColumn = i; + } else { + weekendEndColumn = i; + } + } else { + formatter.setStyleName(0, i, css().weekdayLabel()); + } + } + + // Set up the calendar grid. + for (int row = 1; row <= CalendarModel.WEEKS_IN_MONTH; row++) { + for (int column = 0; column < CalendarModel.DAYS_IN_WEEK; column++) { + final int index = grid.getNumCells(); + final DateCell cell = new DateCell( + this, + css(), + column == weekendStartColumn || column == weekendEndColumn, + index); + grid.addElement(cell); + grid.addCell(cell); + grid.setWidget(row, column, cell); + } + } + initWidget(grid); + grid.setStyleName(css().days()); + } + + private DateCell getCell(JsDate date) { + int index = CalendarUtil.getDaysBetween(firstDisplayed, date); + if (index < 0 || grid.getNumCells() <= index) { + return null; + } + + final DateCell cell = grid.getCell(index); + if (cell instanceof DateCell) { + final DateCell dateCell = (DateCell) cell; +// final double cellTime = dateCell.value.getTime(); +// final double time = date.getTime(); +// if (cellTime != time) { +// throw new IllegalStateException(date + " cannot be associated with cell " +// + cell + " as it has date " + dateCell.value); +// } + return dateCell; + } + + return null; + } +} diff --git a/stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/DefaultMonthSelector.java b/stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/DefaultMonthSelector.java new file mode 100644 index 00000000000..9f4263ba98b --- /dev/null +++ b/stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/DefaultMonthSelector.java @@ -0,0 +1,271 @@ +/* + * Copyright 2008 Google Inc. + * + * 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 stroom.widget.datepicker.client; + +import stroom.docref.HasDisplayValue; +import stroom.item.client.SelectionBox; +import stroom.svg.shared.SvgImage; +import stroom.widget.button.client.InlineSvgButton; +import stroom.widget.datepicker.client.IntlDateTimeFormat.FormatOptions; +import stroom.widget.datepicker.client.IntlDateTimeFormat.FormatOptions.Year; + +import com.google.gwt.dom.client.Element; +import com.google.gwt.user.client.ui.FlowPanel; + +import java.util.Objects; + +/** + * A simple {@link MonthSelector} used for the default date picker. It allows to select months and + * years but the name is not changed for backward compatibility. Also not extensible as we wish to + * evolve it freely over time. + */ + +public final class DefaultMonthSelector extends MonthSelector { + + private InlineSvgButton monthBackwards; + private InlineSvgButton monthForwards; + private FlowPanel layout; + private InlineSvgButton yearBackwards; + private InlineSvgButton yearForwards; + private SelectionBox monthSelect; + private SelectionBox yearSelect; + private int monthColumn; + + /** + * Returns the button for moving to the previous month. + */ + public Element getBackwardButtonElement() { + return monthBackwards.getElement(); + } + + /** + * Returns the button for moving to the next month. + */ + public Element getForwardButtonElement() { + return monthForwards.getElement(); + } + + /** + * Returns the button for moving to the previous year. + */ + public Element getYearBackwardButtonElement() { + return yearBackwards.getElement(); + } + + /** + * Returns the button for moving to the next year. + */ + public Element getYearForwardButtonElement() { + return yearForwards.getElement(); + } + + /** + * Returns the ListBox for selecting the month + */ + public SelectionBox getMonthSelectListBox() { + return monthSelect; + } + + /** + * Returns the ListBox for selecting the year + */ + public SelectionBox getYearSelectListBox() { + return yearSelect; + } + + @Override + protected void refresh() { + if (isDatePickerConfigChanged()) { + // if the config has changed since the last refresh, rebuild the grid + setupGrid(); + } + + setDate(getModel().getCurrentMonth()); + } + + @Override + protected void setup() { + // previous, next buttons + monthBackwards = createNavigationButton(SvgImage.ARROW_LEFT, -1, css().previousButton()); + monthForwards = createNavigationButton(SvgImage.ARROW_RIGHT, 1, css().nextButton()); + yearBackwards = createNavigationButton(SvgImage.ARROW_LEFT, -12, css().previousYearButton()); + yearForwards = createNavigationButton(SvgImage.ARROW_RIGHT, 12, css().nextYearButton()); + + // month and year selector + monthSelect = createMonthSelect(); + yearSelect = createYearSelect(); + + // Set up grid. + layout = new FlowPanel(); + layout.setStyleName(css().monthSelector()); + + setupGrid(); + + initWidget(layout); + } + + private InlineSvgButton createNavigationButton( + final SvgImage svgImage, final int noOfMonths, String styleName) { + InlineSvgButton button = new InlineSvgButton(); + button.setSvg(svgImage); + + button.addClickHandler(event -> addMonths(noOfMonths)); + + button.addStyleName(styleName); + + return button; + } + + private SelectionBox createMonthSelect() { + final SelectionBox monthListBox = new SelectionBox<>(); + + for (int i = 0; i < CalendarModel.MONTHS_IN_YEAR; i++) { + monthListBox.addItem(new IntItem(getModel().formatMonth(i), i)); + } + + monthListBox.addValueChangeHandler(e -> { + int previousMonth = getModel().getCurrentMonth().getMonth(); + int newMonth = monthListBox.getValue().getValue(); + int delta = newMonth - previousMonth; + addMonths(delta); + }); + + return monthListBox; + } + + private SelectionBox createYearSelect() { + final SelectionBox yearListBox = new SelectionBox<>(); + yearListBox.addValueChangeHandler(e -> { + int deltaYears = yearListBox.getValue().getValue() - getNoOfYearsToDisplayBefore(); + addMonths(deltaYears * CalendarModel.MONTHS_IN_YEAR); + }); + + return yearListBox; + } + + + private boolean isDatePickerConfigChanged() { + boolean isMonthCurrentlySelectable = monthSelect.getParent() != null; + boolean isYearNavigationCurrentlyEnabled = yearBackwards.getParent() != null; + + return getDatePicker().isYearAndMonthDropdownVisible() != isMonthCurrentlySelectable || + getDatePicker().isYearArrowsVisible() != isYearNavigationCurrentlyEnabled; + } + + private void setDate(JsDate date) { + if (getDatePicker().isYearAndMonthDropdownVisible()) { + // setup months dropdown + int month = date.getMonth(); + monthSelect.setValue(new IntItem(getModel().formatMonth(month), month)); + + // setup years dropdown + yearSelect.clear(); + + int year = date.getFullYear(); + int startYear = year - getNoOfYearsToDisplayBefore(); + int endYear = year + getNoOfYearsToDisplayAfter(); + + JsDate newDate = JsDate.create(); + + for (int i = startYear; i <= endYear; i++) { + newDate.setFullYear(i); + + yearSelect.addItem(new IntItem(formatYear(newDate), i)); + } + yearSelect.setValue(new IntItem(formatYear(JsDate.create(year, 0)), year)); + } else { +// layout.getElement().setInnerHTML(getModel().formatCurrentMonthAndYear()); + } + } + + private String formatYear(JsDate newDate) { + final FormatOptions yearFormat = FormatOptions.builder().year(Year.NUMERIC).build(); + return IntlDateTimeFormat.format(newDate, IntlDateTimeFormat.DEFAULT_LOCALE, yearFormat); + } + + private int getNoOfYearsToDisplayBefore() { + return (getDatePicker().getVisibleYearCount() - 1) / 2; + } + + private int getNoOfYearsToDisplayAfter() { + return getDatePicker().getVisibleYearCount() / 2; + } + + private void setupGrid() { + layout.clear(); + + // Month/Year column + if (getDatePicker().isYearAndMonthDropdownVisible()) { + final FlowPanel year = new FlowPanel(); + year.setStyleName(css().year()); + year.add(yearSelect); + year.add(yearBackwards); + year.add(yearForwards); + + final FlowPanel month = new FlowPanel(); + month.setStyleName(css().month()); + month.add(monthSelect); + month.add(monthBackwards); + month.add(monthForwards); + +// if (getModel().isMonthBeforeYear()) { +// layout.add(month); +// layout.add(year); +// } else { + layout.add(year); + layout.add(month); +// } + } + } + + public static class IntItem implements HasDisplayValue { + + private final String label; + private final int value; + + public IntItem(final String label, final int value) { + this.label = label; + this.value = value; + } + + @Override + public String getDisplayValue() { + return label; + } + + public int getValue() { + return value; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final IntItem that = (IntItem) o; + return value == that.value; + } + + @Override + public int hashCode() { + return Objects.hash(value); + } + } +} diff --git a/stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/IntlDateTimeFormat.java b/stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/IntlDateTimeFormat.java new file mode 100644 index 00000000000..3c5e7b2c955 --- /dev/null +++ b/stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/IntlDateTimeFormat.java @@ -0,0 +1,780 @@ +package stroom.widget.datepicker.client; + +import com.google.gwt.core.client.JavaScriptObject; + +import java.util.Objects; + +public class IntlDateTimeFormat { + + public static final String[] DEFAULT_LOCALE = new String[]{"default"}; + + public static class FormatOptions { + + private final String locale; + private final String calendar; + private final String numberingSystem; + private final String timeZone; + private final HourCycle hourCycle; + private final Boolean hour12; + private final Weekday weekday; + private final Era era; + private final Year year; + private final Month month; + private final Day day; + private final DayPeriod dayPeriod; + private final Hour hour; + private final Minute minute; + private final Second second; + private final Integer fractionalSecondDigits; + private final TimeZoneName timeZoneName; + private final DateStyle dateStyle; + private final TimeStyle timeStyle; + + public FormatOptions(final String locale, + final String calendar, + final String numberingSystem, + final String timeZone, + final HourCycle hourCycle, + final Boolean hour12, + final Weekday weekday, + final Era era, + final Year year, + final Month month, + final Day day, + final DayPeriod dayPeriod, + final Hour hour, + final Minute minute, + final Second second, + final Integer fractionalSecondDigits, + final TimeZoneName timeZoneName, + final DateStyle dateStyle, + final TimeStyle timeStyle) { + this.locale = locale; + this.calendar = calendar; + this.numberingSystem = numberingSystem; + this.timeZone = timeZone; + this.hourCycle = hourCycle; + this.hour12 = hour12; + this.weekday = weekday; + this.era = era; + this.year = year; + this.month = month; + this.day = day; + this.dayPeriod = dayPeriod; + this.hour = hour; + this.minute = minute; + this.second = second; + this.fractionalSecondDigits = fractionalSecondDigits; + this.timeZoneName = timeZoneName; + this.dateStyle = dateStyle; + this.timeStyle = timeStyle; + } + + public String getLocale() { + return locale; + } + + public String getCalendar() { + return calendar; + } + + public String getNumberingSystem() { + return numberingSystem; + } + + public String getTimeZone() { + return timeZone; + } + + public HourCycle getHourCycle() { + return hourCycle; + } + + public Boolean getHour12() { + return hour12; + } + + public Weekday getWeekday() { + return weekday; + } + + public Era getEra() { + return era; + } + + public Year getYear() { + return year; + } + + public Month getMonth() { + return month; + } + + public Day getDay() { + return day; + } + + public DayPeriod getDayPeriod() { + return dayPeriod; + } + + public Hour getHour() { + return hour; + } + + public Minute getMinute() { + return minute; + } + + public Second getSecond() { + return second; + } + + public Integer getFractionalSecondDigits() { + return fractionalSecondDigits; + } + + public TimeZoneName getTimeZoneName() { + return timeZoneName; + } + + public DateStyle getDateStyle() { + return dateStyle; + } + + public TimeStyle getTimeStyle() { + return timeStyle; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final FormatOptions that = (FormatOptions) o; + return Objects.equals(locale, that.locale) && + Objects.equals(calendar, that.calendar) && + Objects.equals(numberingSystem, that.numberingSystem) && + Objects.equals(timeZone, that.timeZone) && + hourCycle == that.hourCycle && + Objects.equals(hour12, that.hour12) && + weekday == that.weekday && + era == that.era && + year == that.year && + month == that.month && + day == that.day && + dayPeriod == that.dayPeriod && + hour == that.hour && + minute == that.minute && + second == that.second && + Objects.equals(fractionalSecondDigits, that.fractionalSecondDigits) && + timeZoneName == that.timeZoneName && + dateStyle == that.dateStyle && + timeStyle == that.timeStyle; + } + + @Override + public int hashCode() { + return Objects.hash(locale, + calendar, + numberingSystem, + timeZone, + hourCycle, + hour12, + weekday, + era, + year, + month, + day, + dayPeriod, + hour, + minute, + second, + fractionalSecondDigits, + timeZoneName, + dateStyle, + timeStyle); + } + + @Override + public String toString() { + return "FormatOptions{" + + "locale='" + locale + '\'' + + ", calendar='" + calendar + '\'' + + ", numberingSystem='" + numberingSystem + '\'' + + ", timeZone='" + timeZone + '\'' + + ", hourCycle=" + hourCycle + + ", hour12=" + hour12 + + ", weekday=" + weekday + + ", era=" + era + + ", year=" + year + + ", month=" + month + + ", day=" + day + + ", dayPeriod=" + dayPeriod + + ", hour=" + hour + + ", minute=" + minute + + ", second=" + second + + ", fractionalSecondDigits=" + fractionalSecondDigits + + ", timeZoneName=" + timeZoneName + + ", dateStyle=" + dateStyle + + ", timeStyle=" + timeStyle + + '}'; + } + + public Builder copy() { + return new Builder(this); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private String locale; + private String calendar; + private String numberingSystem; + private String timeZone; + private HourCycle hourCycle; + private Boolean hour12; + private Weekday weekday; + private Era era; + private Year year; + private Month month; + private Day day; + private DayPeriod dayPeriod; + private Hour hour; + private Minute minute; + private Second second; + private Integer fractionalSecondDigits; + private TimeZoneName timeZoneName; + private DateStyle dateStyle; + private TimeStyle timeStyle; + + public Builder() { + } + + public Builder(final FormatOptions options) { + this.locale = options.locale; + this.calendar = options.calendar; + this.numberingSystem = options.numberingSystem; + this.timeZone = options.timeZone; + this.hourCycle = options.hourCycle; + this.hour12 = options.hour12; + this.weekday = options.weekday; + this.era = options.era; + this.year = options.year; + this.month = options.month; + this.day = options.day; + this.dayPeriod = options.dayPeriod; + this.hour = options.hour; + this.minute = options.minute; + this.second = options.second; + this.fractionalSecondDigits = options.fractionalSecondDigits; + this.timeZoneName = options.timeZoneName; + this.dateStyle = options.dateStyle; + this.timeStyle = options.timeStyle; + } + + public Builder locale(final String locale) { + this.locale = locale; + return this; + } + + public Builder calendar(final String calendar) { + this.calendar = calendar; + return this; + } + + public Builder numberingSystem(final String numberingSystem) { + this.numberingSystem = numberingSystem; + return this; + } + + public Builder timeZone(final String timeZone) { + this.timeZone = timeZone; + return this; + } + + public Builder hourCycle(final HourCycle hourCycle) { + this.hourCycle = hourCycle; + return this; + } + + public Builder hour12(final Boolean hour12) { + this.hour12 = hour12; + return this; + } + + public Builder weekday(final Weekday weekday) { + this.weekday = weekday; + return this; + } + + public Builder era(final Era era) { + this.era = era; + return this; + } + + public Builder year(final Year year) { + this.year = year; + return this; + } + + public Builder month(final Month month) { + this.month = month; + return this; + } + + public Builder day(final Day day) { + this.day = day; + return this; + } + + public Builder dayPeriod(final DayPeriod dayPeriod) { + this.dayPeriod = dayPeriod; + return this; + } + + public Builder hour(final Hour hour) { + this.hour = hour; + return this; + } + + public Builder minute(final Minute minute) { + this.minute = minute; + return this; + } + + public Builder second(final Second second) { + this.second = second; + return this; + } + + public Builder fractionalSecondDigits(final Integer fractionalSecondDigits) { + this.fractionalSecondDigits = fractionalSecondDigits; + return this; + } + + public Builder timeZoneName(final TimeZoneName timeZoneName) { + this.timeZoneName = timeZoneName; + return this; + } + + public Builder dateStyle(final DateStyle dateStyle) { + this.dateStyle = dateStyle; + return this; + } + + public Builder timeStyle(final TimeStyle timeStyle) { + this.timeStyle = timeStyle; + return this; + } + + public FormatOptions build() { + return new FormatOptions( + locale, + calendar, + numberingSystem, + timeZone, + hourCycle, + hour12, + weekday, + era, + year, + month, + day, + dayPeriod, + hour, + minute, + second, + fractionalSecondDigits, + timeZoneName, + dateStyle, + timeStyle); + } + } + + + public enum Weekday { + NARROW("narrow"), + SHORT("short"), + LONG("long"); + + private final String name; + + Weekday(final String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + + public enum Era { + NARROW("narrow"), + SHORT("short"), + LONG("long"); + + private final String name; + + Era(final String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + + public enum Year { + TWO_DIGIT("2-digit"), + NUMERIC("numeric"); + + private final String name; + + Year(final String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + + public enum Month { + TWO_DIGIT("2-digit"), + NUMERIC("numeric"), + NARROW("narrow"), + SHORT("short"), + LONG("long"); + + private final String name; + + Month(final String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + + public enum Day { + TWO_DIGIT("2-digit"), + NUMERIC("numeric"); + + private final String name; + + Day(final String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + + public enum DayPeriod { + NARROW("narrow"), + SHORT("short"), + LONG("long"); + + private final String name; + + DayPeriod(final String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + + public enum Hour { + TWO_DIGIT("2-digit"), + NUMERIC("numeric"); + + private final String name; + + Hour(final String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + + public enum Minute { + TWO_DIGIT("2-digit"), + NUMERIC("numeric"); + + private final String name; + + Minute(final String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + + public enum Second { + TWO_DIGIT("2-digit"), + NUMERIC("numeric"); + + private final String name; + + Second(final String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + + public enum TimeZoneName { + SHORT("short"), + LONG("long"), + SHORT_OFFSET("shortOffset"), + LONG_OFFSET("longOffset"), + SHORT_GENERIC("shortGeneric"), + LONG_GENERIC("longGeneric"); + + private final String name; + + TimeZoneName(final String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + + public enum HourCycle { + H11("h11"), + H12("h12"), + H23("h23"), + H24("h24"); + + private final String name; + + HourCycle(final String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + + public enum DateStyle { + FULL("full"), + LONG("long"), + MEDIUM("medium"), + SHORT("short"); + + private final String name; + + DateStyle(final String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + + public enum TimeStyle { + FULL("full"), + LONG("long"), + MEDIUM("medium"), + SHORT("short"); + + private final String name; + + TimeStyle(final String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + } + + public static String format(final JsDate date, final String[] locales, final FormatOptions options) { + final JsFormatOptions jsFormatOptions = JsFormatOptions.create( + options.locale, + options.calendar, + options.numberingSystem, + options.timeZone, + options.hourCycle == null + ? null + : options.hourCycle.getName(), + options.hour12, + options.weekday == null + ? null + : options.weekday.getName(), + options.era == null + ? null + : options.era.getName(), + options.year == null + ? null + : options.year.getName(), + options.month == null + ? null + : options.month.getName(), + options.day == null + ? null + : options.day.getName(), + options.dayPeriod == null + ? null + : options.dayPeriod.getName(), + options.hour == null + ? null + : options.hour.getName(), + options.minute == null + ? null + : options.minute.getName(), + options.second == null + ? null + : options.second.getName(), + options.fractionalSecondDigits, + options.timeZoneName == null + ? null + : options.timeZoneName.getName(), + options.dateStyle == null + ? null + : options.dateStyle.getName(), + options.timeStyle == null + ? null + : options.timeStyle.getName()); + return format(date, locales, jsFormatOptions); + } + + private static native String format(final JsDate date, final String[] locales, final JsFormatOptions options) /*-{ + return new Intl.DateTimeFormat(locales, options).format(date); + }-*/; + + public static native String[] getTimeZones() /*-{ + return Intl.supportedValuesOf('timeZone'); + }-*/; + + public static native String getTimeZone() /*-{ + return Intl.DateTimeFormat().resolvedOptions().timeZone; + }-*/; + +// /** +// * A timezone offset in minutes +// */ +// public static native int getTimeZoneOffset(final JsDate date, final String timeZone) /*-{ +// var isoDateString = date.toISOString(); +// var utcDate = new Date(isoDateString); +// var localDateString = date.toLocaleString('en-GB', { timeZone: timeZone }); +// var tzDate = new Date(localDateString); +// return (tzDate.getTime() - utcDate.getTime()) / 6e4; +// }-*/; + + public static native int getLanguageCount() /*-{ + return navigator.languages.length; + }-*/; + + public static native String getLanguage(int i) /*-{ + return navigator.languages[i]; + }-*/; + + private static class JsFormatOptions extends JavaScriptObject { + + protected JsFormatOptions() { + } + + private static native JsFormatOptions create(final String locale, + final String calendar, + final String numberingSystem, + final String timeZone, + final String hourCycle, + final Boolean hour12, + final String weekday, + final String era, + final String year, + final String month, + final String day, + final String dayPeriod, + final String hour, + final String minute, + final String second, + final Integer fractionalSecondDigits, + final String timeZoneName, + final String dateStyle, + final String timeStyle) /*-{ + var options = {}; + if (locale) { + options.locale = locale; + } + if (calendar) { + options.calendar = calendar; + } + if (numberingSystem) { + options.numberingSystem = numberingSystem; + } + if (timeZone) { + options.timeZone = timeZone; + } + if (hourCycle) { + options.hourCycle = hourCycle; + } + if (hour12) { + options.hour12 = hour12; + } + if (weekday) { + options.weekday = weekday; + } + if (era) { + options.era = era; + } + if (year) { + options.year = year; + } + if (month) { + options.month = month; + } + if (day) { + options.day = day; + } + if (dayPeriod) { + options.dayPeriod = dayPeriod; + } + if (hour) { + options.hour = hour; + } + if (minute) { + options.minute = minute; + } + if (second) { + options.second = second; + } + if (fractionalSecondDigits) { + options.fractionalSecondDigits = fractionalSecondDigits; + } + if (timeZoneName) { + options.timeZoneName = timeZoneName; + } + if (dateStyle) { + options.dateStyle = dateStyle; + } + if (timeStyle) { + options.timeStyle = timeStyle; + } + return options; + }-*/; + } +} diff --git a/stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/JsDate.java b/stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/JsDate.java new file mode 100644 index 00000000000..bc2a3658786 --- /dev/null +++ b/stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/JsDate.java @@ -0,0 +1,689 @@ +/* + * Copyright 2010 Google Inc. + * + * 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 stroom.widget.datepicker.client; + +import com.google.gwt.core.client.JavaScriptObject; + +/** + * A simple wrapper around a native JS Date object. + */ +public class JsDate extends JavaScriptObject { + + /** + * Creates a new date set to the beginning of the current day. + */ + public static JsDate today() { + final JsDate now = create(); + return create(now.getFullYear(), now.getMonth(), now.getDate()); + } + + /** + * Creates a new date with the current time. + */ + public static native JsDate create() /*-{ + return new Date(); + }-*/; + + /** + * Creates a new date with the specified internal representation, which is the + * number of milliseconds since midnight on January 1st, 1970. This is the + * same representation returned by {@link #getTime()}. + */ + public static native JsDate create(double milliseconds) /*-{ + return new Date(milliseconds); + }-*/; + + /** + * Creates a new date using the specified values. + */ + public static native JsDate create(int year, + int month) /*-{ + return new Date(year, month); + }-*/; + + /** + * Creates a new date using the specified values. + */ + public static native JsDate create(int year, + int month, + int dayOfMonth) /*-{ + return new Date(year, month, dayOfMonth); + }-*/; + + /** + * Creates a new date using the specified values. + */ + public static native JsDate create(int year, + int month, + int dayOfMonth, + int hours) /*-{ + return new Date(year, month, dayOfMonth, hours); + }-*/; + + /** + * Creates a new date using the specified values. + */ + public static native JsDate create(int year, + int month, + int dayOfMonth, + int hours, + int minutes) /*-{ + return new Date(year, month, dayOfMonth, hours, minutes); + }-*/; + + /** + * Creates a new date using the specified values. + */ + public static native JsDate create(int year, + int month, + int dayOfMonth, + int hours, + int minutes, + int seconds) /*-{ + return new Date(year, month, dayOfMonth, hours, minutes, seconds); + }-*/; + + /** + * Creates a new date using the specified values. + */ + public static native JsDate create(int year, + int month, + int dayOfMonth, + int hours, + int minutes, + int seconds, + int millis) /*-{ + return new Date(year, month, dayOfMonth, hours, minutes, seconds, millis); + }-*/; + + /** + * Creates a new date from a string to be parsed. + */ + public static native JsDate create(String dateString) /*-{ + return new Date(dateString); + }-*/; + + /** + * Returns the numeric value corresponding to the current time - + * the number of milliseconds elapsed since 1 January 1970 00:00:00 UTC. + */ + public static native double now() /*-{ + return Date.now(); + }-*/; + + /** + * Parses a string representation of a date and time and returns the internal + * millisecond representation. If the string cannot be parsed, the returned + * value will be NaN. Use {@link Double#isNaN(double)} to check + * the result. + */ + public static native double parse(String dateString) /*-{ + return Date.parse(dateString); + }-*/; + + // CHECKSTYLE_OFF: Matching the spec. + + /** + * Returns the internal millisecond representation of the specified UTC date + * and time. + */ + public static native double UTC(int year, + int month, + int dayOfMonth, + int hours, + int minutes, + int seconds, + int millis) /*-{ + return Date.UTC(year, month, dayOfMonth, hours, minutes, seconds, millis); + }-*/; + + // CHECKSTYLE_ON + + /** + * Non directly instantiable, use one of the {@link #create()} methods. + */ + protected JsDate() { + } + + /** + * Returns the day of the month. + */ + public final native int getDate() /*-{ + return this.getDate(); + }-*/; + + /** + * Returns the day of the week, from 0 (Sunday) to 6 + * Saturday. + */ + public final native int getDay() /*-{ + return this.getDay(); + }-*/; + + /** + * Returns the four-digit year. + */ + public final native int getFullYear() /*-{ + return this.getFullYear(); + }-*/; + + /** + * Returns the hour, between 0 (midnight) and 23. + */ + public final native int getHours() /*-{ + return this.getHours(); + }-*/; + + /** + * Returns the minutes, between 0 and 59. + */ + public final native int getMinutes() /*-{ + return this.getMinutes(); + }-*/; + + /** + * Returns the month, from 0 (January) to 11 + * December. + */ + public final native int getMonth() /*-{ + return this.getMonth(); + }-*/; + + /** + * Returns the seconds, between 0 and 59. + */ + public final native int getSeconds() /*-{ + return this.getSeconds(); + }-*/; + + /** + * Returns the milliseconds, between 0 and 999. + */ + public final native int getMilliseconds() /*-{ + return this.getMilliseconds(); + }-*/; + + /** + * Returns the internal millisecond representation of the date, the number of + * milliseconds since midnight on January 1st, 1970. + */ + public final native double getTime() /*-{ + return this.getTime(); + }-*/; + + /** + * Returns the difference, in minutes, between the local and UTC + * representations of this date. The value returned is affected by whether or + * not daylight savings time would be in effect on specified date. + */ + public final native int getTimezoneOffset() /*-{ + return this.getTimezoneOffset(); + }-*/; + + /** + * Returns the day of the month, in UTC. + */ + public final native int getUTCDate() /*-{ + return this.getUTCDate(); + }-*/; + + /** + * Returns the day of the week, from 0 (Sunday) to 6 + * Saturday, in UTC. + */ + public final native int getUTCDay() /*-{ + return this.getUTCDay(); + }-*/; + + /** + * Returns the four-digit year, in UTC. + */ + public final native int getUTCFullYear() /*-{ + return this.getUTCFullYear(); + }-*/; + + /** + * Returns the hour, between 0 (midnight) and 23, in + * UTC. + */ + public final native int getUTCHours() /*-{ + return this.getUTCHours(); + }-*/; + + /** + * Returns the minutes, between 0 and 59, in UTC. + */ + public final native int getUTCMinutes() /*-{ + return this.getUTCMinutes(); + }-*/; + + /** + * Returns the month, from 0 (January) to 11 + * December, in UTC. + */ + public final native int getUTCMonth() /*-{ + return this.getUTCMonth(); + }-*/; + + /** + * Returns the seconds, between 0 and 59, in UTC. + */ + public final native int getUTCSeconds() /*-{ + return this.getUTCSeconds(); + }-*/; + + /** + * Returns the milliseconds, between 0 and 999, in + * UTC. + */ + public final native int getUTCMilliseconds() /*-{ + return this.getUTCMilliseconds(); + }-*/; + + /** + * Sets the day of the month. Returns the millisecond representation of the + * adjusted date. + */ + public final native double setDate(int dayOfMonth) /*-{ + this.setDate(dayOfMonth); + return this.getTime(); + }-*/; + + /** + * Sets the year. Returns the millisecond representation of the adjusted date. + */ + public final native double setFullYear(int year) /*-{ + this.setFullYear(year); + return this.getTime(); + }-*/; + + /** + * Sets the year and month. Returns the millisecond representation of the + * adjusted date. + */ + public final native double setFullYear(int year, + int month) /*-{ + this.setFullYear(year, month); + return this.getTime(); + }-*/; + + /** + * Sets the year, month, and day. Returns the millisecond representation of + * the adjusted date. + */ + public final native double setFullYear(int year, + int month, + int day) /*-{ + this.setFullYear(year, month, day); + return this.getTime(); + }-*/; + + /** + * Sets the hour. Returns the millisecond representation of the adjusted date. + */ + public final native double setHours(int hours) /*-{ + this.setHours(hours); + return this.getTime(); + }-*/; + + /** + * Sets the hour and minutes. Returns the millisecond representation of the + * adjusted date. + */ + public final native double setHours(int hours, + int minutes) /*-{ + this.setHours(hours, minutes); + return this.getTime(); + }-*/; + + /** + * Sets the hour, minutes, and seconds. Returns the millisecond representation + * of the adjusted date. + */ + public final native double setHours(int hours, + int minutes, + int seconds) /*-{ + this.setHours(hours, minutes, seconds); + return this.getTime(); + }-*/; + + /** + * Sets the hour, minutes, seconds, and milliseconds. Returns the millisecond + * representation of the adjusted date. + */ + public final native double setHours(int hours, + int minutes, + int seconds, + int milliseconds) /*-{ + this.setHours(hours, minutes, seconds, milliseconds); + return this.getTime(); + }-*/; + + /** + * Sets the minutes. Returns the millisecond representation of the adjusted + * date. + */ + public final native double setMinutes(int minutes) /*-{ + this.setMinutes(minutes); + return this.getTime(); + }-*/; + + /** + * Sets the minutes and seconds. Returns the millisecond representation of the + * adjusted date. + */ + public final native double setMinutes(int minutes, + int seconds) /*-{ + this.setMinutes(minutes, seconds); + return this.getTime(); + }-*/; + + /** + * Sets the minutes, seconds, and milliseconds. Returns the millisecond + * representation of the adjusted date. + */ + public final native double setMinutes(int minutes, + int seconds, + int milliseconds) /*-{ + this.setMinutes(minutes, seconds, milliseconds); + return this.getTime(); + }-*/; + + /** + * Sets the month. Returns the millisecond representation of the adjusted + * date. + */ + public final native double setMonth(int month) /*-{ + this.setMonth(month); + return this.getTime(); + }-*/; + + /** + * Sets the month and day. Returns the millisecond representation of the + * adjusted date. + */ + public final native double setMonth(int month, + int dayOfMonth) /*-{ + this.setMonth(month, dayOfMonth); + return this.getTime(); + }-*/; + + /** + * Sets the seconds. Returns the millisecond representation of the adjusted + * date. + */ + public final native double setSeconds(int seconds) /*-{ + this.setSeconds(seconds); + return this.getTime(); + }-*/; + + /** + * Sets the seconds and milliseconds. Returns the millisecond representation + * of the adjusted date. + */ + public final native double setSeconds(int seconds, + int milliseconds) /*-{ + this.setSeconds(seconds, milliseconds); + return this.getTime(); + }-*/; + + /** + * Sets the milliseconds. Returns the millisecond representation + * of the adjusted date. + */ + public final native double setMilliseconds(int milliseconds) /*-{ + this.setMilliseconds(milliseconds); + return this.getTime(); + }-*/; + + /** + * Sets the internal date representation. Returns the + * milliseconds argument. + */ + public final native double setTime(double milliseconds) /*-{ + this.setTime(milliseconds); + return this.getTime(); + }-*/; + + /** + * Sets the day of the month, in UTC. Returns the millisecond representation + * of the adjusted date. + */ + public final native double setUTCDate(int dayOfMonth) /*-{ + this.setUTCDate(dayOfMonth); + return this.getTime(); + }-*/; + + /** + * Sets the year, in UTC. Returns the millisecond representation of the + * adjusted date. + */ + public final native double setUTCFullYear(int year) /*-{ + this.setUTCFullYear(year); + return this.getTime(); + }-*/; + + /** + * Sets the year and month, in UTC. Returns the millisecond representation of + * the adjusted date. + */ + public final native double setUTCFullYear(int year, + int month) /*-{ + this.setUTCFullYear(year, month); + return this.getTime(); + }-*/; + + /** + * Sets the year, month, and day, in UTC. Returns the millisecond + * representation of the adjusted date. + */ + public final native double setUTCFullYear(int year, + int month, + int day) /*-{ + this.setUTCFullYear(year, month, day); + return this.getTime(); + }-*/; + + /** + * Sets the hour, in UTC. Returns the millisecond representation of the + * adjusted date. + */ + public final native double setUTCHours(int hours) /*-{ + this.setUTCHours(hours); + return this.getTime(); + }-*/; + + /** + * Sets the hour and minutes, in UTC. Returns the millisecond representation + * of the adjusted date. + */ + public final native double setUTCHours(int hours, + int minutes) /*-{ + this.setUTCHours(hours, minutes); + return this.getTime(); + }-*/; + + /** + * Sets the hour, minutes, and seconds, in UTC. Returns the millisecond + * representation of the adjusted date. + */ + public final native double setUTCHours(int hours, + int minutes, + int seconds) /*-{ + this.setUTCHours(hours, minutes, seconds); + return this.getTime(); + }-*/; + + /** + * Sets the hour, minutes, seconds, and milliseconds, in UTC. Returns the + * millisecond representation of the adjusted date. + */ + public final native double setUTCHours(int hours, + int minutes, + int seconds, + int milliseconds) /*-{ + this.setUTCHours(hours, minutes, seconds, milliseconds); + return this.getTime(); + }-*/; + + /** + * Sets the minutes, in UTC. Returns the millisecond representation of the + * adjusted date. + */ + public final native double setUTCMinutes(int minutes) /*-{ + this.setUTCMinutes(minutes); + return this.getTime(); + }-*/; + + /** + * Sets the minutes and seconds, in UTC. Returns the millisecond + * representation of the adjusted date. + */ + public final native double setUTCMinutes(int minutes, + int seconds) /*-{ + this.setUTCMinutes(minutes, seconds); + return this.getTime(); + }-*/; + + /** + * Sets the minutes, seconds, and milliseconds, in UTC. Returns the + * millisecond representation of the adjusted date. + */ + public final native double setUTCMinutes(int minutes, + int seconds, + int milliseconds) /*-{ + this.setUTCMinutes(minutes, seconds, milliseconds); + return this.getTime(); + }-*/; + + /** + * Sets the month, in UTC. Returns the millisecond representation of the + * adjusted date. + */ + public final native double setUTCMonth(int month) /*-{ + this.setUTCMonth(month); + return this.getTime(); + }-*/; + + /** + * Sets the month and day, in UTC. Returns the millisecond representation of + * the adjusted date. + */ + public final native double setUTCMonth(int month, + int dayOfMonth) /*-{ + this.setUTCMonth(month, dayOfMonth); + return this.getTime(); + }-*/; + + /** + * Sets the seconds, in UTC. Returns the millisecond representation of the + * adjusted date. + */ + public final native double setUTCSeconds(int seconds) /*-{ + this.setUTCSeconds(seconds); + return this.getTime(); + }-*/; + + /** + * Sets the seconds and milliseconds, in UTC. Returns the millisecond + * representation of the adjusted date. + */ + public final native double setUTCSeconds(int seconds, + int milliseconds) /*-{ + this.setUTCSeconds(seconds, milliseconds); + return this.getTime(); + }-*/; + + /** + * Sets the milliseconds, in UTC. Returns the millisecond + * representation of the adjusted date. + */ + public final native double setUTCMilliseconds(int milliseconds) /*-{ + this.setUTCMilliseconds(milliseconds); + return this.getTime(); + }-*/; + + /** + * Returns a date string in the local time zone. + */ + public final native String toDateString() /*-{ + return this.toDateString(); + }-*/; + + /** + * The toISOString() method of Date instances returns a string representing this date in the date time string + * format, a simplified format based on ISO 8601, which is always 24 or 27 characters long + * (YYYY-MM-DDTHH:mm:ss.sssZ or ±YYYYYY-MM-DDTHH:mm:ss.sssZ, respectively). + * The timezone is always UTC, as denoted by the suffix Z. + */ + public final native String toISOString() /*-{ + return this.toISOString(); + }-*/; + + /** + * The toJSON() method of Date instances returns a string representing this date in the same ISO format as + * toISOString(). + */ + public final native String toJSON() /*-{ + return this.toJSON(); + }-*/; + + /** + * Returns a date string in the local time zone according to local formatting + * conventions. + */ + public final native String toLocaleDateString() /*-{ + return this.toLocaleDateString(); + }-*/; + + /** + * Returns a date and time string in the local time zone according to local + * formatting conventions. + */ + public final native String toLocaleString() /*-{ + return this.toLocaleString(); + }-*/; + + /** + * Returns a time string in the local time zone according to local formatting + * conventions. + */ + public final native String toLocaleTimeString() /*-{ + return this.toLocaleTimeString(); + }-*/; + + /** + * Returns a time string in the local time zone. + */ + public final native String toTimeString() /*-{ + return this.toTimeString(); + }-*/; + + /** + * Returns a date and time string in UTC. + */ + public final native String toUTCString() /*-{ + return this.toUTCString(); + }-*/; + + /** + * Returns the millisecond representation, as {@link #getTime()}. + */ + public final native double valueOf() /*-{ + return this.valueOf(); + }-*/; +} diff --git a/stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/MonthSelector.java b/stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/MonthSelector.java new file mode 100644 index 00000000000..87f75aa4be6 --- /dev/null +++ b/stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/MonthSelector.java @@ -0,0 +1,24 @@ +/* + * Copyright 2008 Google Inc. + * + * 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 stroom.widget.datepicker.client; + +/** + * Abstract month selector widget. + */ +public abstract class MonthSelector extends DatePickerComponent { + +} \ No newline at end of file diff --git a/stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/TimeZone.java b/stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/TimeZone.java new file mode 100644 index 00000000000..2f45c581ac0 --- /dev/null +++ b/stroom-core-client-widget/src/main/java/stroom/widget/datepicker/client/TimeZone.java @@ -0,0 +1,275 @@ +/* + * Copyright 2008 Google Inc. + * + * 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 stroom.widget.datepicker.client; + +import com.google.gwt.core.client.JsArrayInteger; +import com.google.gwt.core.client.JsArrayString; +import com.google.gwt.i18n.client.TimeZoneInfo; + +/** + * The TimeZone class implements a time zone information source for client + * applications. The time zone object is instantiated from a TimeZoneData object, + * which is made from a JSON string that contains all the data needed for + * the specified time zone. Applications can instantiate a time zone statically, + * in which case the data could be retrieved from + * the {@link com.google.gwt.i18n.client.constants.TimeZoneConstants TimeZoneConstants} + * class. Applications can also choose to instantiate from a string obtained + * from a server. The time zone string contains locale specific data. If the + * application only uses a short representation, the English data will usually + * satisfy the user's need. In the case that only the time zone offset is known, + * there is a decent fallback that only uses the time zone offset to create a + * TimeZone object. + */ +public class TimeZone { + // constants to reference time zone names in the time zone names array + private static final int STD_SHORT_NAME = 0; + private static final int STD_LONG_NAME = 1; + private static final int DLT_SHORT_NAME = 2; + private static final int DLT_LONG_NAME = 3; + + /** + * This factory method provides a decent fallback to create a time zone object + * just based on a given time zone offset. + * + * @param timeZoneOffsetInMinutes time zone offset in minutes + * @return a new time zone object + */ + public static TimeZone createTimeZone(int timeZoneOffsetInMinutes) { + TimeZone tz = new TimeZone(); + tz.standardOffset = timeZoneOffsetInMinutes; + tz.timezoneID = composePOSIXTimeZoneID(timeZoneOffsetInMinutes); + tz.tzNames = new String[2]; + tz.tzNames[0] = composeUTCString(timeZoneOffsetInMinutes); + tz.tzNames[1] = composeUTCString(timeZoneOffsetInMinutes); + tz.transitionPoints = null; + tz.adjustments = null; + return tz; + } + + /** + * This factory method creates a time zone instance from a JSON string that + * contains the time zone information for desired time zone. Applications can + * get such a string from the TimeZoneConstants class, or it can request the + * string from the server. Either way, the application obtains the original + * string from the data provided in the TimeZoneConstant.properties file, + * which was carefully prepared from CLDR and Olson time zone database. + * + * @param tzJSON JSON string that contains time zone data + * @return a new time zone object + */ + public static TimeZone createTimeZone(String tzJSON) { + TimeZoneInfo tzData = TimeZoneInfo.buildTimeZoneData(tzJSON); + + return createTimeZone(tzData); + } + + public static TimeZone createTimeZone(TimeZoneInfo timezoneData) { + TimeZone tz = new TimeZone(); + + tz.timezoneID = timezoneData.getID(); + tz.standardOffset = -timezoneData.getStandardOffset(); + + JsArrayString jsTimezoneNames = timezoneData.getNames(); + + tz.tzNames = new String[jsTimezoneNames.length()]; + + for (int i = 0; i < jsTimezoneNames.length(); i++) { + tz.tzNames[i] = jsTimezoneNames.get(i); + } + + JsArrayInteger transitions = timezoneData.getTransitions(); + + if (transitions == null || transitions.length() == 0) { + tz.transitionPoints = null; + tz.adjustments = null; + } else { + int transitionNum = transitions.length() / 2; + tz.transitionPoints = new int[transitionNum]; + tz.adjustments = new int[transitionNum]; + + for (int i = 0; i < transitionNum; ++i) { + tz.transitionPoints[i] = transitions.get(i * 2); + tz.adjustments[i] = transitions.get(i * 2 + 1); + } + } + return tz; + } + + /** + * In GMT representation, +/- has reverse sign of time zone offset. + * when offset == 480, it should output GMT-08:00. + */ + private static String composeGMTString(int offset) { + char data[] = {'G', 'M', 'T', '-', '0', '0', ':', '0', '0'}; + if (offset <= 0) { + data[3] = '+'; + offset = -offset; // suppress the '-' sign for text display. + } + data[4] = (char) (data[4] + (offset / 60) / 10); + data[5] = (char) (data[5] + (offset / 60) % 10); + data[7] = (char) (data[7] + (offset % 60) / 10); + data[8] = (char) (data[8] + offset % 10); + return new String(data); + } + + /** + * POSIX time zone ID as fallback. + */ + private static String composePOSIXTimeZoneID(int offset) { + if (offset == 0) { + return "Etc/GMT"; + } + String str; + if (offset < 0) { + offset = -offset; + str = "Etc/GMT-"; + } else { + str = "Etc/GMT+"; + } + return str + offsetDisplay(offset); + } + + private static String composeUTCString(int offset) { + if (offset == 0) { + return "UTC"; + } + String str; + if (offset < 0) { + offset = -offset; + str = "UTC+"; + } else { + str = "UTC-"; + } + return str + offsetDisplay(offset); + } + + private static String offsetDisplay(int offset) { + int hour = offset / 60; + int mins = offset % 60; + if (mins == 0) { + return Integer.toString(hour); + } + return Integer.toString(hour) + ":" + Integer.toString(mins); + } + + private String timezoneID; + private int standardOffset; + private String[] tzNames; + private int[] transitionPoints; + private int[] adjustments; + + private TimeZone() { + } + + /* (non-Javadoc) + * @see com.google.gwt.i18n.client.TimeZoneIntf#getDaylightAdjustment(JsDate) + */ + public int getDaylightAdjustment(JsDate date) { + if (transitionPoints == null) { + return 0; + } + double timeInHours = date.getTime() / 1000 / 3600; + int index = 0; + while (index < transitionPoints.length && + timeInHours >= transitionPoints[index]) { + ++index; + } + return (index == 0) ? 0 : adjustments[index - 1]; + } + + /* (non-Javadoc) + * @see com.google.gwt.i18n.client.TimeZoneIntf#getGMTString(JsDate) + */ + public String getGMTString(JsDate date) { + return composeGMTString(getOffset(date)); + } + + /* (non-Javadoc) + * @see com.google.gwt.i18n.client.TimeZoneIntf#getID() + */ + public String getID() { + return timezoneID; + } + + /* (non-Javadoc) + * @see com.google.gwt.i18n.client.TimeZoneIntf#getISOTimeZoneString(JsDate) + */ + public String getISOTimeZoneString(JsDate date) { + int offset = -getOffset(date); + char data[] = {'+', '0', '0', ':', '0', '0'}; + if (offset < 0) { + data[0] = '-'; + offset = -offset; // suppress the '-' sign for text display. + } + data[1] = (char) (data[1] + (offset / 60) / 10); + data[2] = (char) (data[2] + (offset / 60) % 10); + data[4] = (char) (data[4] + (offset % 60) / 10); + data[5] = (char) (data[5] + offset % 10); + return new String(data); + } + + /* (non-Javadoc) + * @see com.google.gwt.i18n.client.TimeZoneIntf#getLongName(JsDate) + */ + public String getLongName(JsDate date) { + return tzNames[isDaylightTime(date) ? DLT_LONG_NAME : STD_LONG_NAME]; + } + + /* (non-Javadoc) + * @see com.google.gwt.i18n.client.TimeZoneIntf#getOffset(JsDate) + */ + public int getOffset(JsDate date) { + return standardOffset - getDaylightAdjustment(date); + } + + /* (non-Javadoc) + * @see com.google.gwt.i18n.client.TimeZoneIntf#getRFCTimeZoneString(JsDate) + */ + public String getRFCTimeZoneString(JsDate date) { + int offset = -getOffset(date); + char data[] = {'+', '0', '0', '0', '0'}; + if (offset < 0) { + data[0] = '-'; + offset = -offset; // suppress the '-' sign for text display. + } + data[1] = (char) (data[1] + (offset / 60) / 10); + data[2] = (char) (data[2] + (offset / 60) % 10); + data[3] = (char) (data[3] + (offset % 60) / 10); + data[4] = (char) (data[4] + offset % 10); + return new String(data); + } + + /* (non-Javadoc) + * @see com.google.gwt.i18n.client.TimeZoneIntf#getShortName(JsDate) + */ + public String getShortName(JsDate date) { + return tzNames[isDaylightTime(date) ? DLT_SHORT_NAME : STD_SHORT_NAME]; + } + + /* (non-Javadoc) + * @see com.google.gwt.i18n.client.TimeZoneIntf#getStandardOffset() + */ + public int getStandardOffset() { + return standardOffset; + } + + /* (non-Javadoc) + * @see com.google.gwt.i18n.client.TimeZoneIntf#isDaylightTime(JsDate) + */ + public boolean isDaylightTime(JsDate date) { + return getDaylightAdjustment(date) > 0; + } +} diff --git a/stroom-core-client-widget/src/main/resources/stroom/widget/datepicker/DatePicker.gwt.xml b/stroom-core-client-widget/src/main/resources/stroom/widget/datepicker/DatePicker.gwt.xml new file mode 100644 index 00000000000..dfeb77f7adb --- /dev/null +++ b/stroom-core-client-widget/src/main/resources/stroom/widget/datepicker/DatePicker.gwt.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/stroom-core-client/src/main/java/stroom/analytics/client/presenter/ExecutionScheduleEditPresenter.java b/stroom-core-client/src/main/java/stroom/analytics/client/presenter/ExecutionScheduleEditPresenter.java index ee757d72ecc..f3bd44b5702 100644 --- a/stroom-core-client/src/main/java/stroom/analytics/client/presenter/ExecutionScheduleEditPresenter.java +++ b/stroom-core-client/src/main/java/stroom/analytics/client/presenter/ExecutionScheduleEditPresenter.java @@ -10,7 +10,8 @@ import stroom.document.client.event.DirtyEvent.DirtyHandler; import stroom.document.client.event.HasDirtyHandlers; import stroom.explorer.client.presenter.EntityDropDownPresenter; -import stroom.job.client.presenter.SchedulePresenter; +import stroom.job.client.presenter.DateTimePopup; +import stroom.job.client.presenter.SchedulePopup; import stroom.job.shared.ScheduleReferenceTime; import stroom.job.shared.ScheduleRestriction; import stroom.node.client.NodeManager; @@ -43,38 +44,38 @@ public ExecutionScheduleEditPresenter(final EventBus eventBus, final ExecutionScheduleEditView view, final EntityDropDownPresenter errorFeedPresenter, final NodeManager nodeManager, - final Provider schedulePresenterProvider, + final Provider schedulePresenterProvider, + final Provider dateTimePopupProvider, final RestFactory restFactory) { super(eventBus, view); view.setUiHandlers(this); + view.getStartTime().setPopupProvider(dateTimePopupProvider); + view.getEndTime().setPopupProvider(dateTimePopupProvider); this.errorFeedPresenter = errorFeedPresenter; view.getScheduleBox().setSchedulePresenterProvider(schedulePresenterProvider); view.getScheduleBox().setScheduleRestriction(new ScheduleRestriction(false, false, true)); - view.getScheduleBox().setScheduleReferenceTimeConsumer(scheduleReferenceTimeConsumer -> { - final ScheduleBounds scheduleBounds = getView().getScheduleBounds(); - restFactory - .builder() - .forType(ExecutionTracker.class) - .onSuccess(tracker -> { - Long lastExecuted = null; - if (tracker != null) { - lastExecuted = tracker.getLastEffectiveExecutionTimeMs(); - } - Long referenceTime = lastExecuted; - if (referenceTime == null && scheduleBounds != null) { - referenceTime = scheduleBounds.getStartTimeMs(); - } - if (referenceTime == null) { - referenceTime = System.currentTimeMillis(); - } - - scheduleReferenceTimeConsumer.accept(new ScheduleReferenceTime(referenceTime, lastExecuted)); - }) - .call(EXECUTION_SCHEDULE_RESOURCE) - .fetchTracker(executionSchedule); + view.getScheduleBox().setScheduleReferenceTimeConsumer(scheduleReferenceTimeConsumer -> restFactory + .builder() + .forType(ExecutionTracker.class) + .onSuccess(tracker -> { + Long lastExecuted = null; + if (tracker != null) { + lastExecuted = tracker.getLastEffectiveExecutionTimeMs(); + } + Long referenceTime = lastExecuted; + if (referenceTime == null) { + referenceTime = getView().getStartTime().getLongValue(); + } + if (referenceTime == null) { + referenceTime = System.currentTimeMillis(); + } - }); + scheduleReferenceTimeConsumer.accept(new ScheduleReferenceTime(referenceTime, + lastExecuted)); + }) + .call(EXECUTION_SCHEDULE_RESOURCE) + .fetchTracker(executionSchedule)); nodeManager.listAllNodes( list -> { @@ -129,7 +130,7 @@ public void read(final ExecutionSchedule executionSchedule) { getView().setName(executionSchedule.getName()); getView().setEnabled(executionSchedule.isEnabled()); getView().setNode(executionSchedule.getNodeName()); - getView().setScheduleBounds(executionSchedule.getScheduleBounds()); + setScheduleBounds(executionSchedule.getScheduleBounds()); getView().getScheduleBox().setValue(executionSchedule.getSchedule()); } @@ -138,16 +139,25 @@ public void write(final Consumer consumer) { if (scheduledTimes.isError()) { AlertEvent.fireWarn(this, scheduledTimes.getError(), null); } else { - final ExecutionSchedule schedule = executionSchedule - .copy() - .name(getView().getName()) - .enabled(getView().isEnabled()) - .nodeName(getView().getNode()) - .schedule(scheduledTimes.getSchedule()) - .contiguous(true) - .scheduleBounds(getView().getScheduleBounds()) - .build(); - consumer.accept(schedule); + if (!getView().getStartTime().isValid()) { + AlertEvent.fireWarn(this, "Invalid start time", null); + } else if (!getView().getEndTime().isValid()) { + AlertEvent.fireWarn(this, "Invalid end time", null); + } else { + final ScheduleBounds scheduleBounds = new ScheduleBounds( + getView().getStartTime().getLongValue(), + getView().getEndTime().getLongValue()); + final ExecutionSchedule schedule = executionSchedule + .copy() + .name(getView().getName()) + .enabled(getView().isEnabled()) + .nodeName(getView().getNode()) + .schedule(scheduledTimes.getSchedule()) + .contiguous(true) + .scheduleBounds(scheduleBounds) + .build(); + consumer.accept(schedule); + } } }); } @@ -165,4 +175,14 @@ public void onDirty() { public HandlerRegistration addDirtyHandler(final DirtyHandler handler) { return addHandlerToSource(DirtyEvent.getType(), handler); } + + private void setScheduleBounds(final ScheduleBounds scheduleBounds) { + if (scheduleBounds == null) { + getView().getStartTime().setLongValue(null); + getView().getEndTime().setLongValue(null); + } else { + getView().getStartTime().setLongValue(scheduleBounds.getStartTimeMs()); + getView().getEndTime().setLongValue(scheduleBounds.getEndTimeMs()); + } + } } diff --git a/stroom-core-client/src/main/java/stroom/analytics/client/presenter/ExecutionScheduleEditView.java b/stroom-core-client/src/main/java/stroom/analytics/client/presenter/ExecutionScheduleEditView.java index 76c75517a4a..3a8af500f4e 100644 --- a/stroom-core-client/src/main/java/stroom/analytics/client/presenter/ExecutionScheduleEditView.java +++ b/stroom-core-client/src/main/java/stroom/analytics/client/presenter/ExecutionScheduleEditView.java @@ -1,6 +1,6 @@ package stroom.analytics.client.presenter; -import stroom.analytics.shared.ScheduleBounds; +import stroom.job.client.presenter.DateTimeBox; import stroom.job.client.presenter.ScheduleBox; import com.google.gwt.user.client.ui.Focus; @@ -29,7 +29,7 @@ public interface ExecutionScheduleEditView extends View, ScheduleBox getScheduleBox(); - ScheduleBounds getScheduleBounds(); + DateTimeBox getStartTime(); - void setScheduleBounds(ScheduleBounds scheduleBounds); + DateTimeBox getEndTime(); } diff --git a/stroom-core-client/src/main/java/stroom/analytics/client/view/ExecutionScheduleEditViewImpl.java b/stroom-core-client/src/main/java/stroom/analytics/client/view/ExecutionScheduleEditViewImpl.java index ea5a222cd63..bdfa68c9e52 100644 --- a/stroom-core-client/src/main/java/stroom/analytics/client/view/ExecutionScheduleEditViewImpl.java +++ b/stroom-core-client/src/main/java/stroom/analytics/client/view/ExecutionScheduleEditViewImpl.java @@ -18,10 +18,9 @@ import stroom.analytics.client.presenter.ExecutionScheduleEditView; import stroom.analytics.client.presenter.ProcessingStatusUiHandlers; -import stroom.analytics.shared.ScheduleBounds; import stroom.item.client.SelectionBox; +import stroom.job.client.presenter.DateTimeBox; import stroom.job.client.presenter.ScheduleBox; -import stroom.widget.customdatebox.client.MyDateBox; import stroom.widget.tickbox.client.view.CustomCheckBox; import com.google.gwt.event.logical.shared.ValueChangeEvent; @@ -50,9 +49,9 @@ public class ExecutionScheduleEditViewImpl @UiField ScheduleBox schedule; @UiField - MyDateBox startTime; + DateTimeBox startTime; @UiField - MyDateBox endTime; + DateTimeBox endTime; private String selectedNode; @@ -125,19 +124,13 @@ public ScheduleBox getScheduleBox() { } @Override - public ScheduleBounds getScheduleBounds() { - return new ScheduleBounds(startTime.getMilliseconds(), endTime.getMilliseconds()); + public DateTimeBox getStartTime() { + return startTime; } @Override - public void setScheduleBounds(final ScheduleBounds scheduleBounds) { - if (scheduleBounds == null) { - startTime.setValue(null); - endTime.setValue(null); - } else { - startTime.setMilliseconds(scheduleBounds.getStartTimeMs()); - endTime.setMilliseconds(scheduleBounds.getEndTimeMs()); - } + public DateTimeBox getEndTime() { + return endTime; } @UiHandler("name") diff --git a/stroom-core-client/src/main/java/stroom/job/client/presenter/DateTimeBox.java b/stroom-core-client/src/main/java/stroom/job/client/presenter/DateTimeBox.java new file mode 100644 index 00000000000..6645c76a992 --- /dev/null +++ b/stroom-core-client/src/main/java/stroom/job/client/presenter/DateTimeBox.java @@ -0,0 +1,178 @@ +package stroom.job.client.presenter; + +import stroom.item.client.EventBinder; +import stroom.svg.client.SvgIconBox; +import stroom.svg.shared.SvgImage; +import stroom.util.shared.GwtNullSafe; + +import com.google.gwt.event.dom.client.KeyCodes; +import com.google.gwt.event.logical.shared.HasValueChangeHandlers; +import com.google.gwt.event.logical.shared.ValueChangeEvent; +import com.google.gwt.event.logical.shared.ValueChangeHandler; +import com.google.gwt.user.client.ui.Composite; +import com.google.gwt.user.client.ui.Focus; +import com.google.gwt.user.client.ui.TextBox; +import com.google.inject.Provider; + +import java.util.Objects; + +public class DateTimeBox + extends Composite + implements Focus, HasValueChangeHandlers { + + private Provider popupProvider; + private final TextBox textBox; + private final SvgIconBox svgIconBox; + private String stringValue; + private Long longValue; + private DateTimePopup popup; + + private final EventBinder eventBinder = new EventBinder() { + @Override + protected void onBind() { + registerHandler(svgIconBox.addClickHandler(event -> showPopup())); + registerHandler(textBox.addKeyDownHandler(event -> { + int keyCode = event.getNativeKeyCode(); + if (KeyCodes.KEY_ENTER == keyCode) { + showPopup(); + } + })); + registerHandler(textBox.addKeyUpHandler(event -> { + final String newStringValue = GwtNullSafe.isBlankString(textBox.getValue()) + ? null + : textBox.getValue(); + if (!Objects.equals(newStringValue, stringValue)) { + stringValue = newStringValue; + ValueChangeEvent.fire(DateTimeBox.this, stringValue); + } + })); + registerHandler(textBox.addBlurHandler(event -> onBlur())); + registerHandler(textBox.addFocusHandler(event -> onFocus())); + } + }; + + private DateTimePopup getPopup() { + if (popup == null && popupProvider != null) { + popup = popupProvider.get(); + popup.setTime(System.currentTimeMillis()); + } + return popup; + } + + public DateTimeBox() { + textBox = new TextBox(); + textBox.addStyleName("ScheduleBox-textBox stroom-control allow-focus"); + + svgIconBox = new SvgIconBox(); + svgIconBox.addStyleName("ScheduleBox"); + svgIconBox.setWidget(textBox, SvgImage.CALENDAR); + + initWidget(svgIconBox); + } + + @Override + protected void onLoad() { + eventBinder.bind(); + } + + @Override + protected void onUnload() { + eventBinder.unbind(); + } + + private void showPopup() { + final DateTimePopup popup = getPopup(); + if (popup != null) { + popup.setTime(longValue); + popup.show(newValue -> { + if (!Objects.equals(longValue, newValue)) { + setLongValue(newValue, true); + } + }); + } + } + + @Override + public void focus() { + textBox.setFocus(true); + } + + public void setName(final String name) { + textBox.setName(name); + } + + public void setEnabled(final boolean enabled) { + textBox.setEnabled(enabled); + } + + public String getStringValue() { + return stringValue; + } + + public void setStringValue(final String value) { + setStringValue(value, false); + } + + public void setStringValue(final String value, final boolean fireEvents) { + this.longValue = getPopup().parse(value); + this.stringValue = value; + textBox.setValue(stringValue); + textBox.getElement().removeClassName("invalid"); + if (fireEvents) { + ValueChangeEvent.fire(this, value); + } + } + + public Long getLongValue() { + return getPopup().parse(textBox.getValue()); + } + + public void setLongValue(final Long value) { + setLongValue(value, false); + } + + public void setLongValue(final Long value, final boolean fireEvents) { + if (value != null) { + this.longValue = value; + this.stringValue = getPopup().format(value); + } else { + this.longValue = null; + this.stringValue = null; + } + textBox.setValue(stringValue); + textBox.getElement().removeClassName("invalid"); + if (fireEvents) { + ValueChangeEvent.fire(this, null); + } + } + + public boolean isValid() { + if (stringValue == null) { + return true; + } + final Long ms = getPopup().parse(stringValue); + return ms != null; + } + + private void onFocus() { + textBox.getElement().removeClassName("invalid"); + } + + private void onBlur() { + if (isValid()) { + textBox.getElement().removeClassName("invalid"); + } else { + textBox.getElement().addClassName("invalid"); + } + } + + @Override + public com.google.gwt.event.shared.HandlerRegistration addValueChangeHandler( + final ValueChangeHandler handler) { + return addHandler(handler, ValueChangeEvent.getType()); + } + + public void setPopupProvider(final Provider popupProvider) { + this.popupProvider = popupProvider; + } +} diff --git a/stroom-core-client/src/main/java/stroom/job/client/presenter/DateTimePopup.java b/stroom-core-client/src/main/java/stroom/job/client/presenter/DateTimePopup.java new file mode 100644 index 00000000000..c63569606a7 --- /dev/null +++ b/stroom-core-client/src/main/java/stroom/job/client/presenter/DateTimePopup.java @@ -0,0 +1,71 @@ +package stroom.job.client.presenter; + +import stroom.job.client.presenter.DateTimePopup.DateTimeView; +import stroom.preferences.client.DateTimeFormatter; +import stroom.widget.popup.client.event.ShowPopupEvent; +import stroom.widget.popup.client.presenter.PopupSize; +import stroom.widget.popup.client.presenter.PopupType; +import stroom.widget.popup.client.presenter.Size; + +import com.google.gwt.user.client.ui.Focus; +import com.google.inject.Inject; +import com.google.web.bindery.event.shared.EventBus; +import com.gwtplatform.mvp.client.MyPresenterWidget; +import com.gwtplatform.mvp.client.View; + +import java.util.function.Consumer; + +public class DateTimePopup extends MyPresenterWidget { + + private final DateTimeFormatter dateTimeFormatter; + + @Inject + public DateTimePopup(final EventBus eventBus, + final DateTimeView view, + final DateTimeFormatter dateTimeFormatter) { + super(eventBus, view); + this.dateTimeFormatter = dateTimeFormatter; + } + + public void show(final Consumer consumer) { + ShowPopupEvent.builder(this) + .popupType(PopupType.OK_CANCEL_DIALOG) + .popupSize(PopupSize.resizable(400, 430, 300, 430)) + .caption("Set Date And Time") + .onShow(e -> getView().focus()) + .onHideRequest(e -> { + // This method is overwritten so that we can validate the schedule + // before saving. Getting the scheduled times acts as validation. + if (e.isOk()) { + consumer.accept(getTime()); + e.hide(); + } else { + e.hide(); + } + }) + .fire(); + } + + public long getTime() { + return getView().getTime(); + } + + public void setTime(final long ms) { + getView().setTime(ms); + } + + public Long parse(final String dateTime) { + return dateTimeFormatter.parse(dateTime); + } + + public String format(final long ms) { + return dateTimeFormatter.format(ms); + } + + public interface DateTimeView extends View, Focus { + + long getTime(); + + void setTime(long time); + } +} diff --git a/stroom-core-client/src/main/java/stroom/job/client/presenter/JobNodeListPresenter.java b/stroom-core-client/src/main/java/stroom/job/client/presenter/JobNodeListPresenter.java index 9c75dad9d89..147df3a03be 100644 --- a/stroom-core-client/src/main/java/stroom/job/client/presenter/JobNodeListPresenter.java +++ b/stroom-core-client/src/main/java/stroom/job/client/presenter/JobNodeListPresenter.java @@ -70,7 +70,7 @@ public class JobNodeListPresenter extends MyPresenterWidget { private final RestFactory restFactory; private final DateTimeFormatter dateTimeFormatter; - private final SchedulePresenter schedulePresenter; + private final SchedulePopup schedulePresenter; private final UiConfigCache clientPropertyCache; private final RestDataProvider> dataProvider; @@ -86,7 +86,7 @@ public JobNodeListPresenter(final EventBus eventBus, final PagerView view, final RestFactory restFactory, final DateTimeFormatter dateTimeFormatter, - final SchedulePresenter schedulePresenter, + final SchedulePopup schedulePresenter, final UiConfigCache clientPropertyCache) { super(eventBus, view); this.restFactory = restFactory; diff --git a/stroom-core-client/src/main/java/stroom/job/client/presenter/ScheduleBox.java b/stroom-core-client/src/main/java/stroom/job/client/presenter/ScheduleBox.java index c2576bd878f..bf1a8cb3f65 100644 --- a/stroom-core-client/src/main/java/stroom/job/client/presenter/ScheduleBox.java +++ b/stroom-core-client/src/main/java/stroom/job/client/presenter/ScheduleBox.java @@ -25,14 +25,14 @@ public class ScheduleBox extends Composite implements Focus, HasValueChangeHandlers { - private Provider schedulePresenterProvider; + private Provider schedulePresenterProvider; private final TextBox textBox; private final SvgIconBox svgIconBox; private Schedule value = Schedule .builder() .type(ScheduleType.CRON) .build(); - private SchedulePresenter popup; + private SchedulePopup popup; private ScheduleRestriction scheduleRestriction = new ScheduleRestriction(false, true, true); private Consumer> scheduleReferenceTimeConsumer = (consumer) -> @@ -61,7 +61,7 @@ public void validate() { public void validate(final Consumer consumer) { final Schedule schedule = value.copy().expression(textBox.getValue()).build(); - final SchedulePresenter popup = getSchedulePresenter(); + final SchedulePopup popup = getSchedulePresenter(); if (popup != null) { schedulePresenterProvider.get().validate(schedule, scheduleRestriction, scheduledTimes -> { if (scheduledTimes.isError()) { @@ -102,12 +102,12 @@ protected void onUnload() { eventBinder.unbind(); } - public void setSchedulePresenterProvider(final Provider schedulePresenterProvider) { + public void setSchedulePresenterProvider(final Provider schedulePresenterProvider) { this.schedulePresenterProvider = schedulePresenterProvider; } private void showPopup() { - final SchedulePresenter popup = getSchedulePresenter(); + final SchedulePopup popup = getSchedulePresenter(); if (popup != null) { value = value.copy().expression(textBox.getValue()).build(); popup.validate(value, @@ -127,7 +127,7 @@ private void showPopup() { } } - private SchedulePresenter getSchedulePresenter() { + private SchedulePopup getSchedulePresenter() { if (popup == null && schedulePresenterProvider != null) { popup = schedulePresenterProvider.get(); } diff --git a/stroom-core-client/src/main/java/stroom/job/client/presenter/SchedulePresenter.java b/stroom-core-client/src/main/java/stroom/job/client/presenter/SchedulePopup.java similarity index 95% rename from stroom-core-client/src/main/java/stroom/job/client/presenter/SchedulePresenter.java rename to stroom-core-client/src/main/java/stroom/job/client/presenter/SchedulePopup.java index bcd3419a9ae..f623f130ae3 100644 --- a/stroom-core-client/src/main/java/stroom/job/client/presenter/SchedulePresenter.java +++ b/stroom-core-client/src/main/java/stroom/job/client/presenter/SchedulePopup.java @@ -42,8 +42,8 @@ import java.util.function.Consumer; -public class SchedulePresenter - extends MyPresenterWidget +public class SchedulePopup + extends MyPresenterWidget implements ScheduleUiHandlers { private final ScheduledTimeClient scheduledTimeClient; @@ -60,10 +60,10 @@ public class SchedulePresenter @Inject - public SchedulePresenter(final EventBus eventBus, - final ScheduleView view, - final ScheduledTimeClient scheduledTimeClient, - final DateTimeFormatter dateTimeFormatter) { + public SchedulePopup(final EventBus eventBus, + final ScheduleView view, + final ScheduledTimeClient scheduledTimeClient, + final DateTimeFormatter dateTimeFormatter) { super(eventBus, view); this.scheduledTimeClient = scheduledTimeClient; this.dateTimeFormatter = dateTimeFormatter; diff --git a/stroom-core-client/src/main/java/stroom/job/client/view/DateTimeViewImpl.java b/stroom-core-client/src/main/java/stroom/job/client/view/DateTimeViewImpl.java new file mode 100644 index 00000000000..a006066b0e6 --- /dev/null +++ b/stroom-core-client/src/main/java/stroom/job/client/view/DateTimeViewImpl.java @@ -0,0 +1,333 @@ +/* + * Copyright 2016 Crown Copyright + * + * 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 stroom.job.client.view; + +import stroom.item.client.SelectionBox; +import stroom.job.client.presenter.DateTimePopup.DateTimeView; +import stroom.widget.datepicker.client.CustomDatePicker; +import stroom.widget.datepicker.client.IntlDateTimeFormat; +import stroom.widget.datepicker.client.IntlDateTimeFormat.FormatOptions; +import stroom.widget.datepicker.client.IntlDateTimeFormat.FormatOptions.Day; +import stroom.widget.datepicker.client.IntlDateTimeFormat.FormatOptions.Hour; +import stroom.widget.datepicker.client.IntlDateTimeFormat.FormatOptions.Minute; +import stroom.widget.datepicker.client.IntlDateTimeFormat.FormatOptions.Month; +import stroom.widget.datepicker.client.IntlDateTimeFormat.FormatOptions.Second; +import stroom.widget.datepicker.client.IntlDateTimeFormat.FormatOptions.TimeZoneName; +import stroom.widget.datepicker.client.IntlDateTimeFormat.FormatOptions.Year; +import stroom.widget.datepicker.client.JsDate; +import stroom.widget.valuespinner.client.ValueSpinner; + +import com.google.gwt.event.logical.shared.ValueChangeEvent; +import com.google.gwt.uibinder.client.UiBinder; +import com.google.gwt.uibinder.client.UiField; +import com.google.gwt.uibinder.client.UiHandler; +import com.google.gwt.user.client.ui.Label; +import com.google.gwt.user.client.ui.Widget; +import com.google.inject.Inject; +import com.gwtplatform.mvp.client.ViewImpl; + +public class DateTimeViewImpl extends ViewImpl implements DateTimeView { + + private final Widget widget; + + @UiField + Label date; + @UiField + CustomDatePicker datePicker; + @UiField + Label time; + @UiField + ValueSpinner hour; + @UiField + ValueSpinner minute; + @UiField + ValueSpinner second; + @UiField + ValueSpinner millisecond; + @UiField + SelectionBox timeZone; + + @Inject + public DateTimeViewImpl(final Binder binder) { + widget = binder.createAndBindUi(this); + datePicker.setCurrentMonth(JsDate.create()); +// datePicker.setVisibleYearCount(100); + datePicker.setYearAndMonthDropdownVisible(true); + datePicker.setYearArrowsVisible(true); + + hour.setMin(0); + hour.setMax(23); + minute.setMin(0); + minute.setMax(59); + second.setMin(0); + second.setMax(59); + millisecond.setMin(0); + millisecond.setMax(999); + + timeZone.addItems(IntlDateTimeFormat.getTimeZones()); + timeZone.setValue(IntlDateTimeFormat.getTimeZone()); + } + + private JsDate getCurrentDate() { + final JsDate date = JsDate.create(datePicker.getValue().getTime()); + date.setHours(hour.getIntValue()); + date.setMinutes(minute.getIntValue()); + date.setSeconds(second.getIntValue()); + date.setMilliseconds(millisecond.getIntValue()); + +// for (final TimeZoneName timeZoneName : TimeZoneName.values()) { +// final String offset = IntlDateTimeFormat.format(date, IntlDateTimeFormat.DEFAULT_LOCALE, +// FormatOptions +// .builder() +// .locale("en-GB") +// .timeZone(timeZone.getValue()) +// .hour(Hour.TWO_DIGIT) +// .minute(Minute.TWO_DIGIT) +// .timeZoneName(timeZoneName) +// .build()); +// GWT.log(timeZoneName + " = " + offset); +// } + + final String offset2 = IntlDateTimeFormat.format(date, IntlDateTimeFormat.DEFAULT_LOCALE, + FormatOptions + .builder() + .locale("en-GB") + .timeZone(timeZone.getValue()) + .hour(Hour.TWO_DIGIT) + .minute(Minute.TWO_DIGIT) + .timeZoneName(TimeZoneName.SHORT_OFFSET) + .build()); + final int index = offset2.lastIndexOf(" "); + if (index != -1) { + String offsetString = offset2.substring(index + 1); + final int plusIndex = offsetString.indexOf("+"); + final int minusIndex = offsetString.indexOf("-"); + Boolean plus = null; + if (plusIndex != -1) { + offsetString = offsetString.substring(plusIndex + 1); + plus = true; + } else if (minusIndex != -1) { + offsetString = offsetString.substring(minusIndex + 1); + plus = false; + } + if (plus != null) { + final int separatorIndex = offsetString.indexOf(":"); + final int milliseconds; + if (separatorIndex != -1) { + final String hourString = offsetString.substring(0, separatorIndex); + final String minuteString = offsetString.substring(separatorIndex + 1); + final int hours = Integer.parseInt(hourString); + final int minutes = Integer.parseInt(minuteString); + milliseconds = (hours * 60 * 60 * 1000) + (minutes * 60 * 1000); + } else { + final int hours = Integer.parseInt(offsetString); + milliseconds = hours * 60 * 60 * 1000; + } + + if (plus) { + date.setTime(date.getTime() - milliseconds); + } else { + date.setTime(date.getTime() + milliseconds); + } + } + } + + +// final String offset2 = IntlDateTimeFormat.format(date, IntlDateTimeFormat.DEFAULT_LOCALE, +// FormatOptions +// .builder() +// .locale("en-GB") +// .timeZone(timeZone.getValue()) +// .hour(Hour.TWO_DIGIT) +// .minute(Minute.TWO_DIGIT) +// .timeZoneName(TimeZoneName.SHORT_OFFSET) +// .build()); +// GWT.log("offset2 = " + offset2); + +// final int offset3 = IntlDateTimeFormat.getTimeZoneOffset(date, timeZone.getValue()); +// GWT.log("offset3 = " + offset3); + + return date; + } + + @Override + public long getTime() { + return (long) getCurrentDate().getTime(); + } + + @Override + public void setTime(final long time) { + final JsDate current = JsDate.create(time); + hour.setValue(current.getHours()); + minute.setValue(current.getMinutes()); + second.setValue(current.getSeconds()); + millisecond.setValue(current.getMilliseconds()); + + current.setHours(0, 0, 0, 0); + datePicker.setCurrentMonth(current); + datePicker.setValue(current); + + updateDateLabel(); + updateTimeLabel(); + } + + // +// private void enterFrequencyMode() { +// final FlowPanel minute = createPanel("Minute"); +// for (final FrequencyExpression cronExpression : FrequencyExpressions.MINUTE) { +// minute.add(createLabel(cronExpression)); +// } +// +// final FlowPanel hour = createPanel("Hour"); +// for (final FrequencyExpression cronExpression : FrequencyExpressions.HOUR) { +// hour.add(createLabel(cronExpression)); +// } +// +// final FlowPanel quickSettingsPanel = new FlowPanel(); +// quickSettingsPanel.setStyleName("timeRange-quickSettings"); +// quickSettingsPanel.add(minute); +// quickSettingsPanel.add(hour); +// +// this.quickSettings.setWidget(quickSettingsPanel); +// } +// +// private void enterCronMode() { +// final FlowPanel minute = createPanel("Minute"); +// for (final CronExpression cronExpression : CronExpressions.MINUTE) { +// minute.add(createLabel(cronExpression)); +// } +// +// final FlowPanel hour = createPanel("Hour"); +// for (final CronExpression cronExpression : CronExpressions.HOUR) { +// hour.add(createLabel(cronExpression)); +// } +// +// final FlowPanel day = createPanel("Day"); +// for (final CronExpression cronExpression : CronExpressions.DAY) { +// day.add(createLabel(cronExpression)); +// } +// +// final FlowPanel month = createPanel("Month"); +// for (final CronExpression cronExpression : CronExpressions.MONTH) { +// month.add(createLabel(cronExpression)); +// } +// +// final FlowPanel quickSettingsPanel = new FlowPanel(); +// quickSettingsPanel.setStyleName("timeRange-quickSettings"); +// quickSettingsPanel.add(minute); +// quickSettingsPanel.add(hour); +// quickSettingsPanel.add(day); +// quickSettingsPanel.add(month); +// +// this.quickSettings.setWidget(quickSettingsPanel); +// } +// +// private FlowPanel createPanel(final String name) { +// final FlowPanel panel = new FlowPanel(); +// panel.addStyleName("timeRange-quickSettingPanel"); +// final Label label = new Label(name, false); +// label.addStyleName("timeRange-quickSettingPanel-title"); +// panel.add(label); +// return panel; +// } +// +// private Label createLabel(final CronExpression cronExpression) { +// final Label label = new Label(cronExpression.getName(), false); +// label.addStyleName("timeRange-quickSetting"); +// label.addClickHandler(event -> { +// expression.setValue(cronExpression.getExpression(), true); +// }); +// return label; +// } +// +// private Label createLabel(final FrequencyExpression cronExpression) { +// final Label label = new Label(cronExpression.getName(), false); +// label.addStyleName("timeRange-quickSetting"); +// label.addClickHandler(event -> { +// expression.setValue(cronExpression.getExpression(), true); +// }); +// return label; +// } + + @Override + public Widget asWidget() { + return widget; + } + + @Override + public void focus() { + hour.focus(); + } + + public interface Binder extends UiBinder { + + } + + @UiHandler("datePicker") + public void onDatePicker(final ValueChangeEvent event) { + updateDateLabel(); + } + + @UiHandler("hour") + public void onHour(final ValueChangeEvent event) { + updateTimeLabel(); + } + + @UiHandler("minute") + public void onMinute(final ValueChangeEvent event) { + updateTimeLabel(); + } + + @UiHandler("second") + public void onSecond(final ValueChangeEvent event) { + updateTimeLabel(); + } + + @UiHandler("millisecond") + public void onMillisecond(final ValueChangeEvent event) { + updateTimeLabel(); + } + + @UiHandler("timeZone") + public void onTimeZone(final ValueChangeEvent event) { + updateTimeLabel(); + } + + private void updateDateLabel() { + final FormatOptions options = FormatOptions + .builder() + .year(Year.NUMERIC) + .month(Month.LONG) + .day(Day.NUMERIC) + .build(); + date.setText(IntlDateTimeFormat.format(datePicker.getValue(), IntlDateTimeFormat.DEFAULT_LOCALE, options)); + } + + private void updateTimeLabel() { + final JsDate date = getCurrentDate(); + final FormatOptions options = FormatOptions + .builder() + .timeZone(timeZone.getValue()) + .hour(Hour.NUMERIC) + .minute(Minute.NUMERIC) + .second(Second.NUMERIC) + .fractionalSecondDigits(3) + .timeZoneName(TimeZoneName.SHORT) + .build(); + time.setText(IntlDateTimeFormat.format(date, IntlDateTimeFormat.DEFAULT_LOCALE, options)); + } +} diff --git a/stroom-core-client/src/main/java/stroom/job/client/view/ScheduleViewImpl.java b/stroom-core-client/src/main/java/stroom/job/client/view/ScheduleViewImpl.java index 6196cccf37b..b6b162aa9e4 100644 --- a/stroom-core-client/src/main/java/stroom/job/client/view/ScheduleViewImpl.java +++ b/stroom-core-client/src/main/java/stroom/job/client/view/ScheduleViewImpl.java @@ -17,7 +17,7 @@ package stroom.job.client.view; import stroom.item.client.SelectionBox; -import stroom.job.client.presenter.SchedulePresenter.ScheduleView; +import stroom.job.client.presenter.SchedulePopup.ScheduleView; import stroom.job.client.presenter.ScheduleUiHandlers; import stroom.query.api.v2.CronExpression; import stroom.query.api.v2.CronExpressions; diff --git a/stroom-core-client/src/main/java/stroom/monitoring/client/gin/MonitoringModule.java b/stroom-core-client/src/main/java/stroom/monitoring/client/gin/MonitoringModule.java index 0ef03444a4c..7aa68a5f57d 100644 --- a/stroom-core-client/src/main/java/stroom/monitoring/client/gin/MonitoringModule.java +++ b/stroom-core-client/src/main/java/stroom/monitoring/client/gin/MonitoringModule.java @@ -29,10 +29,13 @@ import stroom.data.grid.client.WrapperViewImpl; import stroom.data.store.impl.fs.client.ManageFsVolumesPlugin; import stroom.index.client.ManageIndexVolumesPlugin; +import stroom.job.client.presenter.DateTimePopup; +import stroom.job.client.presenter.DateTimePopup.DateTimeView; import stroom.job.client.presenter.JobPresenter; import stroom.job.client.presenter.JobPresenter.JobView; -import stroom.job.client.presenter.SchedulePresenter; -import stroom.job.client.presenter.SchedulePresenter.ScheduleView; +import stroom.job.client.presenter.SchedulePopup; +import stroom.job.client.presenter.SchedulePopup.ScheduleView; +import stroom.job.client.view.DateTimeViewImpl; import stroom.job.client.view.JobViewImpl; import stroom.job.client.view.ScheduleViewImpl; import stroom.monitoring.client.DatabaseTablesMonitoringPlugin; @@ -56,9 +59,13 @@ protected void configure() { // Job management. bindPlugin(JobListPlugin.class); bindPresenterWidget( - SchedulePresenter.class, + SchedulePopup.class, ScheduleView.class, ScheduleViewImpl.class); + bindPresenterWidget( + DateTimePopup.class, + DateTimeView.class, + DateTimeViewImpl.class); // Node management. bindPlugin(NodeMonitoringPlugin.class); diff --git a/stroom-core-client/src/main/java/stroom/preferences/client/DateTimeFormatter.java b/stroom-core-client/src/main/java/stroom/preferences/client/DateTimeFormatter.java index 45f7f5b2f1b..766fdd56e1e 100644 --- a/stroom-core-client/src/main/java/stroom/preferences/client/DateTimeFormatter.java +++ b/stroom-core-client/src/main/java/stroom/preferences/client/DateTimeFormatter.java @@ -3,6 +3,7 @@ import stroom.expression.api.TimeZone; import stroom.expression.api.TimeZone.Use; import stroom.ui.config.shared.UserPreferences; +import stroom.widget.customdatebox.client.ClientDateUtil; import stroom.widget.customdatebox.client.ClientDurationUtil; import javax.inject.Inject; @@ -42,8 +43,8 @@ public String format(final Long ms) { final UserPreferences userPreferences = userPreferencesManager.getCurrentUserPreferences(); if (userPreferences != null) { - if (userPreferences.getDateTimePattern() != null - && userPreferences.getDateTimePattern().trim().length() > 0) { + if (userPreferences.getDateTimePattern() != null && + userPreferences.getDateTimePattern().trim().length() > 0) { pattern = userPreferences.getDateTimePattern(); pattern = convertJavaDateTimePattern(pattern); @@ -68,6 +69,22 @@ public String format(final Long ms) { return nativeToDateString(ms, use.getDisplayValue(), pattern, zoneId, offsetMinutes); } + public Long parse(final String dateTime) { + Long ms = ClientDateUtil.fromISOString(dateTime); + if (ms == null) { + String pattern = "YYYY-MM-DDTHH:mm:ss.SSS[Z]"; + final UserPreferences userPreferences = userPreferencesManager.getCurrentUserPreferences(); + if (userPreferences != null) { + if (userPreferences.getDateTimePattern() != null && + userPreferences.getDateTimePattern().trim().length() > 0) { + pattern = userPreferences.getDateTimePattern(); + } + } + ms = ClientDateUtil.parseWithJavaFormat(dateTime, pattern).orElse(null); + } + return ms; + } + String convertJavaDateTimePattern(final String pattern) { String converted = pattern; converted = converted.replace('y', 'Y'); diff --git a/stroom-core-client/src/main/resources/stroom/analytics/client/view/ExecutionScheduleEditViewImpl.ui.xml b/stroom-core-client/src/main/resources/stroom/analytics/client/view/ExecutionScheduleEditViewImpl.ui.xml index b17a3f7076e..45516e0e566 100644 --- a/stroom-core-client/src/main/resources/stroom/analytics/client/view/ExecutionScheduleEditViewImpl.ui.xml +++ b/stroom-core-client/src/main/resources/stroom/analytics/client/view/ExecutionScheduleEditViewImpl.ui.xml @@ -3,9 +3,9 @@ xmlns:g="urn:import:com.google.gwt.user.client.ui" xmlns:tickbox="urn:import:stroom.widget.tickbox.client.view" xmlns:form="urn:import:stroom.widget.form.client" - xmlns:c="urn:import:stroom.widget.customdatebox.client" xmlns:s="urn:import:stroom.job.client.presenter" - xmlns:l="urn:import:stroom.item.client"> + xmlns:l="urn:import:stroom.item.client" + xmlns:datetimebox="urn:import:stroom.job.client.presenter"> @@ -20,10 +20,10 @@ - + - + diff --git a/stroom-core-client/src/main/resources/stroom/job/client/view/DateTimeViewImpl.ui.xml b/stroom-core-client/src/main/resources/stroom/job/client/view/DateTimeViewImpl.ui.xml new file mode 100644 index 00000000000..8c51ddcd224 --- /dev/null +++ b/stroom-core-client/src/main/resources/stroom/job/client/view/DateTimeViewImpl.ui.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/stroom-core-shared/src/main/java/stroom/svg/shared/SvgImage.java b/stroom-core-shared/src/main/java/stroom/svg/shared/SvgImage.java index 1462b82423a..2157039edb4 100644 --- a/stroom-core-shared/src/main/java/stroom/svg/shared/SvgImage.java +++ b/stroom-core-shared/src/main/java/stroom/svg/shared/SvgImage.java @@ -281,6 +281,34 @@ public enum SvgImage { "8333,161.83333 279.5,279.5 68.6667,117.66667 103,246.16667 103,385.5 z\"/> \n" + ""), + CALENDAR("calendar.svg", "svg-image__calendar", "" + + "\n" + + ""), + CANCEL("cancel.svg", "svg-image__cancel", "" + " +

+
calendar.svg
+
+
+
+
+
+
+
cancel.svg
diff --git a/stroom-dashboard-gwt/src/main/resources/stroom/dashboard/DashboardApp.gwt.xml b/stroom-dashboard-gwt/src/main/resources/stroom/dashboard/DashboardApp.gwt.xml index f22444070d6..c7547938028 100644 --- a/stroom-dashboard-gwt/src/main/resources/stroom/dashboard/DashboardApp.gwt.xml +++ b/stroom-dashboard-gwt/src/main/resources/stroom/dashboard/DashboardApp.gwt.xml @@ -38,6 +38,7 @@ + diff --git a/unreleased_changes/20240227_224134_784__4041.md b/unreleased_changes/20240227_224134_784__4041.md new file mode 100644 index 00000000000..34bdff061e6 --- /dev/null +++ b/unreleased_changes/20240227_224134_784__4041.md @@ -0,0 +1,24 @@ +* Issue **#4041** : Improve date picker. + + +```sh +# ******************************************************************************** +# Issue title: Improve date/time picker to make time a separate input +# Issue link: https://github.com/gchq/stroom/issues/4041 +# ******************************************************************************** + +# ONLY the top line will be included as a change entry in the CHANGELOG. +# The entry should be in GitHub flavour markdown and should be written on a SINGLE +# line with no hard breaks. You can have multiple change files for a single GitHub issue. +# The entry should be written in the imperative mood, i.e. 'Fix nasty bug' rather than +# 'Fixed nasty bug'. +# +# Examples of acceptable entries are: +# +# +# * Issue **123** : Fix bug with an associated GitHub issue in this repository +# +# * Issue **namespace/other-repo#456** : Fix bug with an associated GitHub issue in another repository +# +# * Fix bug with no associated GitHub issue. +```