diff --git a/projects/dev-app/src/app/datetimepicker/datetimepicker-demo.component.html b/projects/dev-app/src/app/datetimepicker/datetimepicker-demo.component.html index 4be5fcf2..95d29b29 100644 --- a/projects/dev-app/src/app/datetimepicker/datetimepicker-demo.component.html +++ b/projects/dev-app/src/app/datetimepicker/datetimepicker-demo.component.html @@ -1,8 +1,10 @@ + +withSeconds
DateTime - + required @@ -14,7 +16,7 @@ DateTime with manual input + [timeInput]="true" [withSeconds]="withSeconds"> required @@ -22,11 +24,12 @@ max + DateTime Year selector + [timeInterval]="5" [withSeconds]="withSeconds"> required @@ -37,7 +40,7 @@ Time - + required @@ -46,7 +49,7 @@ Time AM/PM + [twelvehour]="true" type="time" [withSeconds]="withSeconds"> required @@ -55,7 +58,7 @@ DateTime AM/PM with manual input + [timeInput]="true" [twelvehour]="true" [withSeconds]="withSeconds"> required @@ -66,7 +69,7 @@ Date - + required @@ -74,8 +77,7 @@ Month - - + required @@ -83,7 +85,7 @@ Year - + required @@ -93,7 +95,7 @@ - + required min max @@ -104,7 +106,7 @@ - + required filter @@ -113,8 +115,7 @@ TouchUi - - + required @@ -124,12 +125,12 @@

Calendar Inline

- +

Selected date: {{selectedDate}}

- +

Selected time: {{selectedTime}}

diff --git a/projects/dev-app/src/app/datetimepicker/datetimepicker-demo.component.ts b/projects/dev-app/src/app/datetimepicker/datetimepicker-demo.component.ts index 71dbba04..91eea05a 100644 --- a/projects/dev-app/src/app/datetimepicker/datetimepicker-demo.component.ts +++ b/projects/dev-app/src/app/datetimepicker/datetimepicker-demo.component.ts @@ -12,6 +12,15 @@ import { MtxDatetimepickerFilterType } from '@ng-matero/extensions/datetimepicke styleUrls: ['datetimepicker-demo.component.scss'], }) export class DatetimepickerDemoComponent implements OnInit, OnDestroy { + private _withSeconds = false; + public set withSeconds(val: boolean) { + this._withSeconds = val; + localStorage.setItem('withSeconds', JSON.stringify(val)); + window.location.reload(); + } + public get withSeconds() { + return this._withSeconds; + } type = 'moment'; group: UntypedFormGroup; @@ -30,23 +39,31 @@ export class DatetimepickerDemoComponent implements OnInit, OnDestroy { private dateAdapter: DateAdapter, private translate: TranslateService ) { + const tmpWithSeconds = localStorage.getItem('withSeconds'); + this._withSeconds = tmpWithSeconds ? JSON.parse(tmpWithSeconds) : false; + this.today = moment.utc(); this.tomorrow = moment.utc().date(moment.utc().date() + 1); this.yesterday = moment.utc().date(moment.utc().date() - 1); this.min = this.today.clone().year(2018).month(10).date(3).hour(11).minute(10); this.max = this.min.clone().date(4).minute(45); this.start = this.today.clone().year(1930).month(9).date(28); - this.filter = (date: moment.Moment | null, type: MtxDatetimepickerFilterType) => { - if (date === null) { + this.filter = (date: moment.Moment | null, type: MtxDatetimepickerFilterType): boolean => { + if (!date) { return true; } + + const isEven = (value: number) => value % 2 === 0; + switch (type) { case MtxDatetimepickerFilterType.DATE: - return date.year() % 2 === 0 && date.month() % 2 === 0 && date.date() % 2 === 0; + return isEven(date.year()) && isEven(date.month()) && isEven(date.date()); case MtxDatetimepickerFilterType.HOUR: - return date.hour() % 2 === 0; + return isEven(date.hour()); case MtxDatetimepickerFilterType.MINUTE: - return date.minute() % 2 === 0; + return isEven(date.minute()); + case MtxDatetimepickerFilterType.SECOND: + return isEven(date.second()); } }; diff --git a/projects/dev-app/src/app/datetimepicker/datetimepicker-demo.module.ts b/projects/dev-app/src/app/datetimepicker/datetimepicker-demo.module.ts index 46739ae0..ea4bba8f 100644 --- a/projects/dev-app/src/app/datetimepicker/datetimepicker-demo.module.ts +++ b/projects/dev-app/src/app/datetimepicker/datetimepicker-demo.module.ts @@ -28,14 +28,18 @@ import { DatetimepickerDemoComponent } from './datetimepicker-demo.component'; monthInput: 'MMMM', yearInput: 'YYYY', timeInput: 'HH:mm', + timeWithSecondsInput: 'HH:mm:ss', datetimeInput: 'YYYY-MM-DD HH:mm', + datetimeWithSecondsInput: 'YYYY-MM-DD HH:mm:ss', }, display: { dateInput: 'YYYY-MM-DD', monthInput: 'MMMM', yearInput: 'YYYY', timeInput: 'HH:mm', + timeWithSecondsInput: 'HH:mm:ss', datetimeInput: 'YYYY-MM-DD HH:mm', + datetimeWithSecondsInput: 'YYYY-MM-DD HH:mm:ss', monthYearLabel: 'YYYY MMMM', dateA11yLabel: 'LL', monthYearA11yLabel: 'MMMM YYYY', diff --git a/projects/docs/src/app/pages/components/datetimepicker/examples/configurable/app.component.ts b/projects/docs/src/app/pages/components/datetimepicker/examples/configurable/app.component.ts index 2ae67586..5f47e0d3 100644 --- a/projects/docs/src/app/pages/components/datetimepicker/examples/configurable/app.component.ts +++ b/projects/docs/src/app/pages/components/datetimepicker/examples/configurable/app.component.ts @@ -20,14 +20,18 @@ import { MTX_DATETIME_FORMATS } from '@ng-matero/extensions/core'; monthInput: 'MMMM', yearInput: 'YYYY', timeInput: 'HH:mm', + timeWithSecondsInput: 'HH:mm:ss', datetimeInput: 'YYYY-MM-DD HH:mm', + datetimeWithSecondsInput: 'YYYY-MM-DD HH:mm:ss', }, display: { dateInput: 'YYYY-MM-DD', monthInput: 'MMMM', yearInput: 'YYYY', timeInput: 'HH:mm', + timeWithSecondsInput: 'HH:mm:ss', datetimeInput: 'YYYY-MM-DD HH:mm', + datetimeWithSecondsInput: 'YYYY-MM-DD HH:mm:ss', monthYearLabel: 'YYYY MMMM', dateA11yLabel: 'LL', monthYearA11yLabel: 'MMMM YYYY', diff --git a/projects/extensions-date-fns-adapter/adapter/date-fns-adapter.ts b/projects/extensions-date-fns-adapter/adapter/date-fns-adapter.ts index 48757313..51bc5b51 100644 --- a/projects/extensions-date-fns-adapter/adapter/date-fns-adapter.ts +++ b/projects/extensions-date-fns-adapter/adapter/date-fns-adapter.ts @@ -5,8 +5,10 @@ import { addHours, addMinutes, addMonths, + addSeconds, getHours, getMinutes, + getSeconds, isValid, startOfMonth, } from 'date-fns'; @@ -41,12 +43,23 @@ export class DateFnsDateTimeAdapter extends DatetimeAdapter { return getMinutes(date); } + getSecond(date: Date): number { + return getSeconds(date); + } + isInNextMonth(startDate: Date, endDate: Date): boolean { const nextMonth = this.getDateInNextMonth(startDate); return super.sameMonthAndYear(nextMonth, endDate); } - createDatetime(year: number, month: number, day: number, hour: number, minute: number): Date { + createDatetime( + year: number, + month: number, + day: number, + hour: number, + minute: number, + second: number + ): Date { if (month < 0 || month > 11) { throw Error(`Invalid month index "${month}". Month index has to be between 0 and 11.`); } @@ -63,7 +76,11 @@ export class DateFnsDateTimeAdapter extends DatetimeAdapter { throw Error(`Invalid minute "${minute}". Minute has to be between 0 and 59.`); } - const result = new Date(year, month, day, hour, minute); + if (second < 0 || second > 59) { + throw Error(`Invalid second "${second}". Second has to be between 0 and 59.`); + } + + const result = new Date(year, month, day, hour, minute, second); if (!isValid(result)) { throw Error(`Invalid date "${day}" for month with index "${month}".`); @@ -84,6 +101,10 @@ export class DateFnsDateTimeAdapter extends DatetimeAdapter { return range(60, i => i.toLocaleString(this.locale)); } + getSecondsNames(): string[] { + return range(60, i => i.toLocaleString(this.locale)); + } + addCalendarHours(date: Date, hours: number): Date { return addHours(date, hours); } @@ -92,6 +113,10 @@ export class DateFnsDateTimeAdapter extends DatetimeAdapter { return addMinutes(date, minutes); } + addCalendarSeconds(date: Date, seconds: number): Date { + return addSeconds(date, seconds); + } + deserialize(value: any): Date | null { return this._delegate.deserialize(value); } diff --git a/projects/extensions-luxon-adapter/adapter/luxon-datetime-adapter.ts b/projects/extensions-luxon-adapter/adapter/luxon-datetime-adapter.ts index 2dd6b613..ba577d3f 100644 --- a/projects/extensions-luxon-adapter/adapter/luxon-datetime-adapter.ts +++ b/projects/extensions-luxon-adapter/adapter/luxon-datetime-adapter.ts @@ -43,12 +43,23 @@ export class LuxonDatetimeAdapter extends DatetimeAdapter { return date.minute; } + getSecond(date: DateTime): number { + return date.second; + } + isInNextMonth(startDate: DateTime, endDate: DateTime): boolean { const nextMonth = this.getDateInNextMonth(startDate); return super.sameMonthAndYear(nextMonth, endDate); } - createDatetime(year: number, month: number, day: number, hour: number, minute: number): DateTime { + createDatetime( + year: number, + month: number, + day: number, + hour: number, + minute: number, + second: number + ): DateTime { if (month < 0 || month > 11) { throw Error(`Invalid month index "${month}". Month index has to be between 0 and 11.`); } @@ -65,12 +76,16 @@ export class LuxonDatetimeAdapter extends DatetimeAdapter { throw Error(`Invalid minute "${minute}". Minute has to be between 0 and 59.`); } + if (second < 0 || second > 59) { + throw Error(`Invalid second "${second}". Second has to be between 0 and 59.`); + } + // Luxon uses 1-indexed months so we need to add one to the month. let result; if (this._useUtc) { - result = DateTime.utc(year, month + 1, day, hour, minute); + result = DateTime.utc(year, month + 1, day, hour, minute, second); } else { - result = DateTime.local(year, month + 1, day, hour, minute); + result = DateTime.local(year, month + 1, day, hour, minute, second); } if (!result.isValid) { @@ -92,6 +107,10 @@ export class LuxonDatetimeAdapter extends DatetimeAdapter { return range(60, i => i.toLocaleString(this.locale)); } + getSecondsNames(): string[] { + return range(60, i => i.toLocaleString(this.locale)); + } + addCalendarHours(date: DateTime, hours: number): DateTime { return date.plus({ hours }); } @@ -100,6 +119,10 @@ export class LuxonDatetimeAdapter extends DatetimeAdapter { return date.plus({ minutes }); } + addCalendarSeconds(date: DateTime, seconds: number): DateTime { + return date.plus({ seconds }); + } + deserialize(value: any): DateTime | null { return this._delegate.deserialize(value); } diff --git a/projects/extensions-moment-adapter/adapter/moment-datetime-adapter.ts b/projects/extensions-moment-adapter/adapter/moment-datetime-adapter.ts index febf513f..2c34d15e 100644 --- a/projects/extensions-moment-adapter/adapter/moment-datetime-adapter.ts +++ b/projects/extensions-moment-adapter/adapter/moment-datetime-adapter.ts @@ -28,6 +28,7 @@ export class MomentDatetimeAdapter extends DatetimeAdapter { dates: string[]; hours: string[]; minutes: string[]; + seconds: string[]; longDaysOfWeek: string[]; shortDaysOfWeek: string[]; narrowDaysOfWeek: string[]; @@ -56,8 +57,9 @@ export class MomentDatetimeAdapter extends DatetimeAdapter { longMonths: momentLocaleData.months(), shortMonths: momentLocaleData.monthsShort(), dates: range(31, i => super.createDate(2017, 0, i + 1).format('D')), - hours: range(24, i => this.createDatetime(2017, 0, 1, i, 0).format('H')), - minutes: range(60, i => this.createDatetime(2017, 0, 1, 1, i).format('m')), + hours: range(24, i => this.createDatetime(2017, 0, 1, i, 0, 0).format('H')), + minutes: range(60, i => this.createDatetime(2017, 0, 1, 1, i, 0).format('m')), + seconds: range(60, i => this.createDatetime(2017, 0, 1, 0, 0, i).format('s')), longDaysOfWeek: momentLocaleData.weekdays(), shortDaysOfWeek: momentLocaleData.weekdaysShort(), narrowDaysOfWeek: momentLocaleData.weekdaysMin(), @@ -72,12 +74,23 @@ export class MomentDatetimeAdapter extends DatetimeAdapter { return super.clone(date).minute(); } + getSecond(date: Moment): number { + return super.clone(date).second(); + } + isInNextMonth(startDate: Moment, endDate: Moment): boolean { const nextMonth = this.getDateInNextMonth(startDate); return super.sameMonthAndYear(nextMonth, endDate); } - createDatetime(year: number, month: number, date: number, hour: number, minute: number): Moment { + createDatetime( + year: number, + month: number, + date: number, + hour: number, + minute: number, + second: number + ): Moment { // Check for invalid month and date (except upper bound on date which we have to check after // creating the Date). if (month < 0 || month > 11) { @@ -96,11 +109,15 @@ export class MomentDatetimeAdapter extends DatetimeAdapter { throw Error(`Invalid minute "${minute}". Minute has to be between 0 and 59.`); } + if (second < 0 || second > 59) { + throw Error(`Invalid second "${second}". Second has to be between 0 and 59.`); + } + let result; if (this._useUtc) { - result = moment.utc({ year, month, date, hour, minute }); + result = moment.utc({ year, month, date, hour, minute, second }); } else { - result = moment({ year, month, date, hour, minute }); + result = moment({ year, month, date, hour, minute, second }); } // If the result isn't valid, the date must have been out of bounds for this month. @@ -123,6 +140,10 @@ export class MomentDatetimeAdapter extends DatetimeAdapter { return this._localeData.minutes; } + getSecondsNames(): string[] { + return this._localeData.seconds; + } + addCalendarHours(date: Moment, hours: number): Moment { return super.clone(date).add({ hours }); } @@ -131,6 +152,10 @@ export class MomentDatetimeAdapter extends DatetimeAdapter { return super.clone(date).add({ minutes }); } + addCalendarSeconds(date: Moment, seconds: number): Moment { + return super.clone(date).add({ seconds }); + } + deserialize(value: any): Moment | null { return this._delegate.deserialize(value); } diff --git a/projects/extensions/core/datetime/datetime-adapter.ts b/projects/extensions/core/datetime/datetime-adapter.ts index 940763fb..70bb7cf9 100644 --- a/projects/extensions/core/datetime/datetime-adapter.ts +++ b/projects/extensions/core/datetime/datetime-adapter.ts @@ -9,6 +9,8 @@ export abstract class DatetimeAdapter extends DateAdapter { abstract getMinute(date: D): number; + abstract getSecond(date: D): number; + abstract getFirstDateOfMonth(date: D): D; abstract isInNextMonth(startDate: D, endDate: D): boolean; @@ -17,27 +19,38 @@ export abstract class DatetimeAdapter extends DateAdapter { abstract getMinuteNames(): string[]; + abstract getSecondsNames(): string[]; + abstract addCalendarHours(date: D, months: number): D; abstract addCalendarMinutes(date: D, minutes: number): D; + abstract addCalendarSeconds(date: D, seconds: number): D; + abstract createDatetime( year: number, month: number, date: number, hour: number, - minute: number + minute: number, + seconds: number ): D; getValidDateOrNull(obj: any): D | null { return this.isDateInstance(obj) && this.isValid(obj) ? obj : null; } - compareDatetime(first: D, second: D, respectMinutePart: boolean = true): number | boolean { + compareDatetime( + first: D, + second: D, + respectMinutePart: boolean = true, + respectSecondPart: boolean = true + ): number | boolean { return ( this.compareDate(first, second) || this.getHour(first) - this.getHour(second) || - (respectMinutePart && this.getMinute(first) - this.getMinute(second)) + (respectMinutePart && this.getMinute(first) - this.getMinute(second)) || + (respectSecondPart && this.getSecond(first) - this.getSecond(second)) ); } @@ -81,6 +94,15 @@ export abstract class DatetimeAdapter extends DateAdapter { ); } + sameSecond(first: D | null, second: D | null) { + return ( + first && + second && + this.getSecond(first) === this.getSecond(second) && + this.sameMinute(first, second) + ); + } + sameMonthAndYear(first: D | null, second: D | null): boolean { if (first && second) { const firstValid = this.isValid(first); @@ -185,10 +207,10 @@ export abstract class DatetimeAdapter extends DateAdapter { } clampDate(date: D, min?: D | null, max?: D | null): D { - if (min && this.compareDatetime(date, min) < 0) { + if (min && (this.compareDatetime(date, min) as number) < 0) { return min; } - if (max && this.compareDatetime(date, max) > 0) { + if (max && (this.compareDatetime(date, max) as number) > 0) { return max; } return date; diff --git a/projects/extensions/core/datetime/datetime-formats.ts b/projects/extensions/core/datetime/datetime-formats.ts index d2e69a59..326f8b6a 100644 --- a/projects/extensions/core/datetime/datetime-formats.ts +++ b/projects/extensions/core/datetime/datetime-formats.ts @@ -6,14 +6,18 @@ export interface MtxDatetimeFormats { monthInput?: any; yearInput?: any; timeInput?: any; + timeWithSecondsInput?: any; datetimeInput?: any; + datetimeWithSecondsInput?: any; }; display: { dateInput: any; monthInput: any; yearInput?: any; timeInput: any; + timeWithSecondsInput?: any; datetimeInput: any; + datetimeWithSecondsInput?: any; monthYearLabel: any; dateA11yLabel: any; monthYearA11yLabel: any; diff --git a/projects/extensions/core/datetime/native-datetime-adapter.ts b/projects/extensions/core/datetime/native-datetime-adapter.ts index 117cbbdf..e9d1aa53 100644 --- a/projects/extensions/core/datetime/native-datetime-adapter.ts +++ b/projects/extensions/core/datetime/native-datetime-adapter.ts @@ -8,6 +8,9 @@ const DEFAULT_HOUR_NAMES = range(24, i => String(i)); /** The default minute names to use if Intl API is not available. */ const DEFAULT_MINUTE_NAMES = range(60, i => String(i)); +/** The default second names to use if Intl API is not available. */ +const DEFAULT_SECOND_NAMES = range(60, i => String(i)); + function range(length: number, valueFunction: (index: number) => T): T[] { const valuesArray = Array(length); for (let i = 0; i < length; i++) { @@ -32,7 +35,8 @@ export class NativeDatetimeAdapter extends DatetimeAdapter { this.getMonth(date), this.getDate(date), this.getHour(date), - this.getMinute(date) + this.getMinute(date), + this.getSecond(date) ); } @@ -44,12 +48,23 @@ export class NativeDatetimeAdapter extends DatetimeAdapter { return date.getMinutes(); } + getSecond(date: Date): number { + return date.getSeconds(); + } + isInNextMonth(startDate: Date, endDate: Date): boolean { const nextMonth = this.getDateInNextMonth(startDate); return this.sameMonthAndYear(nextMonth, endDate); } - createDatetime(year: number, month: number, date: number, hour: number, minute: number): Date { + createDatetime( + year: number, + month: number, + date: number, + hour: number, + minute: number, + second: number + ): Date { // Check for invalid month and date (except upper bound on date which we have to check after // creating the Date). if (month < 0 || month > 11) { @@ -68,7 +83,11 @@ export class NativeDatetimeAdapter extends DatetimeAdapter { throw Error(`Invalid minute "${minute}". Minute has to be between 0 and 59.`); } - const result = this._createDateWithOverflow(year, month, date, hour, minute); + if (second < 0 || second > 59) { + throw Error(`Invalid second "${second}". Second has to be between 0 and 59.`); + } + + const result = this._createDateWithOverflow(year, month, date, hour, minute, second); // Check that the date wasn't above the upper bound for the month, causing the month to overflow if (result.getMonth() !== month) { @@ -92,6 +111,10 @@ export class NativeDatetimeAdapter extends DatetimeAdapter { return DEFAULT_MINUTE_NAMES; } + getSecondsNames(): string[] { + return DEFAULT_SECOND_NAMES; + } + addCalendarYears(date: Date, years: number): Date { return this.addCalendarMonths(date, years * 12); } @@ -102,7 +125,8 @@ export class NativeDatetimeAdapter extends DatetimeAdapter { this.getMonth(date) + months, this.getDate(date), this.getHour(date), - this.getMinute(date) + this.getMinute(date), + this.getSecond(date) ); // It's possible to wind up in the wrong month if the original month has more days than the new @@ -115,7 +139,8 @@ export class NativeDatetimeAdapter extends DatetimeAdapter { this.getMonth(newDate), 0, this.getHour(date), - this.getMinute(date) + this.getMinute(date), + this.getSecond(date) ); } @@ -128,7 +153,8 @@ export class NativeDatetimeAdapter extends DatetimeAdapter { this.getMonth(date), this.getDate(date) + days, this.getHour(date), - this.getMinute(date) + this.getMinute(date), + this.getSecond(date) ); } @@ -138,7 +164,8 @@ export class NativeDatetimeAdapter extends DatetimeAdapter { this.getMonth(date), this.getDate(date), this.getHour(date) + hours, - this.getMinute(date) + this.getMinute(date), + this.getSecond(date) ); } @@ -148,7 +175,19 @@ export class NativeDatetimeAdapter extends DatetimeAdapter { this.getMonth(date), this.getDate(date), this.getHour(date), - this.getMinute(date) + minutes + this.getMinute(date) + minutes, + this.getSecond(date) + ); + } + + addCalendarSeconds(date: Date, seconds: number): Date { + return this._createDateWithOverflow( + this.getYear(date), + this.getMonth(date), + this.getDate(date), + this.getHour(date), + this.getMinute(date), + this.getSecond(date) + seconds ); } @@ -156,7 +195,11 @@ export class NativeDatetimeAdapter extends DatetimeAdapter { return ( super.toIso8601(date) + 'T' + - [this._2digit(date.getUTCHours()), this._2digit(date.getUTCMinutes())].join(':') + [ + this._2digit(date.getUTCHours()), + this._2digit(date.getUTCMinutes()), + this._2digit(date.getUTCSeconds()), + ].join(':') ); } @@ -190,9 +233,10 @@ export class NativeDatetimeAdapter extends DatetimeAdapter { month: number, date: number, hours: number, - minutes: number + minutes: number, + seconds: number ) { - const result = new Date(year, month, date, hours, minutes); + const result = new Date(year, month, date, hours, minutes, seconds); // We need to correct for the fact that JS native Date treats years in range [0, 99] as // abbreviations for 19xx. diff --git a/projects/extensions/core/datetime/native-datetime-formats.ts b/projects/extensions/core/datetime/native-datetime-formats.ts index c7d36e29..59dcad6f 100644 --- a/projects/extensions/core/datetime/native-datetime-formats.ts +++ b/projects/extensions/core/datetime/native-datetime-formats.ts @@ -12,7 +12,16 @@ export const MTX_NATIVE_DATETIME_FORMATS: MtxDatetimeFormats = { hour: '2-digit', minute: '2-digit', }, + datetimeWithSecondsInput: { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + }, timeInput: { hour: '2-digit', minute: '2-digit' }, + timeWithSecondsInput: { hour: '2-digit', minute: '2-digit', second: '2-digit' }, monthYearLabel: { year: 'numeric', month: 'short' }, dateA11yLabel: { year: 'numeric', month: 'long', day: 'numeric' }, monthYearA11yLabel: { year: 'numeric', month: 'long' }, diff --git a/projects/extensions/datetimepicker/calendar.html b/projects/extensions/datetimepicker/calendar.html index 8fe9aeec..a800a38c 100644 --- a/projects/extensions/datetimepicker/calendar.html +++ b/projects/extensions/datetimepicker/calendar.html @@ -1,43 +1,50 @@ -
+
+ mat-button type="button" class="mtx-calendar-header-date" + [class.active]="currentView === 'month'" + [class.not-clickable]="type === 'month'" + [attr.aria-label]="_dateButtonLabel" + (click)="_dateClicked()">{{ _dateButtonText }} + [class.active]="currentView === 'clock'"> + [class.active]="_clockView === 'hour'" + [attr.aria-label]="_hourButtonLabel" + (click)="_hoursClicked()">{{ _hoursButtonText }} : + [class.active]="_clockView === 'minute'" + [attr.aria-label]="_minuteButtonLabel" + (click)="_minutesClicked()">{{ _minutesButtonText }} + + : + + + [class.active]="_AMPM === 'AM'" aria-label="AM" + (click)="_ampmClicked('AM')">AM + [class.active]="_AMPM === 'PM'" aria-label="PM" + (click)="_ampmClicked('PM')">PM
@@ -45,29 +52,29 @@
+ class="mtx-month-content">
+ [@slideCalendar]="_calendarState" + (@slideCalendar.done)="_calendarStateDone()"> {{ _yearPeriodText }}
+ (_userSelection)="_userSelected()" + (selectedChange)="_dateSelected($event)" + [activeDate]="_activeDate" + [dateFilter]="_dateFilterForViews" + [selected]="selected!" + [type]="type"> + (_userSelection)="_userSelected()" + (selectedChange)="_monthSelected($event)" + [activeDate]="_activeDate" + [dateFilter]="_dateFilterForViews" + [selected]="selected!" + [type]="type"> + (_userSelection)="_userSelected()" + (selectedChange)="_yearSelected($event)" + [activeDate]="_activeDate" + [dateFilter]="_dateFilterForViews" + [maxDate]="maxDate" + [minDate]="minDate" + [selected]="selected!" + [type]="type"> + (_userSelection)="_userSelected()" + (activeDateChange)="_onActiveDateChange($event)" + (selectedChange)="_timeSelected($event)" + [AMPM]="_AMPM" + (ampmChange)="_ampmClicked($event)" + [clockView]="_clockView" + (clockViewChange)="_clockView = $event" + [twelvehour]="twelvehour" + [dateFilter]="dateFilter" + [interval]="timeInterval" + [maxDate]="maxDate" + [minDate]="minDate" + [selected]="_activeDate" + [withSeconds]="withSeconds"> + (activeDateChange)="_onActiveDateChange($event)" + (selectedChange)="_dialTimeSelected($event)" + [AMPM]="_AMPM" + [dateFilter]="dateFilter" + [interval]="timeInterval" + [maxDate]="maxDate" + [minDate]="minDate" + [selected]="_activeDate" + [startView]="_clockView" + [twelvehour]="twelvehour" + [withSeconds]="withSeconds"> diff --git a/projects/extensions/datetimepicker/calendar.scss b/projects/extensions/datetimepicker/calendar.scss index 40ad9736..36f77144 100644 --- a/projects/extensions/datetimepicker/calendar.scss +++ b/projects/extensions/datetimepicker/calendar.scss @@ -13,7 +13,7 @@ $calendar-prev-icon-transform: translateX(2px) rotate(-45deg); $calendar-next-icon-transform: translateX(-2px) rotate(45deg); $landscape-calendar-header-width: 144px; - +$landscape-calendar-header-with-seconds-width: calc(#{$landscape-calendar-header-width} + 12px); @mixin landscape-calendar-header { .mtx-calendar { display: flex; @@ -21,6 +21,12 @@ $landscape-calendar-header-width: 144px; .mtx-calendar-header { width: $landscape-calendar-header-width; min-width: $landscape-calendar-header-width; + + &.mtx-calendar-header-with-seconds { + width:$landscape-calendar-header-with-seconds-width; + min-width: $landscape-calendar-header-with-seconds-width; + } + padding: 16px 8px; border-radius: 4px 0 0 4px; @@ -44,6 +50,7 @@ $landscape-calendar-header-width: 144px; .mtx-calendar-header-hours, .mtx-calendar-header-minutes, + .mtx-calendar-header-seconds, .mtx-calendar-header-ampm { width: 40px; text-align: center; @@ -79,6 +86,7 @@ $landscape-calendar-header-width: 144px; .mtx-calendar-header-date, .mtx-calendar-header-hours, .mtx-calendar-header-minutes, + .mtx-calendar-header-seconds, .mtx-calendar-header-ampm { height: auto; min-width: auto; @@ -111,6 +119,7 @@ $landscape-calendar-header-width: 144px; .mtx-calendar-header-date, .mtx-calendar-header-hours, .mtx-calendar-header-minutes, +.mtx-calendar-header-seconds, .mtx-calendar-header-ampm { &:not(.active) { opacity: .6; @@ -129,6 +138,7 @@ $landscape-calendar-header-width: 144px; .mtx-calendar-header-hours, .mtx-calendar-header-minutes, + .mtx-calendar-header-seconds, .mtx-calendar-header-ampm { opacity: 1; } diff --git a/projects/extensions/datetimepicker/calendar.ts b/projects/extensions/datetimepicker/calendar.ts index 757e8fe2..c71eaf57 100644 --- a/projects/extensions/datetimepicker/calendar.ts +++ b/projects/extensions/datetimepicker/calendar.ts @@ -51,6 +51,7 @@ import { MtxDatetimepickerIntl } from './datetimepicker-intl'; host: { 'class': 'mtx-calendar', '[class.mtx-calendar-with-time-input]': 'timeInput', + '[class.mtx-calendar-with-time-with-seconds-input]': 'timeInput && withSeconds', 'tabindex': '0', '(keydown)': '_handleCalendarBodyKeydown($event)', }, @@ -92,6 +93,9 @@ export class MtxCalendar implements AfterContentInit, OnDestroy { /** Prevent user to select same date time */ @Input() preventSameDateTimeSelection = false; + /** Includes the option to enter seconds. */ + @Input() withSeconds = false; + /** Emits when the currently selected date changes. */ @Output() selectedChange: EventEmitter = new EventEmitter(); @@ -139,6 +143,12 @@ export class MtxCalendar implements AfterContentInit, OnDestroy { if (this.type === 'year') { this.multiYearSelector = true; } + if (this.withSeconds && this._type !== 'datetime' && this._type !== 'time') { + console.warn( + 'The option \'withSeconds\' is not supported for types other than datetime and time' + ); + this.withSeconds = false; + } } private _type: MtxDatetimepickerType = 'date'; @@ -301,6 +311,14 @@ export class MtxCalendar implements AfterContentInit, OnDestroy { return this._intl.switchToClockMinuteViewLabel; } + get _secondsButtonText(): string { + return this._2digit(this._adapter.getSecond(this._activeDate)); + } + + get _secondButtonLabel(): string { + return this._intl.switchToClockSecondViewLabel; + } + get _prevButtonLabel(): string { switch (this._currentView) { case 'month': @@ -397,6 +415,7 @@ export class MtxCalendar implements AfterContentInit, OnDestroy { 0, 1, 0, + 0, 0 ); this.selectedChange.emit(normalizedDate); @@ -415,9 +434,12 @@ export class MtxCalendar implements AfterContentInit, OnDestroy { } _dialTimeSelected(date: D): void { - if (this._clockView !== 'minute') { + if (this._clockView !== 'minute' && this._clockView !== 'second') { this._activeDate = this._updateDate(date); this._clockView = 'minute'; + } else if (this.withSeconds && this._clockView == 'minute') { + this._activeDate = this._updateDate(date); + this._clockView = 'second'; } else { if (!this._adapter.sameDatetime(date, this.selected) || !this.preventSameDateTimeSelection) { this.selectedChange.emit(date); @@ -476,7 +498,8 @@ export class MtxCalendar implements AfterContentInit, OnDestroy { this._adapter.getMonth(this._activeDate), this._adapter.getDate(this._activeDate), newHourValue, - this._adapter.getMinute(this._activeDate) + this._adapter.getMinute(this._activeDate), + this.withSeconds ? this._adapter.getSecond(this._activeDate) : 0 ), this.minDate, this.maxDate @@ -514,6 +537,11 @@ export class MtxCalendar implements AfterContentInit, OnDestroy { this._clockView = 'minute'; } + _secondsClicked(): void { + this.currentView = 'clock'; + this._clockView = 'second'; + } + /** Handles user clicks on the previous button. */ _previousClicked(): void { this._activeDate = @@ -754,12 +782,16 @@ export class MtxCalendar implements AfterContentInit, OnDestroy { this._activeDate = this._clockView === 'hour' ? this._adapter.addCalendarHours(this._activeDate, 1) + : this._clockView === 'second' + ? this._adapter.addCalendarSeconds(this._activeDate, this.timeInterval) : this._adapter.addCalendarMinutes(this._activeDate, this.timeInterval); break; case DOWN_ARROW: this._activeDate = this._clockView === 'hour' ? this._adapter.addCalendarHours(this._activeDate, -1) + : this._clockView === 'second' + ? this._adapter.addCalendarSeconds(this._activeDate, -this.timeInterval) : this._adapter.addCalendarMinutes(this._activeDate, -this.timeInterval); break; case ENTER: diff --git a/projects/extensions/datetimepicker/clock.html b/projects/extensions/datetimepicker/clock.html index 33a4f255..38b3ea1c 100644 --- a/projects/extensions/datetimepicker/clock.html +++ b/projects/extensions/datetimepicker/clock.html @@ -10,7 +10,7 @@ [style.left]="item.left+'%'" [style.top]="item.top+'%'">{{ item.displayValue }}
-
+
{{ item.displayValue }}
+
+
{{ item.displayValue }}
+
diff --git a/projects/extensions/datetimepicker/clock.scss b/projects/extensions/datetimepicker/clock.scss index 792a9273..3b3370e7 100644 --- a/projects/extensions/datetimepicker/clock.scss +++ b/projects/extensions/datetimepicker/clock.scss @@ -32,10 +32,7 @@ $clock-cell-size: 14.1666% !default; .mtx-clock-hand { position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; + inset: 0; width: 1px; margin: 0 auto; transform-origin: bottom; @@ -52,7 +49,8 @@ $clock-cell-size: 14.1666% !default; } .mtx-clock-hours, -.mtx-clock-minutes { +.mtx-clock-minutes, +.mtx-clock-seconds { position: absolute; top: 0; left: 0; @@ -70,8 +68,9 @@ $clock-cell-size: 14.1666% !default; } } -.mtx-clock-minutes { - transform: scale(.8); +.mtx-clock-minutes, +.mtx-clock-seconds { + transform: scale(0.8); } .mtx-clock-cell { diff --git a/projects/extensions/datetimepicker/clock.ts b/projects/extensions/datetimepicker/clock.ts index 6d2c9cc9..1a99356e 100644 --- a/projects/extensions/datetimepicker/clock.ts +++ b/projects/extensions/datetimepicker/clock.ts @@ -28,7 +28,7 @@ export const CLOCK_OUTER_RADIUS = 41.25; export const CLOCK_TICK_RADIUS = 7.0833; /** Possible views for datetimepicker clock. */ -export type MtxClockView = 'hour' | 'minute'; +export type MtxClockView = 'hour' | 'minute' | 'second'; /** * A clock that is used as part of the datetimepicker. @@ -61,6 +61,9 @@ export class MtxClock implements AfterContentInit, OnDestroy, OnChanges { /** Whether the time is now in AM or PM. */ @Input() AMPM: MtxAMPM = 'AM'; + /** Includes the option to enter seconds. */ + @Input() withSeconds: boolean = false; + /** Emits when the currently selected date changes. */ @Output() selectedChange = new EventEmitter(); @@ -72,15 +75,21 @@ export class MtxClock implements AfterContentInit, OnDestroy, OnChanges { /** Whether the clock is in hour view. */ _hourView: boolean = true; + _minuteView: boolean = false; + _secondView: boolean = false; _hours: any[] = []; _minutes: any[] = []; + _seconds: any[] = []; + _selectedHour!: number; _selectedMinute!: number; + _selectedSecond!: number; + private _timeChanged = false; constructor( @@ -102,6 +111,8 @@ export class MtxClock implements AfterContentInit, OnDestroy, OnChanges { this._activeDate = this._adapter.clampDate(value, this.minDate, this.maxDate); if (!this._adapter.sameMinute(oldActiveDate, this._activeDate)) { this._init(); + } else if (this.withSeconds && !this._adapter.sameSecond(oldActiveDate, this._activeDate)) { + this._init(); } } private _activeDate!: D; @@ -142,13 +153,16 @@ export class MtxClock implements AfterContentInit, OnDestroy, OnChanges { /** Whether the clock should be started in hour or minute view. */ @Input() set startView(value: MtxClockView) { - this._hourView = value !== 'minute'; + this._hourView = value !== 'minute' && value !== 'second'; + this._minuteView = value === 'minute'; + this._secondView = value === 'second'; } get _hand() { const hour = this._adapter.getHour(this.activeDate); this._selectedHour = hour; this._selectedMinute = this._adapter.getMinute(this.activeDate); + this._selectedSecond = this._adapter.getSecond(this.activeDate); let deg = 0; let radius = CLOCK_OUTER_RADIUS; if (this._hourView) { @@ -158,6 +172,8 @@ export class MtxClock implements AfterContentInit, OnDestroy, OnChanges { radius = CLOCK_OUTER_RADIUS; } deg = Math.round(this._selectedHour * (360 / (24 / 2))); + } else if (this._secondView) { + deg = Math.round(this._selectedSecond * (360 / 60)); } else { deg = Math.round(this._selectedMinute * (360 / 60)); } @@ -208,7 +224,7 @@ export class MtxClock implements AfterContentInit, OnDestroy, OnChanges { if (this._timeChanged) { this.selectedChange.emit(this.activeDate); - if (!this._hourView) { + if ((!this.withSeconds && !this._hourView) || (this.withSeconds && this._secondView)) { this._userSelection.emit(); } } @@ -244,9 +260,11 @@ export class MtxClock implements AfterContentInit, OnDestroy, OnChanges { private _init() { this._hours.length = 0; this._minutes.length = 0; + this._seconds.length = 0; const hourNames = this._adapter.getHourNames(); const minuteNames = this._adapter.getMinuteNames(); + const secondNames = this._adapter.getSecondsNames(); if (this.twelvehour) { const hours = []; for (let i = 0; i < hourNames.length; i++) { @@ -259,13 +277,16 @@ export class MtxClock implements AfterContentInit, OnDestroy, OnChanges { this._adapter.getMonth(this.activeDate), this._adapter.getDate(this.activeDate), hour, + 0, 0 ); // Check if the date is enabled, no need to respect the minute setting here const enabled = - (!this.minDate || this._adapter.compareDatetime(date, this.minDate, false) >= 0) && - (!this.maxDate || this._adapter.compareDatetime(date, this.maxDate, false) <= 0) && + (!this.minDate || + (this._adapter.compareDatetime(date, this.minDate, false) as number) >= 0) && + (!this.maxDate || + (this._adapter.compareDatetime(date, this.maxDate, false) as number) <= 0) && (!this.dateFilter || this.dateFilter(date, MtxDatetimepickerFilterType.HOUR)); // display value for twelvehour clock should be from 1-12 not including 0 and not above 12 @@ -294,13 +315,16 @@ export class MtxClock implements AfterContentInit, OnDestroy, OnChanges { this._adapter.getMonth(this.activeDate), this._adapter.getDate(this.activeDate), i, + 0, 0 ); // Check if the date is enabled, no need to respect the minute setting here const enabled = - (!this.minDate || this._adapter.compareDatetime(date, this.minDate, false) >= 0) && - (!this.maxDate || this._adapter.compareDatetime(date, this.maxDate, false) <= 0) && + (!this.minDate || + (this._adapter.compareDatetime(date, this.minDate, false) as number) >= 0) && + (!this.maxDate || + (this._adapter.compareDatetime(date, this.maxDate, false) as number) <= 0) && (!this.dateFilter || this.dateFilter(date, MtxDatetimepickerFilterType.HOUR)); this._hours.push({ @@ -321,11 +345,12 @@ export class MtxClock implements AfterContentInit, OnDestroy, OnChanges { this._adapter.getMonth(this.activeDate), this._adapter.getDate(this.activeDate), this._adapter.getHour(this.activeDate), - i + i, + 0 ); const enabled = - (!this.minDate || this._adapter.compareDatetime(date, this.minDate) >= 0) && - (!this.maxDate || this._adapter.compareDatetime(date, this.maxDate) <= 0) && + (!this.minDate || (this._adapter.compareDatetime(date, this.minDate) as number) >= 0) && + (!this.maxDate || (this._adapter.compareDatetime(date, this.maxDate) as number) <= 0) && (!this.dateFilter || this.dateFilter(date, MtxDatetimepickerFilterType.MINUTE)); this._minutes.push({ value: i, @@ -335,6 +360,31 @@ export class MtxClock implements AfterContentInit, OnDestroy, OnChanges { left: CLOCK_RADIUS + Math.sin(radian) * CLOCK_OUTER_RADIUS - CLOCK_TICK_RADIUS, }); } + + if (this.withSeconds) { + for (let i = 0; i < secondNames.length; i += 5) { + const radian = (i / 30) * Math.PI; + const date = this._adapter.createDatetime( + this._adapter.getYear(this.activeDate), + this._adapter.getMonth(this.activeDate), + this._adapter.getDate(this.activeDate), + this._adapter.getHour(this.activeDate), + this._adapter.getMinute(this.activeDate), + i + ); + const enabled = + (!this.minDate || (this._adapter.compareDatetime(date, this.minDate) as number) >= 0) && + (!this.maxDate || (this._adapter.compareDatetime(date, this.maxDate) as number) <= 0) && + (!this.dateFilter || this.dateFilter(date, MtxDatetimepickerFilterType.SECOND)); + this._seconds.push({ + value: i, + displayValue: i === 0 ? '00' : secondNames[i], + enabled, + top: CLOCK_RADIUS - Math.cos(radian) * CLOCK_OUTER_RADIUS - CLOCK_TICK_RADIUS, + left: CLOCK_RADIUS + Math.sin(radian) * CLOCK_OUTER_RADIUS - CLOCK_TICK_RADIUS, + }); + } + } } /** @@ -384,9 +434,10 @@ export class MtxClock implements AfterContentInit, OnDestroy, OnChanges { this._adapter.getMonth(this.activeDate), this._adapter.getDate(this.activeDate), value, - this._adapter.getMinute(this.activeDate) + this._adapter.getMinute(this.activeDate), + this.withSeconds ? this._adapter.getSecond(this.activeDate) : 0 ); - } else { + } else if (this._secondView) { if (this.interval) { value *= this.interval; } @@ -398,8 +449,24 @@ export class MtxClock implements AfterContentInit, OnDestroy, OnChanges { this._adapter.getMonth(this.activeDate), this._adapter.getDate(this.activeDate), this._adapter.getHour(this.activeDate), + this._adapter.getMinute(this.activeDate), value ); + } else { + if (this.interval) { + value *= this.interval; + } + if (value === 60) { + value = 0; + } + date = this._adapter.createDatetime( + this._adapter.getYear(this.activeDate), + this._adapter.getMonth(this.activeDate), + this._adapter.getDate(this.activeDate), + this._adapter.getHour(this.activeDate), + value, + this.withSeconds ? this._adapter.getSecond(this.activeDate) : 0 + ); } // if there is a dateFilter, check if the date is allowed if it is not then do not set/emit new date @@ -408,7 +475,11 @@ export class MtxClock implements AfterContentInit, OnDestroy, OnChanges { this.dateFilter && !this.dateFilter( date, - this._hourView ? MtxDatetimepickerFilterType.HOUR : MtxDatetimepickerFilterType.MINUTE + this._hourView + ? MtxDatetimepickerFilterType.HOUR + : this._secondView + ? MtxDatetimepickerFilterType.SECOND + : MtxDatetimepickerFilterType.MINUTE ) ) { return; diff --git a/projects/extensions/datetimepicker/datetimepicker-content.html b/projects/extensions/datetimepicker/datetimepicker-content.html index ae5afe17..94671fcf 100644 --- a/projects/extensions/datetimepicker/datetimepicker-content.html +++ b/projects/extensions/datetimepicker/datetimepicker-content.html @@ -1,27 +1,28 @@ diff --git a/projects/extensions/datetimepicker/datetimepicker-content.scss b/projects/extensions/datetimepicker/datetimepicker-content.scss index 673f2530..255fa7ba 100644 --- a/projects/extensions/datetimepicker/datetimepicker-content.scss +++ b/projects/extensions/datetimepicker/datetimepicker-content.scss @@ -1,11 +1,28 @@ $calendar-padding: 8px; $non-touch-calendar-cell-size: 40px; $non-touch-calendar-portrait-width: $non-touch-calendar-cell-size * 7 + $calendar-padding * 2; +$non-touch-calendar-portrait-with-seconds-width: calc( + #{$non-touch-calendar-portrait-width} + 50px +); + $non-touch-calendar-portrait-height: 424px; $non-touch-calendar-landscape-width: 432px; +$non-touch-calendar-landscape-with-seconds-width: calc( + #{$non-touch-calendar-landscape-width} + 60px +); $non-touch-calendar-landscape-height: 328px; + +$non-touch-calendar-landscape-with-seconds-height: calc( + #{$non-touch-calendar-landscape-height} + 20px +); $non-touch-calendar-with-time-input-portrait-height: 490px; +$non-touch-calendar-with-time-input-portrait-with-seconds-height: calc( + #{$non-touch-calendar-with-time-input-portrait-height} + 50px +); $non-touch-calendar-with-time-input-landscape-height: 404px; +$non-touch-calendar-with-time-input-landscape-with-seconds-height: calc( + #{$non-touch-calendar-with-time-input-landscape-height} + 50px +); // Ideally the calendar would have a constant aspect ratio, no matter its size, and we would base // these measurements off the aspect ratio. Unfortunately, the aspect ratio does change a little as @@ -30,10 +47,13 @@ $touch-max-height: 850px; .mtx-calendar { width: $non-touch-calendar-landscape-width; height: $non-touch-calendar-landscape-height; - &.mtx-calendar-with-time-input { height: $non-touch-calendar-with-time-input-landscape-height; } + &.mtx-calendar-with-time-with-seconds-input { + width: $non-touch-calendar-landscape-with-seconds-width; + height: $non-touch-calendar-with-time-input-landscape-with-seconds-height; + } } } @@ -57,6 +77,10 @@ $touch-max-height: 850px; &.mtx-calendar-with-time-input { height: $non-touch-calendar-with-time-input-portrait-height; } + &.mtx-calendar-with-time-with-seconds-input { + width: $non-touch-calendar-portrait-with-seconds-width; + height: $non-touch-calendar-with-time-input-portrait-with-seconds-height; + } } .mtx-datetimepicker-content[mode='landscape'] { diff --git a/projects/extensions/datetimepicker/datetimepicker-filtertype.ts b/projects/extensions/datetimepicker/datetimepicker-filtertype.ts index 33301383..dcfa4877 100644 --- a/projects/extensions/datetimepicker/datetimepicker-filtertype.ts +++ b/projects/extensions/datetimepicker/datetimepicker-filtertype.ts @@ -2,4 +2,5 @@ export enum MtxDatetimepickerFilterType { DATE, HOUR, MINUTE, + SECOND } diff --git a/projects/extensions/datetimepicker/datetimepicker-input.ts b/projects/extensions/datetimepicker/datetimepicker-input.ts index f6434e2f..9391d891 100644 --- a/projects/extensions/datetimepicker/datetimepicker-input.ts +++ b/projects/extensions/datetimepicker/datetimepicker-input.ts @@ -108,6 +108,11 @@ export class MtxDatetimepickerInput /** Whether the last value set on the input was valid. */ private _lastValueValid = false; + /** Includes the option to enter seconds. */ + private get _withSeconds() { + return this._datetimepicker.withSeconds; + } + constructor( private _elementRef: ElementRef, @Optional() public _dateAdapter: DatetimeAdapter, @@ -314,10 +319,32 @@ export class MtxDatetimepickerInput switch (this._datetimepicker.type) { case 'date': return this._dateFormats.display.dateInput; - case 'datetime': + case 'datetime': { + if (this._withSeconds) { + const format = this._dateFormats.display.datetimeWithSecondsInput; + if (!format) { + console.warn( + 'The display format \'datetimeWithSecondsInput\' is not filled, the format from \'datetimeInput\' will be used instead' + ); + } else { + return format; + } + } return this._dateFormats.display.datetimeInput; - case 'time': + } + case 'time': { + if (this._withSeconds) { + const format = this._dateFormats.display.timeWithSecondsInput; + if (!format) { + console.warn( + 'The display format \'timeWithSecondsInput\' is not filled, the format from \'timeInput\' will be used instead' + ); + } else { + return format; + } + } return this._dateFormats.display.timeInput; + } case 'month': return this._dateFormats.display.monthInput; case 'year': @@ -333,10 +360,34 @@ export class MtxDatetimepickerInput parseFormat = this._dateFormats.parse.dateInput; break; case 'datetime': - parseFormat = this._dateFormats.parse.datetimeInput; + { + if (this._withSeconds) { + const tmpParseFormat = this._dateFormats.parse.datetimeWithSecondsInput; + if (!tmpParseFormat) { + console.warn( + 'The display parse format \'datetimeWithSecondsInput\' is not filled, the parse format from \'datetimeInput\' will be used instead' + ); + } else { + parseFormat = tmpParseFormat; + } + } + parseFormat = this._dateFormats.parse.datetimeInput; + } break; case 'time': - parseFormat = this._dateFormats.parse.timeInput; + { + if (this._withSeconds) { + const tmpParseFormat = this._dateFormats.parse.timeWithSecondsInput; + if (!tmpParseFormat) { + console.warn( + 'The display parse format \'timeWithSecondsInput\' is not filled, the parse format from \'timeInput\' will be used instead' + ); + } else { + parseFormat = tmpParseFormat; + } + } + parseFormat = this._dateFormats.parse.timeInput; + } break; case 'month': parseFormat = this._dateFormats.parse.monthInput; diff --git a/projects/extensions/datetimepicker/datetimepicker-intl.ts b/projects/extensions/datetimepicker/datetimepicker-intl.ts index 31974768..d27ea636 100644 --- a/projects/extensions/datetimepicker/datetimepicker-intl.ts +++ b/projects/extensions/datetimepicker/datetimepicker-intl.ts @@ -67,6 +67,9 @@ export class MtxDatetimepickerIntl { /** A label for the 'switch to clock minute view' button (used by screen readers). */ switchToClockMinuteViewLabel = 'Choose minute'; + /** A label for the 'switch to clock seconds view' button (used by screen readers). */ + switchToClockSecondViewLabel = 'Choose seconds'; + /** Label used for ok button within the manual time input. */ okLabel = 'OK'; diff --git a/projects/extensions/datetimepicker/datetimepicker.ts b/projects/extensions/datetimepicker/datetimepicker.ts index a6fa788d..44787289 100644 --- a/projects/extensions/datetimepicker/datetimepicker.ts +++ b/projects/extensions/datetimepicker/datetimepicker.ts @@ -189,6 +189,9 @@ export class MtxDatetimepicker implements OnDestroy { /** Prevent user to select same date time */ @Input() preventSameDateTimeSelection = false; + /** Includes the option to enter seconds. */ + @Input() withSeconds: boolean = false; + /** * Emits new selected date when selected date changes. * @deprecated Switch to the `dateChange` and `dateInput` binding on the input element. @@ -297,6 +300,10 @@ export class MtxDatetimepicker implements OnDestroy { } set type(value: MtxDatetimepickerType) { this._type = value || 'datetime'; + if (this.withSeconds && this._type !== 'datetime' && this._type !== 'time') { + console.warn('The option \'withSeconds\' is not supported for types other than datetime and time'); + this.withSeconds = false; + } } private _type: MtxDatetimepickerType = 'datetime'; @@ -406,6 +413,10 @@ export class MtxDatetimepicker implements OnDestroy { if (!this._dateAdapter.sameDatetime(oldValue, this._selected)) { this.selectedChanged.emit(date); } + else + if (this.withSeconds && !this._dateAdapter.sameSecond(oldValue, this._selected)) { + this.selectedChanged.emit(date); + } } /** diff --git a/projects/extensions/datetimepicker/month-view.ts b/projects/extensions/datetimepicker/month-view.ts index 36173b3f..43f96880 100644 --- a/projects/extensions/datetimepicker/month-view.ts +++ b/projects/extensions/datetimepicker/month-view.ts @@ -140,7 +140,8 @@ export class MtxMonthView implements AfterContentInit { this._adapter.getMonth(this.activeDate), date, this._adapter.getHour(this.activeDate), - this._adapter.getMinute(this.activeDate) + this._adapter.getMinute(this.activeDate), + this._adapter.getSecond(this.activeDate) ) ); if (this.type === 'date') { @@ -162,7 +163,8 @@ export class MtxMonthView implements AfterContentInit { this._adapter.getMonth(this.activeDate), 1, this._adapter.getHour(this.activeDate), - this._adapter.getMinute(this.activeDate) + this._adapter.getMinute(this.activeDate), + this._adapter.getSecond(this.activeDate) ); this._firstWeekOffset = (DAYS_PER_WEEK + @@ -188,7 +190,8 @@ export class MtxMonthView implements AfterContentInit { this._adapter.getMonth(this.activeDate), i + 1, this._adapter.getHour(this.activeDate), - this._adapter.getMinute(this.activeDate) + this._adapter.getMinute(this.activeDate), + this._adapter.getSecond(this.activeDate) ); const enabled = !this.dateFilter || this.dateFilter(date); const ariaLabel = this._adapter.format(date, this._dateFormats.display.dateA11yLabel); diff --git a/projects/extensions/datetimepicker/multi-year-view.ts b/projects/extensions/datetimepicker/multi-year-view.ts index a6e46c61..07d4b711 100644 --- a/projects/extensions/datetimepicker/multi-year-view.ts +++ b/projects/extensions/datetimepicker/multi-year-view.ts @@ -141,7 +141,7 @@ export class MtxMultiYearView implements AfterContentInit { /** Handles when a new year is selected. */ _yearSelected(year: number) { const month = this._adapter.getMonth(this.activeDate); - const normalizedDate = this._adapter.createDatetime(year, month, 1, 0, 0); + const normalizedDate = this._adapter.createDatetime(year, month, 1, 0, 0, 0); this.selectedChange.emit( this._adapter.createDatetime( @@ -152,7 +152,8 @@ export class MtxMultiYearView implements AfterContentInit { this._adapter.getNumDaysInMonth(normalizedDate) ), this._adapter.getHour(this.activeDate), - this._adapter.getMinute(this.activeDate) + this._adapter.getMinute(this.activeDate), + this._adapter.getSecond(this.activeDate) ) ); diff --git a/projects/extensions/datetimepicker/time.html b/projects/extensions/datetimepicker/time.html index 5cfb9d4c..23bfb7ca 100644 --- a/projects/extensions/datetimepicker/time.html +++ b/projects/extensions/datetimepicker/time.html @@ -1,57 +1,76 @@
+ [class.mtx-time-input-active]="clockView === 'hour'" + [class.mtx-time-input-warning]="!hourInput.valid" + #hourInput="mtxTimeInput" + type="text" + inputmode="numeric" + maxlength="2" + [timeMin]="twelvehour ? 1 : 0" + [timeMax]="twelvehour ? 12 : 23" + [timeValue]="hour" + (timeValueChanged)="handleHourInputChange($event)" + (focus)="handleFocus('hour')" />
:
+ [class.mtx-time-input-active]="clockView === 'minute'" + [class.mtx-time-input-warning]="!minuteInput.valid" + #minuteInput="mtxTimeInput" + type="text" + inputmode="numeric" + maxlength="2" + [timeMin]="0" + [timeMax]="59" + [timeValue]="minute" + (timeValueChanged)="handleMinuteInputChange($event)" + [timeInterval]="interval" + (focus)="handleFocus('minute')" /> + +
:
+ + +
+ [class.mtx-time-ampm-active]="AMPM === 'AM'" aria-label="AM" + (keydown)="$event.stopPropagation()" + (click)="ampmChange.emit('AM')">AM + [class.mtx-time-ampm-active]="AMPM === 'PM'" aria-label="PM" + (keydown)="$event.stopPropagation()" + (click)="ampmChange.emit('PM')">PM
+ (activeDateChange)="_onActiveDateChange($event)" + [AMPM]="AMPM" + [dateFilter]="dateFilter" + [interval]="interval" + [maxDate]="maxDate" + [minDate]="minDate" + [selected]="selected" + [startView]="clockView" + [twelvehour]="twelvehour" + [withSeconds]="withSeconds">
diff --git a/projects/extensions/datetimepicker/time.ts b/projects/extensions/datetimepicker/time.ts index 9ae35cdb..f3243e21 100644 --- a/projects/extensions/datetimepicker/time.ts +++ b/projects/extensions/datetimepicker/time.ts @@ -252,6 +252,9 @@ export class MtxTime implements OnChanges, AfterViewInit, OnDestroy { /** Step over minutes. */ @Input() interval: number = 1; + /** Includes the option to enter seconds. */ + @Input() withSeconds: boolean = false; + @ViewChild('hourInput', { read: ElementRef }) protected hourInputElement: ElementRef | undefined; @@ -264,6 +267,12 @@ export class MtxTime implements OnChanges, AfterViewInit, OnDestroy { @ViewChild('minuteInput', { read: MtxTimeInput }) protected minuteInputDirective: MtxTimeInput | undefined; + @ViewChild('secondInput', { read: ElementRef }) + protected secondInputElement: ElementRef | undefined; + + @ViewChild('secondInput', { read: MtxTimeInput }) + protected secondInputDirective: MtxTimeInput | undefined; + datetimepickerIntlChangesSubscription: SubscriptionLike; /** Whether the clock uses 12 hour format. */ @@ -341,7 +350,11 @@ export class MtxTime implements OnChanges, AfterViewInit, OnDestroy { } get isMinuteView() { - return this._clockView === 'hour'; + return this._clockView === 'minute'; + } + + get isSecondView() { + return this._clockView === 'second'; } get hour() { @@ -373,6 +386,14 @@ export class MtxTime implements OnChanges, AfterViewInit, OnDestroy { return '00'; } + get second() { + if (this.activeDate) { + return this.prefixWithZero(this._adapter.getSecond(this.activeDate)); + } + + return '00'; + } + prefixWithZero(value: number) { if (value < 10) { return '0' + String(value); @@ -409,10 +430,14 @@ export class MtxTime implements OnChanges, AfterViewInit, OnDestroy { if (this.hourInputElement) { (this.hourInputElement.nativeElement as HTMLInputElement).focus(); } - } else { + } else if (this.clockView === 'minute') { if (this.minuteInputElement) { (this.minuteInputElement.nativeElement as HTMLInputElement).focus(); } + } else if (this.withSeconds && this.clockView === 'second') { + if (this.secondInputElement) { + (this.secondInputElement.nativeElement as HTMLInputElement).focus(); + } } } @@ -424,7 +449,8 @@ export class MtxTime implements OnChanges, AfterViewInit, OnDestroy { this._adapter.getMonth(this.activeDate), this._adapter.getDate(this.activeDate), this.updateHourForAmPm(hour), - this._adapter.getMinute(this.activeDate) + this._adapter.getMinute(this.activeDate), + this._adapter.getSecond(this.activeDate), ); this._activeDate = this._adapter.clampDate(newValue, this.minDate, this.maxDate); @@ -471,7 +497,8 @@ export class MtxTime implements OnChanges, AfterViewInit, OnDestroy { this._adapter.getMonth(this.activeDate), this._adapter.getDate(this.activeDate), this._adapter.getHour(this._activeDate), - minute + minute, + this._adapter.getSecond(this.activeDate), ); this._activeDate = this._adapter.clampDate(newValue, this.minDate, this.maxDate); this.activeDateChange.emit(this.activeDate); @@ -486,6 +513,30 @@ export class MtxTime implements OnChanges, AfterViewInit, OnDestroy { } } + handleSecondInputChange(value: NumberInput) { + const second = coerceNumberProperty(value); + if (second || second === 0) { + const newValue = this._adapter.createDatetime( + this._adapter.getYear(this.activeDate), + this._adapter.getMonth(this.activeDate), + this._adapter.getDate(this.activeDate), + this._adapter.getHour(this._activeDate), + this._adapter.getMinute(this.activeDate), + second + ); + this._activeDate = this._adapter.clampDate(newValue, this.minDate, this.maxDate); + this.activeDateChange.emit(this.activeDate); + + // If previously we did set [mtxValue]="40" and the input changed to 30, and the clamping + // will make it "40" again then the secondInputDirective will not have been updated + // since "40" === "40" same reference so no change detected by directly setting it within + // this handler, we handle this usecase + if (this.secondInputDirective) { + this.secondInputDirective.timeValue = this.minute; + } + } + } + handleFocus(clockView: MtxClockView) { this.clockView = clockView; this.clockViewChange.emit(clockView); @@ -494,6 +545,8 @@ export class MtxTime implements OnChanges, AfterViewInit, OnDestroy { _timeSelected(date: D): void { if (this.clockView === 'hour') { this.clockView = 'minute'; + } else if (this.withSeconds && this.clockView === 'minute') { + this.clockView = 'second'; } this._activeDate = this.selected = date; } diff --git a/projects/extensions/datetimepicker/year-view.ts b/projects/extensions/datetimepicker/year-view.ts index ab0de49c..dd5458b4 100644 --- a/projects/extensions/datetimepicker/year-view.ts +++ b/projects/extensions/datetimepicker/year-view.ts @@ -124,6 +124,7 @@ export class MtxYearView implements AfterContentInit { month, 1, 0, + 0, 0 ); @@ -136,7 +137,8 @@ export class MtxYearView implements AfterContentInit { this._adapter.getNumDaysInMonth(normalizedDate) ), this._adapter.getHour(this.activeDate), - this._adapter.getMinute(this.activeDate) + this._adapter.getMinute(this.activeDate), + this._adapter.getSecond(this.activeDate) ) ); if (this.type === 'month') { @@ -179,7 +181,8 @@ export class MtxYearView implements AfterContentInit { month, 1, this._adapter.getHour(this.activeDate), - this._adapter.getMinute(this.activeDate) + this._adapter.getMinute(this.activeDate), + this._adapter.getSecond(this.activeDate) ), this._dateFormats.display.monthYearA11yLabel ); @@ -206,7 +209,8 @@ export class MtxYearView implements AfterContentInit { month, 1, this._adapter.getHour(this.activeDate), - this._adapter.getMinute(this.activeDate) + this._adapter.getMinute(this.activeDate), + this._adapter.getSecond(this.activeDate) ); // If any date in the month is enabled count the month as enabled.