diff --git a/packages/main/src/Calendar.hbs b/packages/main/src/Calendar.hbs index 7fcc4ffda6b0..3c01dd0268b3 100644 --- a/packages/main/src/Calendar.hbs +++ b/packages/main/src/Calendar.hbs @@ -7,7 +7,7 @@ id="{{_id}}-daypicker" ?hidden="{{_isDayPickerHidden}}" format-pattern="{{_formatPattern}}" - .selectedDates="{{selectedDates}}" + .selectedDates="{{_selectedDatesTimestamps}}" ._hidden="{{_isDayPickerHidden}}" .primaryCalendarType="{{_primaryCalendarType}}" .selectionMode="{{selectionMode}}" @@ -23,7 +23,7 @@ id="{{_id}}-MP" ?hidden="{{_isMonthPickerHidden}}" format-pattern="{{_formatPattern}}" - .selectedDates="{{selectedDates}}" + .selectedDates="{{_selectedDatesTimestamps}}" ._hidden="{{_isMonthPickerHidden}}" .primaryCalendarType="{{_primaryCalendarType}}" .minDate="{{minDate}}" @@ -37,7 +37,7 @@ id="{{_id}}-YP" ?hidden="{{_isYearPickerHidden}}" format-pattern="{{_formatPattern}}" - .selectedDates="{{selectedDates}}" + .selectedDates="{{_selectedDatesTimestamps}}" ._hidden="{{_isYearPickerHidden}}" .primaryCalendarType="{{_primaryCalendarType}}" .minDate="{{minDate}}" diff --git a/packages/main/src/Calendar.js b/packages/main/src/Calendar.js index 61453b37b3f7..098bbc2321ff 100644 --- a/packages/main/src/Calendar.js +++ b/packages/main/src/Calendar.js @@ -1,8 +1,10 @@ +import CalendarDate from "@ui5/webcomponents-localization/dist/dates/CalendarDate.js"; import RenderScheduler from "@ui5/webcomponents-base/dist/RenderScheduler.js"; import { isF4, isF4Shift, } from "@ui5/webcomponents-base/dist/Keys.js"; +import * as CalendarDateComponent from "./CalendarDate.js"; import CalendarPart from "./CalendarPart.js"; import CalendarHeader from "./CalendarHeader.js"; import DayPicker from "./DayPicker.js"; @@ -73,16 +75,36 @@ const metadata = { type: Boolean, }, }, + managedSlots: true, + slots: /** @lends sap.ui.webcomponents.main.Calendar.prototype */ { + /** + * Defines the selected date or dates (depending on the selectionMode property) for this calendar as instances of ui5-date + * + * @type {HTMLElement[]} + * @slot + * @public + */ + "default": { + propertyName: "dates", + type: HTMLElement, + invalidateOnChildChange: true, + }, + }, events: /** @lends sap.ui.webcomponents.main.Calendar.prototype */ { /** - * Fired when the selected dates changed. + * Fired when the selected dates change. + * Note: If you call preventDefault() for this event, ui5-calendar will not + * create instances of ui5-date for the newly selected dates. In that case you should do this manually. + * * @event sap.ui.webcomponents.main.Calendar#selected-dates-change - * @param {Array} dates The selected dates timestamps + * @param {Array} values The selected dates + * @param {Array} dates The selected dates as UTC timestamps * @public */ "selected-dates-change": { detail: { dates: { type: Array }, + values: { type: Array }, }, }, }, @@ -93,7 +115,14 @@ const metadata = { * *

Overview

* - * The ui5-calendar can be used stand alone to display the years, months, weeks and days + * The ui5-calendar component allows users to select one or more dates. + *

+ * Currently selected dates are represented with instances of ui5-date as + * children of the ui5-calendar. The value property of each ui5-date must be a + * date string, correctly formatted according to the ui5-calendar's formatPattern property. + * Whenever the user changes the date selection, ui5-calendar will automatically create/remove instances + * of ui5-date in itself, unless you prevent this behavior by calling preventDefault() for the + * selected-dates-change event. This is useful if you want to control the selected dates externally. *

* *

Usage

@@ -105,7 +134,7 @@ const metadata = { *
  • Pressing over an year inside the years view
  • * *
    - * The user can comfirm a date selection by pressing over a date inside the days view. + * The user can confirm a date selection by pressing over a date inside the days view. *

    * *

    Keyboard Handling

    @@ -159,6 +188,7 @@ const metadata = { * @alias sap.ui.webcomponents.main.Calendar * @extends CalendarPart * @tagname ui5-calendar + * @appenddocs CalendarDate * @public * @since 1.0.0-rc.11 */ @@ -175,6 +205,36 @@ class Calendar extends CalendarPart { return calendarCSS; } + /** + * @private + */ + get _selectedDatesTimestamps() { + return this.dates.map(date => { + const value = date.value; + return value && !!this.getFormat().parse(value) ? this._getTimeStampFromString(value) / 1000 : undefined; + }).filter(date => !!date); + } + + /** + * @private + */ + _setSelectedDates(selectedDates) { + const selectedValues = selectedDates.map(timestamp => this.getFormat().format(new Date(timestamp * 1000), true)); // Format as UTC + const valuesInDOM = [...this.dates].map(dateElement => dateElement.value); + + // Remove all elements for dates that are no longer selected + this.dates.filter(dateElement => !selectedValues.includes(dateElement.value)).forEach(dateElement => { + this.removeChild(dateElement); + }); + + // Create tags for the selected dates that don't already exist in DOM + selectedValues.filter(value => !valuesInDOM.includes(value)).forEach(value => { + const dateElement = document.createElement("ui5-date"); + dateElement.value = value; + this.appendChild(dateElement); + }); + } + async onAfterRendering() { await RenderScheduler.whenFinished(); // Await for the current picker to render and then ask if it has previous/next pages this._previousButtonDisabled = !this._currentPickerDOM._hasPreviousPage(); @@ -237,10 +297,16 @@ class Calendar extends CalendarPart { onSelectedDatesChange(event) { const timestamp = event.detail.timestamp; const selectedDates = event.detail.dates; + const datesValues = selectedDates.map(ts => { + const calendarDate = CalendarDate.fromTimestamp(ts * 1000, this._primaryCalendarType); + return this.getFormat().format(calendarDate.toUTCJSDate(), true); + }); this.timestamp = timestamp; - this.selectedDates = selectedDates; - this.fireEvent("selected-dates-change", { timestamp, dates: [...selectedDates] }); + const defaultPrevented = !this.fireEvent("selected-dates-change", { timestamp, dates: [...selectedDates], values: datesValues }, true); + if (!defaultPrevented) { + this._setSelectedDates(selectedDates); + } } onSelectedMonthChange(event) { @@ -267,8 +333,28 @@ class Calendar extends CalendarPart { } } + /** + * Returns an array of UTC timestamps, representing the selected dates. + * @protected + * @deprecated + */ + get selectedDates() { + return this._selectedDatesTimestamps; + } + + /** + * Creates instances of ui5-date inside this ui5-calendar with values, equal to the provided UTC timestamps + * @protected + * @deprecated + * @param selectedDates Array of UTC timestamps + */ + set selectedDates(selectedDates) { + this._setSelectedDates(selectedDates); + } + static get dependencies() { return [ + CalendarDateComponent.default, CalendarHeader, DayPicker, MonthPicker, diff --git a/packages/main/src/CalendarDate.js b/packages/main/src/CalendarDate.js new file mode 100644 index 000000000000..d733d2074ec5 --- /dev/null +++ b/packages/main/src/CalendarDate.js @@ -0,0 +1,44 @@ +import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js"; + +/** +* @public +*/ +const metadata = { + tag: "ui5-date", + properties: /** @lends sap.ui.webcomponents.main.CalendarDate.prototype */ { + + /** + * The date formatted according to the formatPattern property of the ui5-calendar that hosts the ui5-date + * + * @type {string} + * @public + */ + value: { + type: String, + }, + }, +}; + +/** + * @class + * + *

    Overview

    + * + * The ui5-date component defines a calendar date to be used inside ui5-calendar + * + * @constructor + * @author SAP SE + * @alias sap.ui.webcomponents.main.CalendarDate + * @extends sap.ui.webcomponents.base.UI5Element + * @tagname ui5-date + * @public + */ +class CalendarDate extends UI5Element { + static get metadata() { + return metadata; + } +} + +CalendarDate.define(); + +export default CalendarDate; diff --git a/packages/main/src/CalendarPart.js b/packages/main/src/CalendarPart.js index 8ce2d2673485..d65ebdc4f701 100644 --- a/packages/main/src/CalendarPart.js +++ b/packages/main/src/CalendarPart.js @@ -13,22 +13,11 @@ const metadata = { * The timestamp of the currently focused date. Set this property to move the component's focus to a certain date. * Node: Timestamp is 10-digit Integer representing the seconds (not milliseconds) since the Unix Epoch. * @type {Integer} - * @public + * @protected */ timestamp: { type: Integer, }, - - /** - * An array of UTC timestamps representing the selected date or dates depending on the capabilities of the picker component. - * @type {Array} - * @public - */ - selectedDates: { - type: Integer, - multiple: true, - compareValues: true, - }, }, }; diff --git a/packages/main/src/DatePicker.js b/packages/main/src/DatePicker.js index fc6fcb9d371e..a87358bb5e72 100644 --- a/packages/main/src/DatePicker.js +++ b/packages/main/src/DatePicker.js @@ -23,6 +23,7 @@ import Icon from "./Icon.js"; import Button from "./Button.js"; import ResponsivePopover from "./ResponsivePopover.js"; import Calendar from "./Calendar.js"; +import * as CalendarDateComponent from "./CalendarDate.js"; import Input from "./Input.js"; import InputType from "./types/InputType.js"; import DatePickerTemplate from "./generated/templates/DatePickerTemplate.lit.js"; @@ -388,12 +389,8 @@ class DatePicker extends DateComponentBase { * @protected */ get _calendarSelectedDates() { - if (!this.value) { - return []; - } - - if (this._checkValueValidity(this.value)) { - return [getRoundedTimestamp(this.dateValueUTC.getTime())]; + if (this.value && this._checkValueValidity(this.value)) { + return [this.value]; } return []; @@ -640,9 +637,8 @@ class DatePicker extends DateComponentBase { * @protected */ onSelectedDatesChange(event) { - const timestamp = event.detail.dates && event.detail.dates[0]; - const calendarDate = CalendarDate.fromTimestamp(timestamp * 1000, this._primaryCalendarType); - const newValue = this.getFormat().format(calendarDate.toUTCJSDate(), true); + event.preventDefault(); + const newValue = event.detail.values && event.detail.values[0]; this._updateValueAndFireEvents(newValue, true, ["change", "value-changed"]); this._focusInputAfterClose = true; @@ -728,6 +724,7 @@ class DatePicker extends DateComponentBase { Icon, ResponsivePopover, Calendar, + CalendarDateComponent.default, Input, Button, ]; diff --git a/packages/main/src/DatePickerPopover.hbs b/packages/main/src/DatePickerPopover.hbs index 560fa827dd04..adc9b6bd4954 100644 --- a/packages/main/src/DatePickerPopover.hbs +++ b/packages/main/src/DatePickerPopover.hbs @@ -44,14 +44,17 @@ primary-calendar-type="{{_primaryCalendarType}}" format-pattern="{{_formatPattern}}" timestamp="{{_calendarTimestamp}}" - .selectedDates={{_calendarSelectedDates}} .selectionMode="{{_calendarSelectionMode}}" .minDate="{{minDate}}" .maxDate="{{maxDate}}" @ui5-selected-dates-change="{{onSelectedDatesChange}}" ?hide-week-numbers="{{hideWeekNumbers}}" ._currentPicker="{{_calendarCurrentPicker}}" - > + > + {{#each _calendarSelectedDates}} + + {{/each}} + {{/inline}} {{#*inline "footer"}}{{/inline}} diff --git a/packages/main/src/DateRangePicker.js b/packages/main/src/DateRangePicker.js index c2e9d8bb3447..d2629ac323d5 100644 --- a/packages/main/src/DateRangePicker.js +++ b/packages/main/src/DateRangePicker.js @@ -24,6 +24,14 @@ const metadata = { type: String, defaultValue: "-", }, + + /** + * The first date in the range during selection (this is a temporary value, not the first date in the value range) + * @private + */ + _tempValue: { + type: String, + }, }, }; @@ -104,7 +112,13 @@ class DateRangePicker extends DatePicker { * @override */ get _calendarSelectedDates() { - return [this._firstDateTimestamp, this._lastDateTimestamp].filter(date => !!date); + if (this._tempValue) { + return [this._tempValue]; + } + if (this.value && this._checkValueValidity(this.value)) { + return this._splitValueByDelimiter(this.value); + } + return []; } /** @@ -178,13 +192,21 @@ class DateRangePicker extends DatePicker { * @override */ onSelectedDatesChange(event) { - const selectedDates = event.detail.dates; - if (selectedDates.length !== 2) { // Do nothing until the user selects 2 dates, we don't change any state at all for one date + event.preventDefault(); // never let the calendar update its own dates, the parent component controls them + const values = event.detail.values; + + if (values.length === 0) { + return; + } + + if (values.length === 1) { // Do nothing until the user selects 2 dates, we don't change any state at all for one date + this._tempValue = values[0]; return; } - const newValue = this._buildValue(...selectedDates); // the value will be normalized so we don't need to order them here + const newValue = this._buildValue(...event.detail.dates); // the value will be normalized so we don't need to order them here this._updateValueAndFireEvents(newValue, true, ["change", "value-changed"]); + this._tempValue = ""; this._focusInputAfterClose = true; this.closePicker(); } diff --git a/packages/main/src/DateTimePicker.js b/packages/main/src/DateTimePicker.js index dff30146ce6e..7ebb6928b82b 100644 --- a/packages/main/src/DateTimePicker.js +++ b/packages/main/src/DateTimePicker.js @@ -251,15 +251,15 @@ class DateTimePicker extends DatePicker { return fallback ? localeData.getCombinedDateTimePattern("medium", "medium", this._primaryCalendarType) : this.formatPattern; } - get _effectiveCalendarTimestamp() { - return this._previewValues.calendarTimestamp ? this._previewValues.calendarTimestamp : this._calendarTimestamp; + get _calendarTimestamp() { + return this._previewValues.calendarTimestamp ? this._previewValues.calendarTimestamp : super._calendarTimestamp; } - get _effectiveCalendarSelectedDates() { - return this._previewValues.calendarSelectedDate ? [this._previewValues.calendarSelectedDate] : this._calendarSelectedDates; + get _calendarSelectedDates() { + return this._previewValues.calendarValue ? [this._previewValues.calendarValue] : super._calendarSelectedDates; } - get _effectiveTimeValue() { + get _timeSelectionValue() { return this._previewValues.timeSelectionValue ? this._previewValues.timeSelectionValue : this.value; } @@ -315,10 +315,12 @@ class DateTimePicker extends DatePicker { * @override */ onSelectedDatesChange(event) { + event.preventDefault(); + this._previewValues = { ...this._previewValues, calendarTimestamp: event.detail.timestamp, - calendarSelectedDate: event.detail.dates[0], + calendarValue: event.detail.values[0], }; } @@ -347,7 +349,7 @@ class DateTimePicker extends DatePicker { } get _submitDisabled() { - return !this._effectiveCalendarSelectedDates || !this._effectiveCalendarSelectedDates.length; + return !this._calendarSelectedDates || !this._calendarSelectedDates.length; } /** @@ -414,8 +416,8 @@ class DateTimePicker extends DatePicker { } getSelectedDateTime() { - const selectedDate = CalendarDate.fromTimestamp(this._effectiveCalendarSelectedDates[0] * 1000).toLocalJSDate(); - const selectedTime = this.getFormat().parse(this._effectiveTimeValue); + const selectedDate = this.getFormat().parse(this._calendarSelectedDates[0]); + const selectedTime = this.getFormat().parse(this._timeSelectionValue); selectedDate.setHours(selectedTime.getHours()); selectedDate.setMinutes(selectedTime.getMinutes()); selectedDate.setSeconds(selectedTime.getSeconds()); diff --git a/packages/main/src/DateTimePickerPopover.hbs b/packages/main/src/DateTimePickerPopover.hbs index 1bd535a52a29..680000125b11 100644 --- a/packages/main/src/DateTimePickerPopover.hbs +++ b/packages/main/src/DateTimePickerPopover.hbs @@ -19,15 +19,18 @@ id="{{_id}}-calendar" primary-calendar-type="{{_primaryCalendarType}}" format-pattern="{{_formatPattern}}" - timestamp="{{_effectiveCalendarTimestamp}}" - .selectedDates={{_effectiveCalendarSelectedDates}} + timestamp="{{_calendarTimestamp}}" .selectionMode="{{_calendarSelectionMode}}" .minDate="{{minDate}}" .maxDate="{{maxDate}}" @ui5-selected-dates-change="{{onSelectedDatesChange}}" ?hide-week-numbers="{{hideWeekNumbers}}" ._currentPicker="{{_calendarCurrentPicker}}" - > + > + {{#each _calendarSelectedDates}} + + {{/each}} + {{#unless phone}} @@ -36,7 +39,7 @@ diff --git a/packages/main/src/MonthPicker.js b/packages/main/src/MonthPicker.js index 5ac5cd3dabc3..5bfe9ea38181 100644 --- a/packages/main/src/MonthPicker.js +++ b/packages/main/src/MonthPicker.js @@ -14,6 +14,7 @@ import { isPageUp, isPageDown, } from "@ui5/webcomponents-base/dist/Keys.js"; +import Integer from "@ui5/webcomponents-base/dist/types/Integer.js"; import getLocale from "@ui5/webcomponents-base/dist/locale/getLocale.js"; import CalendarPart from "./CalendarPart.js"; import MonthPickerTemplate from "./generated/templates/MonthPickerTemplate.lit.js"; @@ -24,6 +25,17 @@ import styles from "./generated/themes/MonthPicker.css.js"; const metadata = { tag: "ui5-monthpicker", properties: /** @lends sap.ui.webcomponents.main.MonthPicker.prototype */ { + /** + * An array of UTC timestamps representing the selected date or dates depending on the capabilities of the picker component. + * @type {Array} + * @public + */ + selectedDates: { + type: Integer, + multiple: true, + compareValues: true, + }, + _months: { type: Object, multiple: true, diff --git a/packages/main/src/YearPicker.js b/packages/main/src/YearPicker.js index 827a581b7a20..c108147a8c33 100644 --- a/packages/main/src/YearPicker.js +++ b/packages/main/src/YearPicker.js @@ -13,6 +13,7 @@ import { isPageUp, isPageDown, } from "@ui5/webcomponents-base/dist/Keys.js"; +import Integer from "@ui5/webcomponents-base/dist/types/Integer.js"; import getLocale from "@ui5/webcomponents-base/dist/locale/getLocale.js"; import CalendarDate from "@ui5/webcomponents-localization/dist/dates/CalendarDate.js"; import { getMaxCalendarDate } from "@ui5/webcomponents-localization/dist/dates/ExtremeDates.js"; @@ -26,6 +27,17 @@ import styles from "./generated/themes/YearPicker.css.js"; const metadata = { tag: "ui5-yearpicker", properties: /** @lends sap.ui.webcomponents.main.YearPicker.prototype */ { + /** + * An array of UTC timestamps representing the selected date or dates depending on the capabilities of the picker component. + * @type {Array} + * @public + */ + selectedDates: { + type: Integer, + multiple: true, + compareValues: true, + }, + _years: { type: Object, multiple: true, diff --git a/packages/main/test/pages/Calendar.html b/packages/main/test/pages/Calendar.html index a80fec9ec263..55541e0ad4aa 100644 --- a/packages/main/test/pages/Calendar.html +++ b/packages/main/test/pages/Calendar.html @@ -56,6 +56,14 @@ +
    + Calendar with preset dates + + + + +
    +