@clr/angular
Version:
Angular components for Clarity
1,222 lines (1,209 loc) • 154 kB
JavaScript
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