UNPKG

@clr/angular

Version:

Angular components for Clarity

1,222 lines (1,209 loc) 154 kB
import * as i5$1 from '@angular/common'; import { getLocaleDayNames, FormStyle, TranslationWidth, getLocaleMonthNames, getLocaleFirstDayOfWeek, getLocaleDateFormat, FormatWidth, isPlatformBrowser, CommonModule } from '@angular/common'; import * as i0 from '@angular/core'; import { Injectable, LOCALE_ID, Inject, DOCUMENT, PLATFORM_ID, HostListener, Component, EventEmitter, Input, Output, ViewChild, Optional, forwardRef, HostBinding, Self, Directive, NgModule } from '@angular/core'; import * as i7 from '@clr/angular/forms/common'; import { ClrAbstractContainer, ControlIdService, ControlClassService, FormsFocusService, NgControlService, WrappedFormControl, ClrCommonFormsModule } from '@clr/angular/forms/common'; import * as i4 from '@clr/angular/utils'; import { DATEPICKER_ENABLE_BREAKPOINT, Keys, isBooleanAttributeSet, CdkTrapFocusModule, ClrHostWrappingModule, ClrConditionalModule } from '@clr/angular/utils'; import { first, filter, startWith } from 'rxjs/operators'; import * as i1 from '@clr/angular/popover/common'; import { ClrPopoverPosition, ClrPopoverType, DROPDOWN_POSITIONS, ClrPopoverHostDirective, ClrPopoverModuleNext } from '@clr/angular/popover/common'; import { Subject, tap } from 'rxjs'; import * as i5 from '@clr/angular/icon'; import { ClarityIcons, successStandardIcon, errorStandardIcon, angleIcon, eventIcon, calendarIcon, ClrIcon } from '@clr/angular/icon'; import * as i6 from '@clr/angular/layout/vertical-nav'; import { ClrVerticalNavModule } from '@clr/angular/layout/vertical-nav'; import * as i1$1 from '@angular/forms'; import { NG_VALIDATORS } from '@angular/forms'; /* * Copyright (c) 2016-2026 Broadcom. All Rights Reserved. * The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. * This software is released under MIT license. * The full license information can be found in LICENSE in the root directory of this project. */ class DateFormControlService { constructor() { this._touchedChange = new Subject(); this._dirtyChange = new Subject(); } get touchedChange() { return this._touchedChange.asObservable(); } get dirtyChange() { return this._dirtyChange.asObservable(); } markAsTouched() { this._touchedChange.next(); } markAsDirty() { this._dirtyChange.next(); } // friendly wrapper setDisabled(state) { this.disabled = state; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: DateFormControlService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: DateFormControlService }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: DateFormControlService, decorators: [{ type: Injectable }] }); /* * Copyright (c) 2016-2026 Broadcom. All Rights Reserved. * The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. * This software is released under MIT license. * The full license information can be found in LICENSE in the root directory of this project. */ class DayModel { constructor(year, month, date) { this.year = year; this.month = month; this.date = date; } /** * Checks if the passed CalendarDate is equal to itself. */ isEqual(day) { if (day) { return this.year === day.year && this.month === day.month && this.date === day.date; } return false; } toDate() { return new Date(this.year, this.month, this.date); } /** * Returns a new DayModel which is incremented based on the value passed. */ incrementBy(value) { // Creating new Javascript Date object to increment because // it will automatically take care of switching to next or previous // months & years without we having to worry about it. const date = new Date(this.year, this.month, this.date + value); return new DayModel(date.getFullYear(), date.getMonth(), date.getDate()); } /** * Clones the current day model. */ clone() { return new DayModel(this.year, this.month, this.date); } toComparisonString() { return `${this.year}${this.pad(this.month)}${this.pad(this.date)}`; } toDateString() { return this.toDate().toLocaleDateString(undefined, { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric', }); } /** * Compares the dates and returns boolean value based on the value passed */ isBefore(day, dayInclusive = false) { return dayInclusive ? this.toDate().getTime() <= day?.toDate().getTime() : this.toDate().getTime() < day?.toDate().getTime(); } /** * Compares the dates and returns boolean value based on the value passed */ isAfter(day, dayInclusive = false) { return dayInclusive ? this.toDate().getTime() >= day?.toDate().getTime() : this.toDate().getTime() > day?.toDate().getTime(); } pad(num) { return num < 10 ? `0${num}` : `${num}`; } } /* * Copyright (c) 2016-2026 Broadcom. All Rights Reserved. * The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. * This software is released under MIT license. * The full license information can be found in LICENSE in the root directory of this project. */ /** * This is the en-001 short locale date format. Setting as default. */ const DEFAULT_LOCALE_FORMAT = 'dd/MM/y'; // https://en.wikipedia.org/wiki/Date_format_by_country const LITTLE_ENDIAN_REGEX = /d+.+m+.+y+/i; const MIDDLE_ENDIAN_REGEX = /m+.+d+.+y+/i; // No need for BIG_ENDIAN_REGEX because anything that doesn't satisfy the above 2 // is automatically BIG_ENDIAN const DELIMITER_REGEX = /d+|m+|y+/i; const USER_INPUT_REGEX = /\d+/g; const MOBILE_USERAGENT_REGEX = /Mobi/i; const RTL_REGEX = /\u200f/g; const YEAR = 'YYYY'; const MONTH = 'MM'; const DATE = 'DD'; const LITTLE_ENDIAN = { name: 'LITTLE_ENDIAN', format: [DATE, MONTH, YEAR], }; const MIDDLE_ENDIAN = { name: 'MIDDLE_ENDIAN', format: [MONTH, DATE, YEAR], }; const BIG_ENDIAN = { name: 'BIG_ENDIAN', format: [YEAR, MONTH, DATE], }; const NO_OF_DAYS_IN_A_WEEK = 7; const NO_OF_ROWS_IN_CALENDAR_VIEW = 6; const TOTAL_DAYS_IN_DAYS_VIEW = NO_OF_DAYS_IN_A_WEEK * NO_OF_ROWS_IN_CALENDAR_VIEW; /* * Copyright (c) 2016-2026 Broadcom. All Rights Reserved. * The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. * This software is released under MIT license. * The full license information can be found in LICENSE in the root directory of this project. */ /** * Returns the number of days in a month. */ function getNumberOfDaysInTheMonth(year, month) { // If we go to the next month, but use a day of 0, it returns the last day from the previous month return new Date(year, month + 1, 0).getDate(); } /** * Returns the day for the corresponding date where 0 represents Sunday. */ function getDay(year, month, date) { return new Date(year, month, date).getDay(); } /** * Takes in a year and if it is a 2 digit year, returns the corresponding 4 digit year. * Window of 80 years before and 20 years after the present year. * Credit: https://github.com/globalizejs/globalize/blob/e1b31cd6a4f1cff75b185b68b7a32220aac5196f/src/date/parse.js */ function parseToFourDigitYear(year) { if (year > 9999 || (year > 100 && year < 999) || year < 10) { return -1; } if (year > 999) { return year; } const currYear = new Date().getFullYear(); const century = Math.floor(currYear / 100) * 100; let result = year + century; if (result > currYear + 20) { result = result - 100; } return result; } function datesAreEqual(date1, date2) { if (date1 instanceof Date && date2 instanceof Date) { return (date1.getFullYear() === date2.getFullYear() && date1.getMonth() === date2.getMonth() && date1.getDate() === date2.getDate()); } else { return false; } } /* * Copyright (c) 2016-2026 Broadcom. All Rights Reserved. * The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. * This software is released under MIT license. * The full license information can be found in LICENSE in the root directory of this project. */ /** * Index of the day of the week, matching JavaScript's Date.getDay() convention. * Used to override the locale-derived first day of the week in the date picker. */ var ClrWeekday; (function (ClrWeekday) { ClrWeekday[ClrWeekday["Sunday"] = 0] = "Sunday"; ClrWeekday[ClrWeekday["Monday"] = 1] = "Monday"; ClrWeekday[ClrWeekday["Tuesday"] = 2] = "Tuesday"; ClrWeekday[ClrWeekday["Wednesday"] = 3] = "Wednesday"; ClrWeekday[ClrWeekday["Thursday"] = 4] = "Thursday"; ClrWeekday[ClrWeekday["Friday"] = 5] = "Friday"; ClrWeekday[ClrWeekday["Saturday"] = 6] = "Saturday"; })(ClrWeekday || (ClrWeekday = {})); /* * Copyright (c) 2016-2026 Broadcom. All Rights Reserved. * The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. * This software is released under MIT license. * The full license information can be found in LICENSE in the root directory of this project. */ /** * This service extracts the Angular CLDR data needed by the datepicker. */ class LocaleHelperService { constructor(locale) { this.locale = locale; this._firstDayOfWeek = ClrWeekday.Sunday; this.initializeLocaleData(); } get firstDayOfWeek() { return this._firstDayOfWeek; } get localeDays() { return this._localeDays; } // leave for backward compatibility get localeDaysNarrow() { return this._localeDays.map(day => day.narrow); } get localeMonthsAbbreviated() { return this._localeMonthsAbbreviated; } get localeMonthsWide() { return this._localeMonthsWide; } get localeDateFormat() { return this._localeDateFormat; } /** * Overrides the first day of the week regardless of locale. * Accepts a `ClrWeekday` value (Sunday=0 through Saturday=6), or null to revert to locale default. * Incorrect values will revert to default value (Sunday). */ updateFirstDayOfWeek(day) { if (day === null || day < ClrWeekday.Sunday || day > ClrWeekday.Saturday) { this.initializeLocaleFirstDayOfWeek(); this.initializeLocaleDays(); return; } this._firstDayOfWeek = day; this.initializeLocaleDays(); } /** * Initializes the locale data. */ initializeLocaleData() { // Order in which these functions is called is very important. this.initializeLocaleFirstDayOfWeek(); this.initializeLocaleDateFormat(); this.initializeLocaleMonthsAbbreviated(); this.initializeLocaleMonthsWide(); this.initializeLocaleDays(); } /** * Initialize day names based on the locale. * eg: [{day: Sunday, narrow: S}, {day: Monday, narrow: M}...] for en-US. */ initializeLocaleDays() { // Get locale day names starting with Sunday const tempArr = []; const tempWideArr = getLocaleDayNames(this.locale, FormStyle.Standalone, TranslationWidth.Wide).slice(); const tempNarrowArr = getLocaleDayNames(this.locale, FormStyle.Standalone, TranslationWidth.Narrow).slice(); for (let i = 0; i < 7; i++) { tempArr.push({ day: tempWideArr[i], narrow: tempNarrowArr[i] }); } // Rearrange the tempArr to start with the first day of the week based on the locale (default or override). if (this.firstDayOfWeek > ClrWeekday.Sunday) { const prevDays = tempArr.splice(0, this.firstDayOfWeek); tempArr.push(...prevDays); } this._localeDays = tempArr; } /** * Initializes the array of month names in the TranslationWidth.Abbreviated format. * e.g. `[Jan, Feb, ...]` for en-US */ initializeLocaleMonthsAbbreviated() { this._localeMonthsAbbreviated = getLocaleMonthNames(this.locale, FormStyle.Standalone, TranslationWidth.Abbreviated).slice(); } /** * Initializes the array of month names in the TranslationWidth.Wide format. * e.g. `[January, February, ...]` for en-US */ initializeLocaleMonthsWide() { this._localeMonthsWide = getLocaleMonthNames(this.locale, FormStyle.Standalone, TranslationWidth.Wide).slice(); } /** * Initializes the first day of the week based on the locale. */ initializeLocaleFirstDayOfWeek() { this._firstDayOfWeek = getLocaleFirstDayOfWeek(this.locale); } initializeLocaleDateFormat() { this._localeDateFormat = getLocaleDateFormat(this.locale, FormatWidth.Short); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: LocaleHelperService, deps: [{ token: LOCALE_ID }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: LocaleHelperService }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: LocaleHelperService, decorators: [{ type: Injectable }], ctorParameters: () => [{ type: undefined, decorators: [{ type: Inject, args: [LOCALE_ID] }] }] }); /* * Copyright (c) 2016-2026 Broadcom. All Rights Reserved. * The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. * This software is released under MIT license. * The full license information can be found in LICENSE in the root directory of this project. */ class DateIOService { constructor(localeHelperService) { /** * This is the default range. It approximates the beginning of time to the end of time. * The disabled dates are the dates that are not allowed to be selected. * The min date is the earliest date that can be selected. * The max date is the latest date that can be selected. * Unless a minDate or maxDate is set with the native HTML5 api the range is all dates */ this.disabledDates = { minDate: new DayModel(0, 0, 1), maxDate: new DayModel(9999, 11, 31), }; this.cldrLocaleDateFormat = DEFAULT_LOCALE_FORMAT; this.minDateChange = new Subject(); this.maxDateChange = new Subject(); this.localeDisplayFormat = LITTLE_ENDIAN; this.delimiters = ['/', '/']; this.cldrLocaleDateFormat = localeHelperService.localeDateFormat; this.initializeLocaleDisplayFormat(); } get placeholderText() { const format = this.localeDisplayFormat.format; return format[0] + this.delimiters[0] + format[1] + this.delimiters[1] + format[2]; } setMinDate(date) { // NOTE: I'm expecting consumers to pass one of four things here: // A proper date string(2019-11-11), null, undefined or empty string ('') if (!date) { // attribute binding was removed, reset back to the beginning of time this.disabledDates.minDate = new DayModel(0, 0, 1); } else { const [year, month, day] = date.split('-').map(n => parseInt(n, 10)); this.disabledDates.minDate = new DayModel(year, month - 1, day); } this.minDateChange.next(this.disabledDates.minDate); } setMaxDate(date) { // NOTE: I'm expecting consumers to pass one of four things here: // A proper date string(2019-11-11), null, undefined or empty string ('') if (!date) { // attribute binding was removed, reset forward to the end of time this.disabledDates.maxDate = new DayModel(9999, 11, 31); } else { const [year, month, day] = date.split('-').map(n => parseInt(n, 10)); this.disabledDates.maxDate = new DayModel(year, month - 1, day); } this.maxDateChange.next(this.disabledDates.maxDate); } setRangeOptions(rangeOptions) { const validatedRangeOption = this.validateDateRangeOptions(rangeOptions); this.dateRangeOptions = validatedRangeOption || []; } getRangeOptions() { return this.dateRangeOptions; } toLocaleDisplayFormatString(date) { if (date) { if (isNaN(date.getTime())) { return ''; } const dateNo = date.getDate(); const monthNo = date.getMonth() + 1; const dateStr = dateNo > 9 ? dateNo.toString() : '0' + dateNo; const monthStr = monthNo > 9 ? monthNo.toString() : '0' + monthNo; if (this.localeDisplayFormat === LITTLE_ENDIAN) { return dateStr + this.delimiters[0] + monthStr + this.delimiters[1] + date.getFullYear(); } else if (this.localeDisplayFormat === MIDDLE_ENDIAN) { return monthStr + this.delimiters[0] + dateStr + this.delimiters[1] + date.getFullYear(); } else { return date.getFullYear() + this.delimiters[0] + monthStr + this.delimiters[1] + dateStr; } } return ''; } getDateValueFromDateString(date) { if (!date || typeof date !== 'string') { return null; } const dateParts = date.match(USER_INPUT_REGEX); if (!dateParts || dateParts.length !== 3) { return null; } const [firstPart, secondPart, thirdPart] = dateParts; if (this.localeDisplayFormat === LITTLE_ENDIAN) { // secondPart is month && firstPart is date return this.validateAndGetDate(thirdPart, secondPart, firstPart); } else if (this.localeDisplayFormat === MIDDLE_ENDIAN) { // firstPart is month && secondPart is date return this.validateAndGetDate(thirdPart, firstPart, secondPart); } else { // secondPart is month && thirdPart is date return this.validateAndGetDate(firstPart, secondPart, thirdPart); } } validateDateRangeOptions(rangeOptions) { const validOptions = []; rangeOptions?.forEach((rangeOption) => { if (rangeOption?.value?.length !== 2 || Object.prototype.toString.call(rangeOption?.value[0]) !== '[object Date]' || Object.prototype.toString.call(rangeOption?.value[1]) !== '[object Date]') { return; } validOptions.push(rangeOption); }); return validOptions; } initializeLocaleDisplayFormat() { const format = this.cldrLocaleDateFormat.toLocaleLowerCase(); if (LITTLE_ENDIAN_REGEX.test(format)) { this.localeDisplayFormat = LITTLE_ENDIAN; } else if (MIDDLE_ENDIAN_REGEX.test(format)) { this.localeDisplayFormat = MIDDLE_ENDIAN; } else { // everything else is set to BIG-ENDIAN FORMAT this.localeDisplayFormat = BIG_ENDIAN; } this.extractDelimiters(); } extractDelimiters() { if (this.cldrLocaleDateFormat) { // Sanitize Date Format. Remove RTL characters. // FIXME: When we support RTL, remove this and handle it correctly. const localeFormat = this.cldrLocaleDateFormat.replace(RTL_REGEX, ''); const delimiters = localeFormat.split(DELIMITER_REGEX); // NOTE: The split from the CLDR date format should always result // in an arary with 4 elements. The 1st and the 2nd values are the delimiters // we will use in order. // Eg: "dd/MM/y".split(/d+|m+|y+/i) results in ["", "/", "/", ""] if (delimiters && delimiters.length === 4) { this.delimiters = [delimiters[1], delimiters[2]]; } else { console.error('Unexpected date format received. Delimiters extracted: ', delimiters); } } } /** * Checks if the month entered by the user is valid or not. * Note: Month is 0 based. */ isValidMonth(month) { return month > -1 && month < 12; } /** * Checks if the date is valid depending on the year and month provided. */ isValidDate(year, month, date) { return date > 0 && date <= getNumberOfDaysInTheMonth(year, month); } /** * Validates the parameters provided and returns the date. * If the parameters are not * valid then return null. * NOTE: (Month here is 1 based since the user has provided that as an input) */ validateAndGetDate(year, month, date) { // I don't know whats wrong with the TS compiler. It throws an error if I write // the below if statement. The error is: // Operator '!==' cannot be applied to types '2' and '4' // More info here: https://github.com/Microsoft/TypeScript/issues/12794#issuecomment-270342936 /* if (year.length !== 2 || year.length !== 4) { return null; } */ // Instead I have to write the logic like this x-( const y = +year; const m = +month - 1; // month is 0 based const d = +date; if (!this.isValidMonth(m) || !this.isValidDate(y, m, d)) { return null; } const result = parseToFourDigitYear(y); return result !== -1 ? new Date(result, m, d) : null; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: DateIOService, deps: [{ token: LocaleHelperService }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: DateIOService }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: DateIOService, decorators: [{ type: Injectable }], ctorParameters: () => [{ type: LocaleHelperService }] }); /* * Copyright (c) 2016-2026 Broadcom. All Rights Reserved. * The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. * This software is released under MIT license. * The full license information can be found in LICENSE in the root directory of this project. */ class CalendarModel { constructor(year, month) { this.year = year; this.month = month; this.initializeDaysInCalendar(); } /** * Checks if the calendar passed is equal to the current calendar. */ isEqual(calendar) { if (calendar) { return this.year === calendar.year && this.month === calendar.month; } return false; } /** * Checks if a DayModel is in the Calendar */ isDayInCalendar(day) { if (day) { return this.year === day.year && this.month === day.month; } return false; } /** * Returns CalendarModel of the previous month. */ previousMonth() { if (this.month === 0) { return new CalendarModel(this.year - 1, 11); } else { return new CalendarModel(this.year, this.month - 1); } } /** * Returns CalendarModel of the next month. */ nextMonth() { if (this.month === 11) { return new CalendarModel(this.year + 1, 0); } else { return new CalendarModel(this.year, this.month + 1); } } /** * Returns CalendarModel of the previous year. */ previousYear() { return new CalendarModel(this.year - 1, this.month); } /** * Returns CalendarModel of the next year. */ nextYear() { return new CalendarModel(this.year + 1, this.month); } /** * Populates the days array with the DayModels in the current Calendar. */ initializeDaysInCalendar() { const noOfDaysInCalendar = getNumberOfDaysInTheMonth(this.year, this.month); this.days = Array(noOfDaysInCalendar) .fill(null) .map((_date, index) => { return new DayModel(this.year, this.month, index + 1); }); } } /* * Copyright (c) 2016-2026 Broadcom. All Rights Reserved. * The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. * This software is released under MIT license. * The full license information can be found in LICENSE in the root directory of this project. */ /** * This service is responsible for: * 1. Initializing the displayed calendar. * 2. Moving the calendar to the next, previous or current months * 3. Managing the focused and selected day models. */ class DateNavigationService { constructor() { this.isRangePicker = false; this.hasActionButtons = false; this._todaysFullDate = new Date(); this._selectedDayChange = new Subject(); this._selectedEndDayChange = new Subject(); this._displayedCalendarChange = new Subject(); this._focusOnCalendarChange = new Subject(); this._refreshCalendarView = new Subject(); this._focusedDayChange = new Subject(); } get today() { return this._today; } get displayedCalendar() { return this._displayedCalendar; } get selectedDayChange() { return this._selectedDayChange.asObservable(); } get selectedEndDayChange() { return this._selectedEndDayChange.asObservable(); } /** * This observable lets the subscriber know that the displayed calendar has changed. */ get displayedCalendarChange() { return this._displayedCalendarChange.asObservable(); } /** * This observable lets the subscriber know that the focus should be applied on the calendar. */ get focusOnCalendarChange() { return this._focusOnCalendarChange.asObservable(); } /** * This observable lets the subscriber know that the focused day in the displayed calendar has changed. */ get focusedDayChange() { return this._focusedDayChange.asObservable().pipe(tap((day) => (this.focusedDay = day))); } /** * This observable lets the subscriber know that the displayed calendar has changed. */ get refreshCalendarView() { return this._refreshCalendarView.asObservable(); } /** * Notifies that the selected day has changed so that the date can be emitted to the user. */ notifySelectedDayChanged(dayObject, { emitEvent } = { emitEvent: true }) { if (this.isRangePicker) { const { startDate, endDate } = dayObject; if (startDate && endDate) { this.setSelectedDay(startDate, emitEvent); this.setSelectedEndDay(endDate, emitEvent); } else { if (endDate !== null) { this.setSelectedEndDay(endDate, emitEvent); } if (startDate !== null) { this.setSelectedDay(startDate, emitEvent); } } } else { const day = dayObject; this.setSelectedDay(day, emitEvent); } this._refreshCalendarView.next(); } /** * Initializes the calendar based on the selected day. */ initializeCalendar() { this.focusedDay = null; // Can be removed later on the store focus this.initializeTodaysDate(); if (this.selectedDay) { this._displayedCalendar = new CalendarModel(this.selectedDay.year, this.selectedDay.month); } else { this._displayedCalendar = new CalendarModel(this.today.year, this.today.month); } } changeMonth(month) { this.setDisplayedCalendar(new CalendarModel(this._displayedCalendar.year, month)); } changeYear(year) { this.setDisplayedCalendar(new CalendarModel(year, this._displayedCalendar.month)); } /** * Moves the displayed calendar to the next month. */ moveToNextMonth() { this.setDisplayedCalendar(this._displayedCalendar.nextMonth()); } /** * Moves the displayed calendar to the previous month. */ moveToPreviousMonth() { this.setDisplayedCalendar(this._displayedCalendar.previousMonth()); } /** * Moves the displayed calendar to the next year. */ moveToNextYear() { this.setDisplayedCalendar(this._displayedCalendar.nextYear()); } /** * Moves the displayed calendar to the previous year. */ moveToPreviousYear() { this.setDisplayedCalendar(this._displayedCalendar.previousYear()); } /** * Moves the displayed calendar to the current month and year. */ moveToCurrentMonth() { if (!this.displayedCalendar.isDayInCalendar(this.today)) { this.setDisplayedCalendar(new CalendarModel(this.today.year, this.today.month)); } this._focusOnCalendarChange.next(); } moveToSpecificMonth(day) { if (!this.displayedCalendar.isDayInCalendar(day)) { this.setDisplayedCalendar(new CalendarModel(day.year, day.month)); } } incrementFocusDay(value) { this.hoveredDay = this.focusedDay = this.focusedDay.incrementBy(value); if (this._displayedCalendar.isDayInCalendar(this.focusedDay)) { this._focusedDayChange.next(this.focusedDay); } else { this.setDisplayedCalendar(new CalendarModel(this.focusedDay.year, this.focusedDay.month)); } this._focusOnCalendarChange.next(); } resetSelectedDay() { this.selectedDay = this.persistedDate; this.selectedEndDay = this.persistedEndDate; } convertDateToDayModel(date) { return new DayModel(date.getFullYear(), date.getMonth(), date.getDate()); } setSelectedDay(dayModel, emitEvent) { this.selectedDay = dayModel; if (emitEvent) { this._selectedDayChange.next(dayModel); } } setSelectedEndDay(dayModel, emitEvent) { this.selectedEndDay = dayModel; if (emitEvent) { this._selectedEndDayChange.next(dayModel); } } // not a setter because i want this to remain private setDisplayedCalendar(value) { if (!this._displayedCalendar.isEqual(value)) { this._displayedCalendar = value; this._displayedCalendarChange.next(); } } initializeTodaysDate() { this._todaysFullDate = new Date(); this._today = new DayModel(this._todaysFullDate.getFullYear(), this._todaysFullDate.getMonth(), this._todaysFullDate.getDate()); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: DateNavigationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: DateNavigationService }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: DateNavigationService, decorators: [{ type: Injectable }] }); /* * Copyright (c) 2016-2026 Broadcom. All Rights Reserved. * The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. * This software is released under MIT license. * The full license information can be found in LICENSE in the root directory of this project. */ class DatepickerEnabledService { constructor(_document) { this._document = _document; this._isUserAgentMobile = false; if (_document) { this._isUserAgentMobile = MOBILE_USERAGENT_REGEX.test(_document.defaultView.navigator.userAgent); this._innerWidth = _document.defaultView.innerWidth; } } /** * Returns if the calendar should be active or not. * If the user agent is mobile and the screen width is less than DATEPICKER_ACTIVE_BREAKPOINT * then the calendar is inactive. */ get isEnabled() { // https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent // What they recommend is: //"In summary, we recommend looking for the string 'Mobi' // anywhere in the User Agent to detect a mobile device." if (this._document) { if (this._innerWidth < DATEPICKER_ENABLE_BREAKPOINT && this._isUserAgentMobile) { return false; } } return true; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: DatepickerEnabledService, deps: [{ token: DOCUMENT }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: DatepickerEnabledService }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: DatepickerEnabledService, decorators: [{ type: Injectable }], ctorParameters: () => [{ type: undefined, decorators: [{ type: Inject, args: [DOCUMENT] }] }] }); /* * Copyright (c) 2016-2026 Broadcom. All Rights Reserved. * The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. * This software is released under MIT license. * The full license information can be found in LICENSE in the root directory of this project. */ /** * This service manages which view is visible in the datepicker popover. */ class ViewManagerService { constructor() { this.position = ClrPopoverPosition.BOTTOM_LEFT; this._currentView = "DAYVIEW" /* DatepickerViewEnum.DAYVIEW */; } get isDayView() { return this._currentView === "DAYVIEW" /* DatepickerViewEnum.DAYVIEW */; } get isYearView() { return this._currentView === "YEARVIEW" /* DatepickerViewEnum.YEARVIEW */; } get isMonthView() { return this._currentView === "MONTHVIEW" /* DatepickerViewEnum.MONTHVIEW */; } changeToMonthView() { this._currentView = "MONTHVIEW" /* DatepickerViewEnum.MONTHVIEW */; } changeToYearView() { this._currentView = "YEARVIEW" /* DatepickerViewEnum.YEARVIEW */; } changeToDayView() { this._currentView = "DAYVIEW" /* DatepickerViewEnum.DAYVIEW */; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ViewManagerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ViewManagerService }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ViewManagerService, decorators: [{ type: Injectable }] }); /* * Copyright (c) 2016-2026 Broadcom. All Rights Reserved. * The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. * This software is released under MIT license. * The full license information can be found in LICENSE in the root directory of this project. */ /** * This service focuses the day that is focusable in the calendar. */ class DatepickerFocusService { constructor(_ngZone, platformId) { this._ngZone = _ngZone; this.platformId = platformId; } focusCell(elRef) { this._ngZone.runOutsideAngular(() => { this.ngZoneIsStableInBrowser().subscribe(() => { const focusEl = elRef.nativeElement.querySelector('[tabindex="0"]'); if (focusEl) { focusEl.focus(); } }); }); } focusInput(element) { this._ngZone.runOutsideAngular(() => this.ngZoneIsStableInBrowser().subscribe(() => element.focus())); } elementIsFocused(element) { return isPlatformBrowser(this.platformId) && document.activeElement === element; } ngZoneIsStableInBrowser() { // Credit: Material: https://github.com/angular/material2/blob/master/src/lib/datepicker/calendar.ts return this._ngZone.onStable.asObservable().pipe(first(), filter(() => isPlatformBrowser(this.platformId))); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: DatepickerFocusService, deps: [{ token: i0.NgZone }, { token: PLATFORM_ID }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: DatepickerFocusService }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: DatepickerFocusService, decorators: [{ type: Injectable }], ctorParameters: () => [{ type: i0.NgZone }, { type: undefined, decorators: [{ type: Inject, args: [PLATFORM_ID] }] }] }); /* * Copyright (c) 2016-2026 Broadcom. All Rights Reserved. * The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. * This software is released under MIT license. * The full license information can be found in LICENSE in the root directory of this project. */ class ClrMonthpicker { constructor(_localeHelperService, _dateNavigationService, _datepickerFocusService, _elRef, _viewManagerService, commonStrings) { this._localeHelperService = _localeHelperService; this._dateNavigationService = _dateNavigationService; this._datepickerFocusService = _datepickerFocusService; this._elRef = _elRef; this._viewManagerService = _viewManagerService; this.commonStrings = commonStrings; this._focusedMonthIndex = this.calendarMonthIndex; } /** * Gets the months array which is used to rendered the monthpicker view. * Months are in the TranslationWidth.Wide format. */ get monthNames() { return this._localeHelperService.localeMonthsWide; } /** * Gets the month value of the Calendar. */ get calendarMonthIndex() { return this._dateNavigationService.displayedCalendar.month; } /** * Gets the year which the user is currently on. */ get calendarEndMonthIndex() { return this._dateNavigationService.selectedEndDay?.month; } get yearAttrString() { return this.commonStrings.parse(this.commonStrings.keys.datepickerSelectYearText, { CALENDAR_YEAR: this.calendarYear.toString(), }); } /** * Returns the year value of the calendar. */ get calendarYear() { return this._dateNavigationService.displayedCalendar.year; } get currentCalendarYear() { return new Date().getFullYear(); } get currentCalendarMonth() { return new Date().getMonth(); } getIsRangeStartMonth(monthIndex) { return (this._dateNavigationService.isRangePicker && this.calendarYear === this._dateNavigationService.selectedDay?.year && monthIndex === this._dateNavigationService.selectedDay?.month); } getIsRangeEndMonth(monthIndex) { return (this._dateNavigationService.isRangePicker && this.calendarYear === this._dateNavigationService.selectedEndDay?.year && monthIndex === this._dateNavigationService.selectedEndDay?.month); } /** * Calls the ViewManagerService to change to the yearpicker view. */ changeToYearView() { this._viewManagerService.changeToYearView(); } /** * Focuses on the current calendar month when the View is initialized. */ ngAfterViewInit() { this._datepickerFocusService.focusCell(this._elRef); } /** * Handles the Keyboard arrow navigation for the monthpicker. */ onKeyDown(event) { // NOTE: Didn't move this to the date navigation service because // the logic is fairly simple and it didn't make sense for me // to create extra observables just to move this logic to the service. if (event) { const key = event.key; if (key === Keys.ArrowUp && this._focusedMonthIndex > 1) { event.preventDefault(); this._focusedMonthIndex -= 2; this._datepickerFocusService.focusCell(this._elRef); } else if (key === Keys.ArrowDown && this._focusedMonthIndex < 10) { event.preventDefault(); this._focusedMonthIndex += 2; this._datepickerFocusService.focusCell(this._elRef); } else if (key === Keys.ArrowRight && this._focusedMonthIndex < 11) { event.preventDefault(); this._focusedMonthIndex++; this._datepickerFocusService.focusCell(this._elRef); } else if (key === Keys.ArrowLeft && this._focusedMonthIndex > 0) { event.preventDefault(); this._focusedMonthIndex--; this._datepickerFocusService.focusCell(this._elRef); } } } isSelected(monthIndex) { return ((this._dateNavigationService.selectedDay?.year === this.calendarYear && monthIndex === this._dateNavigationService.selectedDay?.month) || (this._dateNavigationService.selectedEndDay?.year === this.calendarYear && monthIndex === this.calendarEndMonthIndex)); } /** * Calls the DateNavigationService to update the hovered month value of the calendar */ onHover(monthIndex) { this._dateNavigationService.hoveredMonth = monthIndex; } /** * Calls the DateNavigationService to update the month value of the calendar. * Also changes the view to the daypicker. */ changeMonth(monthIndex) { this._dateNavigationService.changeMonth(monthIndex); this._viewManagerService.changeToDayView(); } /** * Compares the month passed to the focused month and returns the tab index. */ getTabIndex(monthIndex) { return monthIndex === this._focusedMonthIndex ? 0 : -1; } /** * Calls the DateNavigationService to move to the next month. */ nextYear() { this._dateNavigationService.moveToNextYear(); } /** * Calls the DateNavigationService to move to the previous month. */ previousYear() { this._dateNavigationService.moveToPreviousYear(); } /** * Calls the DateNavigationService to move to the current month. */ currentYear() { this._dateNavigationService.moveToCurrentMonth(); } /** * Applicable only to date range picker * Compares the month passed is in between the start and end date range */ isInRange(monthIndex) { if (!this._dateNavigationService.isRangePicker) { return false; } if (this._dateNavigationService.selectedDay && this._dateNavigationService.selectedEndDay) { return ((this.calendarYear === this._dateNavigationService.selectedDay.year && monthIndex > this._dateNavigationService.selectedDay.month && this.calendarYear === this._dateNavigationService.selectedEndDay.year && monthIndex < this._dateNavigationService.selectedEndDay.month) || (this._dateNavigationService.selectedDay.year !== this._dateNavigationService.selectedEndDay.year && this.calendarYear === this._dateNavigationService.selectedDay.year && monthIndex > this._dateNavigationService.selectedDay.month) || (this._dateNavigationService.selectedDay.year !== this._dateNavigationService.selectedEndDay.year && this.calendarYear === this._dateNavigationService.selectedEndDay.year && monthIndex < this._dateNavigationService.selectedEndDay.month) || (this.calendarYear > this._dateNavigationService.selectedDay.year && this.calendarYear < this._dateNavigationService.selectedEndDay.year)); } else if (this._dateNavigationService.selectedDay && !this._dateNavigationService.selectedEndDay) { return ((this.calendarYear === this._dateNavigationService.selectedDay.year && monthIndex > this._dateNavigationService.selectedDay.month && monthIndex < this._dateNavigationService.hoveredMonth) || (this.calendarYear > this._dateNavigationService.selectedDay.year && monthIndex < this._dateNavigationService.hoveredMonth)); } else { return false; } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrMonthpicker, deps: [{ token: LocaleHelperService }, { token: DateNavigationService }, { token: DatepickerFocusService }, { token: i0.ElementRef }, { token: ViewManagerService }, { token: i4.ClrCommonStringsService }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: ClrMonthpicker, isStandalone: false, selector: "clr-monthpicker", host: { attributes: { "role": "application" }, listeners: { "keydown": "onKeyDown($event)" }, properties: { "class.monthpicker": "true" } }, ngImport: i0, template: ` <div class="calendar-header in-monthpicker"> <div class="year-view-switcher"> <button class="calendar-btn yearpicker-trigger" type="button" (click)="changeToYearView()" [attr.aria-label]="yearAttrString" [attr.title]="yearAttrString" > {{ calendarYear }} </button> </div> <div class="calendar-switchers"> <button class="calendar-btn switcher" type="button" (click)="previousYear()" [attr.aria-label]="commonStrings.keys.datepickerPreviousMonth" > <cds-icon shape="angle" direction="left" [attr.title]="commonStrings.keys.datepickerPreviousMonth"></cds-icon> </button> <button class="calendar-btn switcher" type="button" (click)="currentYear()" [attr.aria-label]="commonStrings.keys.datepickerCurrentMonth" > <cds-icon shape="event" [attr.title]="commonStrings.keys.datepickerCurrentMonth"></cds-icon> </button> <button class="calendar-btn switcher" type="button" (click)="nextYear()" [attr.aria-label]="commonStrings.keys.datepickerNextMonth" > <cds-icon shape="angle" direction="right" [attr.title]="commonStrings.keys.datepickerNextMonth"></cds-icon> </button> </div> </div> <div class="months"> @for (month of monthNames; track month; let monthIndex = $index) { <button type="button" class="calendar-btn month" (click)="changeMonth(monthIndex)" [class.is-selected]="isSelected(monthIndex)" [class.is-start-range]="getIsRangeStartMonth(monthIndex)" [class.is-end-range]="getIsRangeEndMonth(monthIndex)" [class.in-range]="isInRange(monthIndex)" [attr.tabindex]="getTabIndex(monthIndex)" [class.is-today]="calendarYear === currentCalendarYear && monthIndex === currentCalendarMonth" (mouseenter)="onHover(monthIndex)" > {{ month }} </button> } </div> `, isInline: true, dependencies: [{ kind: "component", type: i5.ClrIcon, selector: "clr-icon, cds-icon", inputs: ["shape", "size", "direction", "flip", "solid", "status", "inverse", "badge"] }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrMonthpicker, decorators: [{ type: Component, args: [{ selector: 'clr-monthpicker', template: ` <div class="calendar-header in-monthpicker"> <div class="year-view-switcher"> <button class="calendar-btn yearpicker-trigger" type="button" (click)="changeToYearView()" [attr.aria-label]="yearAttrString" [attr.title]="yearAttrString" > {{ calendarYear }} </button> </div> <div class="calendar-switchers"> <button class="calendar-btn switcher" type="button" (click)="previousYear()" [attr.aria-label]="commonStrings.keys.datepickerPreviousMonth" > <cds-icon shape="angle" direction="left" [attr.title]="commonStrings.keys.datepickerPreviousMonth"></cds-icon> </button> <button class="calendar-btn switcher" type="button" (click)="currentYear()" [attr.aria-label]="commonStrings.keys.datepickerCurrentMonth" > <cds-icon shape="event" [attr.title]="commonStrings.keys.datepickerCurrentMonth"></cds-icon> </button> <button class="calendar-btn switcher" type="button" (click)="nextYear()" [attr.aria-label]="commonStrings.keys.datepickerNextMonth" > <cds-icon shape="angle" direction="right" [attr.title]="commonStrings.keys.datepickerNextMonth"></cds-icon> </button> </div> </div> <div class="months"> @for (month of monthNames; track month; let monthIn