UNPKG

@bimeister/pupakit.calendar

Version:
1 lines • 137 kB
{"version":3,"file":"bimeister-pupakit.calendar.mjs","sources":["../../src/components/calendar-cell-empty/calendar-cell-empty.component.ts","../../src/components/calendar-cell-empty/calendar-cell-empty.component.html","../../src/components/calendar-cell-separator/calendar-cell-separator.component.ts","../../src/components/calendar-cell-separator/calendar-cell-separator.component.html","../../src/components/calendar-cell/calendar-cell.component.ts","../../src/components/calendar-cell/calendar-cell.component.html","../../src/declarations/enums/calendar-quick-select-mode.enum.ts","../../src/declarations/enums/calendar-text-key.enum.ts","../../src/declarations/enums/day-of-week.enum.ts","../../src/declarations/constants/months-in-year.const.ts","../../src/declarations/constants/small-calendar-cycle-size-in-years.const.ts","../../src/declarations/constants/default-calendar-config.const.ts","../../src/declarations/constants/tokens/calendar-config.token.ts","../../src/services/calendar-config.service.ts","../../src/services/calendar-manipulator.service.ts","../../src/services/calendar-translation.service.ts","../../src/declarations/functions/get-clear-date.function.ts","../../src/declarations/functions/get-sorted-dates.function.ts","../../src/services/calendar-state.service.ts","../../src/components/calendar-control-panel/calendar-control-panel.component.ts","../../src/components/calendar-control-panel/calendar-control-panel.component.html","../../src/components/calendar-day/calendar-day.component.ts","../../src/components/calendar-day/calendar-day.component.html","../../src/components/calendar-header/calendar-header.component.ts","../../src/components/calendar-header/calendar-header.component.html","../../src/components/calendar-label/calendar-label.component.ts","../../src/components/calendar-label/calendar-label.component.html","../../src/components/calendar-selector-button/calendar-selector-button.component.ts","../../src/components/calendar-selector-button/calendar-selector-button.component.html","../../src/declarations/functions/get-current-calendar-month.function.ts","../../src/pipes/is-current-calendar-month.pipe.ts","../../src/components/calendar-month-selector/calendar-month-selector.component.ts","../../src/components/calendar-month-selector/calendar-month-selector.component.html","../../src/declarations/functions/get-date-from-calendar-day.function.ts","../../src/components/calendar-week/calendar-week.component.ts","../../src/components/calendar-week/calendar-week.component.html","../../src/declarations/constants/days-in-week.const.ts","../../src/declarations/functions/get-current-calendar-day.function.ts","../../src/declarations/functions/get-days-count-in-month.function.ts","../../src/declarations/constants/big-calendar-cycle-size-in-years.const.ts","../../src/declarations/constants/small-calendar-cycle-weekdays-sequence.const.ts","../../src/declarations/functions/is-leap-year.function.ts","../../src/declarations/functions/get-weekday-index.function.ts","../../src/declarations/functions/get-weekday-relative-to-new-week-start.function.ts","../../src/declarations/functions/is-calendar-days-equal.function.ts","../../src/pipes/month-day-cells.pipe.ts","../../src/declarations/functions/get-calendar-day-from-date.function.ts","../../src/pipes/is-day-selected.pipe.ts","../../src/declarations/functions/get-day-position-in-date-range.function.ts","../../src/pipes/day-position-in-date-range.pipe.ts","../../src/pipes/is-empty-day-in-date-range.pipe.ts","../../src/components/calendar-month/calendar-month.component.ts","../../src/components/calendar-month/calendar-month.component.html","../../src/declarations/functions/get-weeks-count-in-month.function.ts","../../src/declarations/functions/get-height-for-each-month-in-calendar-cycle.function.ts","../../src/declarations/classes/calendar-virtual-scroll-strategy.class.ts","../../src/pipes/next-calendar-month.pipe.ts","../../src/components/calendar-scroller/calendar-scroller.component.ts","../../src/components/calendar-scroller/calendar-scroller.component.html","../../src/components/calendar-week-panel/calendar-week-panel.component.ts","../../src/components/calendar-week-panel/calendar-week-panel.component.html","../../src/components/calendar-year-selector/calendar-year-selector.component.ts","../../src/components/calendar-year-selector/calendar-year-selector.component.html","../../src/declarations/functions/is-date.function.ts","../../src/components/calendar/calendar.component.ts","../../src/components/calendar/calendar.component.html","../../src/calendar.module.ts"],"sourcesContent":["import { ChangeDetectionStrategy, Component, Input, ViewEncapsulation } from '@angular/core';\n\n@Component({\n selector: 'pupa-calendar-cell-empty',\n templateUrl: './calendar-cell-empty.component.html',\n styleUrls: ['./calendar-cell-empty.component.scss'],\n encapsulation: ViewEncapsulation.Emulated,\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class CalendarCellEmptyComponent {\n @Input()\n public isInRange: boolean = false;\n}\n","<div class=\"cell\" [class.cell_in-range]=\"isInRange\"></div>\n","import { ChangeDetectionStrategy, Component, Input, ViewEncapsulation } from '@angular/core';\n\n@Component({\n selector: 'pupa-calendar-cell-separator',\n templateUrl: './calendar-cell-separator.component.html',\n styleUrls: ['./calendar-cell-separator.component.scss'],\n encapsulation: ViewEncapsulation.Emulated,\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class CalendarCellSeparatorComponent {\n @Input()\n public isInRange: boolean = false;\n}\n","<div class=\"filler\" [class.filler_in-range]=\"isInRange\"></div>\n","import { ChangeDetectionStrategy, Component, Input, ViewEncapsulation } from '@angular/core';\nimport { DayPositionInDateRange } from '../../declarations/types/day-position-in-date-range.type';\n\n@Component({\n selector: 'pupa-calendar-cell',\n templateUrl: './calendar-cell.component.html',\n styleUrls: ['./calendar-cell.component.scss'],\n encapsulation: ViewEncapsulation.Emulated,\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class CalendarCellComponent {\n @Input()\n public positionInRange: DayPositionInDateRange | undefined;\n}\n","<div class=\"cell\" [ngClass]=\"'cell_in-range-' + positionInRange\">\n <ng-content></ng-content>\n</div>\n","export enum CalendarQuickSelectMode {\n Month = 'Month',\n Year = 'Year',\n}\n","export enum CalendarTextKey {\n SelectYear = 'SelectYear',\n SelectMonth = 'SelectMonth',\n}\n","export enum DayOfWeek {\n Sunday,\n Monday,\n Tuesday,\n Wednesday,\n Thursday,\n Friday,\n Saturday,\n}\n","export const MONTHS_IN_YEAR: number = 12;\n","// calendars repeat every 28 years\nexport const SMALL_CALENDAR_CYCLE_SIZE_IN_YEARS: number = 28;\n","import { CalendarTextKey } from '../enums/calendar-text-key.enum';\nimport { DayOfWeek } from '../enums/day-of-week.enum';\nimport { MonthIndex } from '../enums/month-index.enum';\nimport { BaseCalendarConfig } from '../interfaces/base-calendar-config.interface';\nimport { SMALL_CALENDAR_CYCLE_SIZE_IN_YEARS } from './small-calendar-cycle-size-in-years.const';\n\nexport const DEFAULT_CALENDAR_CONFIG: BaseCalendarConfig = {\n startWeekday: DayOfWeek.Monday,\n yearsRange: SMALL_CALENDAR_CYCLE_SIZE_IN_YEARS * 7,\n translations: {\n en: {\n weekdays: {\n [DayOfWeek.Sunday]: 'Su',\n [DayOfWeek.Monday]: 'Mo',\n [DayOfWeek.Tuesday]: 'Tu',\n [DayOfWeek.Wednesday]: 'We',\n [DayOfWeek.Thursday]: 'Th',\n [DayOfWeek.Friday]: 'Fr',\n [DayOfWeek.Saturday]: 'Sa',\n },\n months: {\n [MonthIndex.January]: 'January',\n [MonthIndex.February]: 'February',\n [MonthIndex.March]: 'March',\n [MonthIndex.April]: 'April',\n [MonthIndex.May]: 'May',\n [MonthIndex.June]: 'June',\n [MonthIndex.July]: 'July',\n [MonthIndex.August]: 'August',\n [MonthIndex.September]: 'September',\n [MonthIndex.October]: 'October',\n [MonthIndex.November]: 'November',\n [MonthIndex.December]: 'December',\n },\n texts: {\n [CalendarTextKey.SelectYear]: 'Select year',\n [CalendarTextKey.SelectMonth]: 'Select month',\n },\n },\n },\n};\n","import { InjectionToken } from '@angular/core';\nimport { CalendarConfig } from '../../../declarations/interfaces/calendar-config.interface';\n\nexport const CALENDAR_CONFIG_TOKEN: InjectionToken<Partial<CalendarConfig>> = new InjectionToken<\n Partial<CalendarConfig>\n>('CALENDAR_CONFIG_TOKEN');\n","import { Inject, Injectable, Optional } from '@angular/core';\nimport { isEmpty, isNil } from '@bimeister/utilities';\nimport { DEFAULT_CALENDAR_CONFIG } from '../declarations/constants/default-calendar-config.const';\nimport { SMALL_CALENDAR_CYCLE_SIZE_IN_YEARS } from '../declarations/constants/small-calendar-cycle-size-in-years.const';\nimport { CALENDAR_CONFIG_TOKEN } from '../declarations/constants/tokens/calendar-config.token';\nimport { DayOfWeek } from '../declarations/enums/day-of-week.enum';\nimport { CalendarConfig } from '../declarations/interfaces/calendar-config.interface';\nimport { CalendarTranslation } from '../declarations/interfaces/calendar-translation.interface';\nimport { CalendarVirtualScrollConfig } from '../declarations/interfaces/calendar-virtual-scroll-config.interface';\n\nconst LABEL_HEIGHT_PX: number = 16;\nconst DIVIDER_HEIGHT_PX: number = 12;\nconst WEEK_HEIGHT_PX: number = 36;\nconst BUFFER_PX: number = 500;\n\n@Injectable()\nexport class CalendarConfigService {\n public readonly startWeekday: DayOfWeek;\n\n public readonly startYear: number;\n\n public readonly endYear: number;\n\n public readonly translations: Record<string, CalendarTranslation>;\n\n public readonly yearsRange: number;\n\n public get virtualScrollConfig(): CalendarVirtualScrollConfig {\n return {\n bufferPx: BUFFER_PX,\n yearsRange: this.yearsRange,\n labelHeightPx: LABEL_HEIGHT_PX,\n dividerHeightPx: DIVIDER_HEIGHT_PX,\n weekHeightPx: WEEK_HEIGHT_PX,\n startWeekday: this.startWeekday,\n startYear: this.startYear,\n };\n }\n\n constructor(@Optional() @Inject(CALENDAR_CONFIG_TOKEN) config: Partial<CalendarConfig>) {\n this.startWeekday = config?.startWeekday ?? DEFAULT_CALENDAR_CONFIG.startWeekday;\n\n this.startYear = config?.startYear ?? CalendarConfigService.getDefaultStartYear();\n\n CalendarConfigService.validateYearsRange(config?.yearsRange);\n\n this.yearsRange = config?.yearsRange ?? CalendarConfigService.getDefaultEndYear() - this.startYear;\n\n this.endYear = this.startYear + this.yearsRange;\n\n this.translations = !isEmpty(config?.translations) ? config.translations : DEFAULT_CALENDAR_CONFIG.translations;\n }\n\n private static getDefaultStartYear(): number {\n return new Date().getFullYear() - Math.floor(DEFAULT_CALENDAR_CONFIG.yearsRange / 2);\n }\n\n private static getDefaultEndYear(): number {\n return new Date().getFullYear() + Math.floor(DEFAULT_CALENDAR_CONFIG.yearsRange / 2);\n }\n\n private static validateYearsRange(yearsRange: number | undefined): void {\n const isValid: boolean = isNil(yearsRange) || yearsRange % SMALL_CALENDAR_CYCLE_SIZE_IN_YEARS === 0;\n\n if (isValid) {\n return;\n }\n\n throw new Error(`[CalendarConfigService] 'yearsRange' must be a multiple of ${SMALL_CALENDAR_CYCLE_SIZE_IN_YEARS}`);\n }\n}\n","import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';\nimport { Injectable } from '@angular/core';\nimport { filterNotNil, filterTruthy, isNil, shareReplayWithRefCount } from '@bimeister/utilities';\nimport { BehaviorSubject, Observable } from 'rxjs';\nimport { map, startWith, switchMap, take } from 'rxjs/operators';\nimport { MONTHS_IN_YEAR } from '../declarations/constants/months-in-year.const';\nimport { MonthIndex } from '../declarations/enums/month-index.enum';\nimport { CalendarMonth } from '../declarations/interfaces/calendar-month.interface';\nimport { CalendarConfigService } from './calendar-config.service';\n\n@Injectable()\nexport class CalendarManipulatorService {\n private readonly virtualScrollViewport$: BehaviorSubject<CdkVirtualScrollViewport | null> =\n new BehaviorSubject<CdkVirtualScrollViewport | null>(null);\n\n private readonly currentIndex$: Observable<number> = this.virtualScrollViewport$.pipe(\n filterNotNil(),\n switchMap((viewport: CdkVirtualScrollViewport) => viewport.scrolledIndexChange),\n shareReplayWithRefCount()\n );\n\n private readonly virtualScrollInitialized$: Observable<boolean> = this.currentIndex$.pipe(\n map(() => true),\n startWith(false),\n shareReplayWithRefCount()\n );\n\n public readonly currentCalendarMonth$: Observable<CalendarMonth> = this.currentIndex$.pipe(\n map((currentIndex: number) => {\n const monthIndex: MonthIndex = currentIndex % MONTHS_IN_YEAR;\n const yearsCount: number = (currentIndex - monthIndex) / MONTHS_IN_YEAR;\n\n return {\n year: yearsCount + this.calendarConfigService.startYear,\n month: monthIndex,\n };\n })\n );\n\n private get initialCalendarMonth(): CalendarMonth {\n const currentDate: Date = new Date();\n\n return {\n month: currentDate.getMonth(),\n year: currentDate.getFullYear(),\n };\n }\n\n private get initialMonthIndex(): number {\n const initialCalendarMonth: CalendarMonth = this.initialCalendarMonth;\n\n return (\n (initialCalendarMonth.year - this.calendarConfigService.startYear) * MONTHS_IN_YEAR +\n Number(initialCalendarMonth.month)\n );\n }\n\n constructor(private readonly calendarConfigService: CalendarConfigService) {}\n\n public setVirtualScrollViewport(viewport: CdkVirtualScrollViewport): void {\n if (isNil(viewport)) {\n throw new Error('[CalendarStateService] viewport can not be null or undefined');\n }\n\n this.virtualScrollViewport$.next(viewport);\n }\n\n public clearVirtualScrollViewport(): void {\n this.virtualScrollViewport$.next(null);\n }\n\n public resetScroll(behavior: ScrollBehavior): void {\n this.scrollToMonthByIndex(this.initialMonthIndex, behavior);\n }\n\n public scrollToYear(year: number, behavior: ScrollBehavior): void {\n const yearFirstMonthIndex: number = (year - this.calendarConfigService.startYear) * MONTHS_IN_YEAR;\n\n this.scrollToMonthByIndex(yearFirstMonthIndex, behavior);\n }\n\n public scrollToMonth(calendarMonth: CalendarMonth, behavior: ScrollBehavior): void {\n const monthIndex: number =\n (calendarMonth.year - this.calendarConfigService.startYear) * MONTHS_IN_YEAR + Number(calendarMonth.month);\n\n this.scrollToMonthByIndex(monthIndex, behavior);\n }\n\n public scrollToNextMonth(behavior: ScrollBehavior): void {\n this.currentIndex$.pipe(take(1)).subscribe((currentIndex: number) => {\n this.scrollToMonthByIndex(currentIndex + 1, behavior);\n });\n }\n\n public scrollToPreviousMonth(behavior: ScrollBehavior): void {\n this.currentIndex$.pipe(take(1)).subscribe((currentIndex: number) => {\n this.scrollToMonthByIndex(currentIndex - 1, behavior);\n });\n }\n\n public scrollToMonthByIndex(index: number, behavior: ScrollBehavior): void {\n this.virtualScrollInitialized$\n .pipe(\n filterTruthy(),\n switchMap(() => this.virtualScrollViewport$),\n take(1)\n )\n .subscribe((viewport: CdkVirtualScrollViewport) =>\n requestAnimationFrame(() => viewport.scrollToIndex(index, behavior))\n );\n }\n}\n","import { Injectable } from '@angular/core';\nimport { LocaleService } from '@bimeister/pupakit.common';\nimport { filterNotEmpty } from '@bimeister/utilities';\nimport { Observable } from 'rxjs';\nimport { map, startWith } from 'rxjs/operators';\nimport { CalendarTranslation } from '../declarations/interfaces/calendar-translation.interface';\nimport { CalendarConfigService } from './calendar-config.service';\n\nconst DEFAULT_LOCALE: string = 'en';\n\n@Injectable()\nexport class CalendarTranslationService {\n private readonly locale$: Observable<string> = this.localeService.locale$.pipe(startWith(DEFAULT_LOCALE));\n\n public readonly translation$: Observable<CalendarTranslation> = this.locale$.pipe(\n map((locale: string) => this.getTranslationByLocale(locale)),\n filterNotEmpty()\n );\n\n constructor(\n private readonly calendarConfigService: CalendarConfigService,\n private readonly localeService: LocaleService<string>\n ) {}\n\n private getTranslationByLocale(locale: string): CalendarTranslation | null {\n return this.calendarConfigService.translations?.[locale] ?? null;\n }\n}\n","export function getClearDate(): Date {\n const date: Date = new Date(null);\n date.setHours(0);\n date.setMinutes(0);\n date.setSeconds(0);\n date.setMilliseconds(0);\n\n return date;\n}\n","import { SortDirection } from '@bimeister/utilities';\n\nexport function getSortedDates(dates: Date[], sortDirection: SortDirection): Date[] {\n const compareFn = (first: number, second: number): number =>\n sortDirection === 'ascending' ? first - second : second - first;\n\n return [...dates].sort((first: Date, second: Date) => compareFn(first.getTime(), second.getTime()));\n}\n","import { Injectable } from '@angular/core';\nimport { BehaviorSubject } from 'rxjs';\nimport { take } from 'rxjs/operators';\nimport { CalendarQuickSelectMode } from '../declarations/enums/calendar-quick-select-mode.enum';\nimport { getClearDate } from '../declarations/functions/get-clear-date.function';\nimport { getSortedDates } from '../declarations/functions/get-sorted-dates.function';\n\nconst MULTI_SELECTION_DATES_COUNT: number = 2;\n\nfunction getDateWithClearedTime(date: Date): Date {\n const clearDate: Date = getClearDate();\n\n clearDate.setFullYear(date.getFullYear());\n clearDate.setMonth(date.getMonth());\n clearDate.setDate(date.getDate());\n\n return clearDate;\n}\n\nfunction findDateIndexInArray(array: Date[], date: Date): number {\n return array.findIndex((item: Date) => item.getTime() === date.getTime());\n}\n\nfunction getUniqDates(dates: Date[]): Date[] {\n return dates.filter((date: Date, index: number, array: Date[]) => {\n const foundIndex: number = findDateIndexInArray(array, date);\n\n return foundIndex === index;\n });\n}\n\nfunction getProcessedDates(dates: Date[]): Date[] {\n return getSortedDates(getUniqDates(dates.map(getDateWithClearedTime)), 'ascending');\n}\n\n@Injectable()\nexport class CalendarStateService {\n private isRange: boolean = false;\n\n public readonly selectedDates$: BehaviorSubject<Date[]> = new BehaviorSubject<Date[]>([]);\n\n public readonly quickSelectMode$: BehaviorSubject<CalendarQuickSelectMode | null> =\n new BehaviorSubject<CalendarQuickSelectMode>(null);\n\n public setSelectedDates(dates: Date[]): void {\n this.selectedDates$.next(getProcessedDates(dates));\n }\n\n public addSelectedDate(newDate: Date): void {\n const preparedDate: Date = getDateWithClearedTime(newDate);\n\n this.selectedDates$.pipe(take(1)).subscribe((selectedDates: Date[]) => {\n const updatedDates: Date[] = this.isRange\n ? this.patchDatesForRangeSelection(selectedDates, preparedDate)\n : [preparedDate];\n\n this.selectedDates$.next(updatedDates);\n });\n }\n\n public patchDatesForRangeSelection(dates: Date[], newDate: Date): Date[] {\n const hasAlreadyAdded: boolean = findDateIndexInArray(dates, newDate) >= 0;\n const isResetNeeded: boolean = dates.length >= MULTI_SELECTION_DATES_COUNT;\n\n if (hasAlreadyAdded || isResetNeeded) {\n return [newDate];\n }\n\n return getProcessedDates([...dates, newDate]);\n }\n\n public setQuickSelectMode(mode: CalendarQuickSelectMode): void {\n this.quickSelectMode$.next(mode);\n }\n\n public resetQuickSelectMode(): void {\n this.quickSelectMode$.next(null);\n }\n\n public setIsRange(isRange: boolean): void {\n this.isRange = isRange;\n }\n}\n","import { ChangeDetectionStrategy, Component, ViewEncapsulation } from '@angular/core';\nimport { Observable, combineLatest } from 'rxjs';\nimport { map } from 'rxjs/operators';\nimport { CalendarQuickSelectMode } from '../../declarations/enums/calendar-quick-select-mode.enum';\nimport { MonthIndex } from '../../declarations/enums/month-index.enum';\nimport { CalendarMonth } from '../../declarations/interfaces/calendar-month.interface';\nimport { CalendarTranslation } from '../../declarations/interfaces/calendar-translation.interface';\nimport { CalendarManipulatorService } from '../../services/calendar-manipulator.service';\nimport { CalendarStateService } from '../../services/calendar-state.service';\nimport { CalendarTranslationService } from '../../services/calendar-translation.service';\n\ninterface DisplayedMonth {\n year: number;\n month: string;\n}\n\n@Component({\n selector: 'pupa-calendar-control-panel',\n templateUrl: './calendar-control-panel.component.html',\n styleUrls: ['./calendar-control-panel.component.scss'],\n encapsulation: ViewEncapsulation.Emulated,\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class CalendarControlPanelComponent {\n private readonly monthTranslationByIndex$: Observable<Record<MonthIndex, string>> =\n this.calendarTranslationService.translation$.pipe(map((translation: CalendarTranslation) => translation.months));\n\n public readonly displayedMonth$: Observable<DisplayedMonth> = combineLatest([\n this.calendarManipulatorService.currentCalendarMonth$,\n this.monthTranslationByIndex$,\n ]).pipe(\n map(([calendarMonth, monthTranslationByIndex]: [CalendarMonth, Record<MonthIndex, string>]) => ({\n year: calendarMonth.year,\n month: monthTranslationByIndex[calendarMonth.month],\n }))\n );\n\n constructor(\n private readonly calendarManipulatorService: CalendarManipulatorService,\n private readonly calendarTranslationService: CalendarTranslationService,\n private readonly calendarStateService: CalendarStateService\n ) {}\n\n public resetScroll(): void {\n this.calendarManipulatorService.resetScroll('smooth');\n }\n\n public scrollToNextMonth(): void {\n this.calendarManipulatorService.scrollToNextMonth('smooth');\n }\n\n public scrollToPreviousMonth(): void {\n this.calendarManipulatorService.scrollToPreviousMonth('smooth');\n }\n\n public setQuickSelectModeMonth(): void {\n this.calendarStateService.setQuickSelectMode(CalendarQuickSelectMode.Month);\n }\n\n public setQuickSelectModeYear(): void {\n this.calendarStateService.setQuickSelectMode(CalendarQuickSelectMode.Year);\n }\n}\n","<div class=\"selection\" *ngIf=\"displayedMonth$ | async as displayedMonth\">\n <pupa-button (click)=\"setQuickSelectModeMonth()\" kind=\"secondary\">\n {{ displayedMonth.month }}\n </pupa-button>\n\n <pupa-button (click)=\"setQuickSelectModeYear()\" kind=\"secondary\">\n {{ displayedMonth.year }}\n </pupa-button>\n</div>\n\n<div class=\"navigation\">\n <pupa-button-icon size=\"m\" kind=\"secondary\" icon=\"app-chevron-down\" (click)=\"scrollToNextMonth()\"></pupa-button-icon>\n\n <pupa-button-icon size=\"m\" kind=\"secondary\" icon=\"ios-radio-button-off\" (click)=\"resetScroll()\"></pupa-button-icon>\n\n <pupa-button-icon\n size=\"m\"\n kind=\"secondary\"\n icon=\"app-chevron-up\"\n (click)=\"scrollToPreviousMonth()\"\n ></pupa-button-icon>\n</div>\n","import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, ViewEncapsulation } from '@angular/core';\n\n@Component({\n selector: 'pupa-calendar-day',\n templateUrl: './calendar-day.component.html',\n styleUrls: ['./calendar-day.component.scss'],\n encapsulation: ViewEncapsulation.Emulated,\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class CalendarDayComponent {\n @Input()\n public value: number | null = null;\n\n @Input()\n public isDisabled: boolean = false;\n\n @Input()\n public isEmpty: boolean = false;\n\n @Input()\n public isCurrent: boolean = false;\n\n @Input()\n public isSelected: boolean = false;\n\n @Output()\n public readonly select: EventEmitter<void> = new EventEmitter<void>();\n\n public handleClick(): void {\n this.select.emit();\n }\n}\n","<div\n class=\"day\"\n [class.day_disabled]=\"isDisabled\"\n [class.day_empty]=\"isEmpty\"\n [class.day_current]=\"isCurrent\"\n [class.day_selected]=\"isSelected\"\n (click)=\"handleClick()\"\n [tabindex]=\"isEmpty ? -1 : 0\"\n>\n {{ value }}\n</div>\n","import { ChangeDetectionStrategy, Component, ViewEncapsulation } from '@angular/core';\n\n@Component({\n selector: 'pupa-calendar-header',\n templateUrl: './calendar-header.component.html',\n styleUrls: ['./calendar-header.component.scss'],\n encapsulation: ViewEncapsulation.Emulated,\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class CalendarHeaderComponent {}\n","<ng-content></ng-content>\n","import { ChangeDetectionStrategy, Component, ViewEncapsulation } from '@angular/core';\n\n@Component({\n selector: 'pupa-calendar-label',\n templateUrl: './calendar-label.component.html',\n styleUrls: ['./calendar-label.component.scss'],\n encapsulation: ViewEncapsulation.Emulated,\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class CalendarLabelComponent {}\n","<ng-content></ng-content>\n","import { ChangeDetectionStrategy, Component, Input, ViewEncapsulation } from '@angular/core';\n\n@Component({\n selector: 'pupa-calendar-selector-button',\n templateUrl: './calendar-selector-button.component.html',\n styleUrls: ['./calendar-selector-button.component.scss'],\n encapsulation: ViewEncapsulation.Emulated,\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class CalendarSelectorButtonComponent {\n @Input() public isCurrent: boolean = false;\n}\n","<button class=\"button\" [class.button_current]=\"isCurrent\">\n <ng-content></ng-content>\n</button>\n","import { CalendarMonth } from '../interfaces/calendar-month.interface';\n\nexport function getCurrentCalendarMonth(): CalendarMonth {\n const date: Date = new Date();\n\n return {\n year: date.getFullYear(),\n month: date.getMonth(),\n };\n}\n","import { Pipe, PipeTransform } from '@angular/core';\nimport { getCurrentCalendarMonth } from '../declarations/functions/get-current-calendar-month.function';\nimport { CalendarMonth } from '../declarations/interfaces/calendar-month.interface';\n\nconst currentCalendarMonth: CalendarMonth = getCurrentCalendarMonth();\n\n@Pipe({\n name: 'isCurrentCalendarMonth',\n pure: true,\n})\nexport class IsCurrentCalendarMonthPipe implements PipeTransform {\n public transform({ year, month }: CalendarMonth): boolean {\n return month === currentCalendarMonth.month && year === currentCalendarMonth.year;\n }\n}\n","import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';\nimport {\n AfterViewInit,\n ChangeDetectionStrategy,\n Component,\n EventEmitter,\n Output,\n ViewChild,\n ViewEncapsulation,\n} from '@angular/core';\nimport { isNil } from '@bimeister/utilities';\nimport { Observable, Subject } from 'rxjs';\nimport { map, switchMap } from 'rxjs/operators';\nimport { MONTHS_IN_YEAR } from '../../declarations/constants/months-in-year.const';\nimport { CalendarTextKey } from '../../declarations/enums/calendar-text-key.enum';\nimport { MonthIndex } from '../../declarations/enums/month-index.enum';\nimport { CalendarMonth } from '../../declarations/interfaces/calendar-month.interface';\nimport { CalendarTranslation } from '../../declarations/interfaces/calendar-translation.interface';\nimport { CalendarConfigService } from '../../services/calendar-config.service';\nimport { CalendarTranslationService } from '../../services/calendar-translation.service';\n\nconst DIVIDER_HEIGHT_PX: number = 12;\nconst YEAR_LABEL_HEIGHT_PX: number = 16;\nconst YEAR_TABLE_HEIGHT_PX: number = 188;\n\nconst ITEM_HEIGHT_PX: number = DIVIDER_HEIGHT_PX * 2 + YEAR_LABEL_HEIGHT_PX + YEAR_TABLE_HEIGHT_PX;\n\n@Component({\n selector: 'pupa-calendar-month-selector',\n templateUrl: './calendar-month-selector.component.html',\n styleUrls: ['./calendar-month-selector.component.scss'],\n encapsulation: ViewEncapsulation.Emulated,\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class CalendarMonthSelectorComponent implements AfterViewInit {\n @Output() public select: EventEmitter<CalendarMonth> = new EventEmitter<CalendarMonth>();\n\n @ViewChild(CdkVirtualScrollViewport)\n private readonly virtualScrollViewport: CdkVirtualScrollViewport;\n\n private readonly virtualScrollViewport$: Subject<CdkVirtualScrollViewport> = new Subject<CdkVirtualScrollViewport>();\n\n public readonly headerTitle$: Observable<string> = this.calendarTranslationService.translation$.pipe(\n map((translation: CalendarTranslation) => translation.texts[CalendarTextKey.SelectMonth])\n );\n\n public readonly itemHeight: number = ITEM_HEIGHT_PX;\n\n public readonly startYear: number = this.calendarConfigService.startYear;\n\n public readonly currentYearInScroll$: Observable<number> = this.virtualScrollViewport$.pipe(\n switchMap((viewport: CdkVirtualScrollViewport) => viewport.scrolledIndexChange),\n map((index: number) => index + this.startYear)\n );\n\n public readonly yearsIndexes: unknown[] = Array.from({\n length: this.calendarConfigService.yearsRange,\n });\n public readonly monthsIndexes: unknown[] = Array.from({ length: MONTHS_IN_YEAR });\n\n public readonly monthNameByIndex$: Observable<Record<MonthIndex, string>> =\n this.calendarTranslationService.translation$.pipe(map((translation: CalendarTranslation) => translation.months));\n\n constructor(\n private readonly calendarTranslationService: CalendarTranslationService,\n private readonly calendarConfigService: CalendarConfigService\n ) {}\n\n public ngAfterViewInit(): void {\n this.scrollToCurrentYear();\n\n if (!isNil(this.virtualScrollViewport)) {\n this.virtualScrollViewport$.next(this.virtualScrollViewport);\n }\n }\n\n public selectMonth(year: number, month: number): void {\n this.select.emit({\n year,\n month,\n });\n }\n\n private scrollToCurrentYear(): void {\n if (isNil(this.virtualScrollViewport)) {\n return;\n }\n\n const currentYearIndex: number = new Date().getFullYear() - this.startYear;\n\n requestAnimationFrame(() => {\n this.virtualScrollViewport.scrollToIndex(currentYearIndex);\n });\n }\n}\n","<pupa-calendar-header>\n <div class=\"header-content\">\n <h4 class=\"header-title\">\n {{ headerTitle$ | async }}\n </h4>\n\n <div class=\"year-label\">{{ currentYearInScroll$ | async }}</div>\n </div>\n</pupa-calendar-header>\n\n<div class=\"scroller-wrapper\">\n <cdk-virtual-scroll-viewport\n *pupaLet=\"monthNameByIndex$ | async as monthNameByIndex\"\n pupaScrollableContent\n class=\"scroller\"\n [itemSize]=\"itemHeight\"\n >\n <div\n class=\"scroller__item\"\n *cdkVirtualFor=\"let _ of yearsIndexes; templateCacheSize: 10; let isLast = last; let yearIndex = index\"\n >\n <div class=\"scroller__divider\"></div>\n\n <div class=\"year-table\">\n <div class=\"year-table__month\" *ngFor=\"let __ of monthsIndexes; let month = index\">\n <pupa-calendar-selector-button\n [isCurrent]=\"{ year: startYear + yearIndex, month } | isCurrentCalendarMonth\"\n (click)=\"selectMonth(startYear + yearIndex, month)\"\n >\n {{ monthNameByIndex[month] }}\n </pupa-calendar-selector-button>\n </div>\n </div>\n\n <ng-container *ngIf=\"!isLast\">\n <div class=\"scroller__divider\"></div>\n <div class=\"year-label\">{{ yearIndex + startYear + 1 }}</div>\n </ng-container>\n </div>\n </cdk-virtual-scroll-viewport>\n</div>\n","import { CalendarDay } from '../interfaces/calendar-day.interface';\n\nexport function getDateFromCalendarDay({ year, month, day }: CalendarDay): Date {\n return new Date(year, month, day);\n}\n","import { ChangeDetectionStrategy, Component, ViewEncapsulation } from '@angular/core';\n\n@Component({\n selector: 'pupa-calendar-week',\n templateUrl: './calendar-week.component.html',\n styleUrls: ['./calendar-week.component.scss'],\n encapsulation: ViewEncapsulation.Emulated,\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class CalendarWeekComponent {}\n","<ng-content></ng-content>\n","export const DAYS_IN_WEEK: number = 7;\n","import { CalendarDay } from '../interfaces/calendar-day.interface';\n\nexport function getCurrentCalendarDay(): CalendarDay {\n const date: Date = new Date();\n\n return {\n year: date.getFullYear(),\n month: date.getMonth(),\n day: date.getDate(),\n };\n}\n","import { MonthIndex } from '../enums/month-index.enum';\n\nexport function getDaysCountInMonth(monthIndex: MonthIndex | number, isLeapYear: boolean): number {\n switch (monthIndex) {\n case MonthIndex.February:\n return isLeapYear ? 29 : 28;\n case MonthIndex.April:\n case MonthIndex.June:\n case MonthIndex.September:\n case MonthIndex.November:\n return 30;\n default:\n return 31;\n }\n}\n","// every 400 year cycle starts on the same day of week\nexport const BIG_CALENDAR_CYCLE_SIZE_IN_YEARS: number = 400;\n","import { DayOfWeek } from '../enums/day-of-week.enum';\n\n// The sequence of weekdays on 1 January in SMALL_CALENDAR_CYCLE\nexport const SMALL_CALENDAR_CYCLE_WEEKDAYS_SEQUENCE: DayOfWeek[] = [\n DayOfWeek.Monday,\n DayOfWeek.Tuesday,\n DayOfWeek.Wednesday,\n DayOfWeek.Thursday,\n DayOfWeek.Saturday,\n DayOfWeek.Sunday,\n DayOfWeek.Monday,\n DayOfWeek.Tuesday,\n DayOfWeek.Thursday,\n DayOfWeek.Friday,\n DayOfWeek.Saturday,\n DayOfWeek.Sunday,\n DayOfWeek.Tuesday,\n DayOfWeek.Wednesday,\n DayOfWeek.Thursday,\n DayOfWeek.Friday,\n DayOfWeek.Sunday,\n DayOfWeek.Monday,\n DayOfWeek.Tuesday,\n DayOfWeek.Wednesday,\n DayOfWeek.Friday,\n DayOfWeek.Saturday,\n DayOfWeek.Sunday,\n DayOfWeek.Monday,\n DayOfWeek.Wednesday,\n DayOfWeek.Thursday,\n DayOfWeek.Friday,\n DayOfWeek.Saturday,\n];\n","/** @note\n * if number of year is a multiple of 400 => year is leap\n * if number of year is a multiple of 100 => year is not leap\n * if other number of year is a multiple of 4 => year is leap\n * other years are not leap\n * */\nexport function isLeapYear(year: number): boolean {\n if (year % 400 === 0) {\n return true;\n }\n\n if (year % 100 === 0) {\n return false;\n }\n\n return year % 4 === 0;\n}\n","import { BIG_CALENDAR_CYCLE_SIZE_IN_YEARS } from '../constants/big-calendar-cycle-size-in-years.const';\nimport { DAYS_IN_WEEK } from '../constants/days-in-week.const';\nimport { SMALL_CALENDAR_CYCLE_SIZE_IN_YEARS } from '../constants/small-calendar-cycle-size-in-years.const';\nimport { SMALL_CALENDAR_CYCLE_WEEKDAYS_SEQUENCE } from '../constants/small-calendar-cycle-weekdays-sequence.const';\nimport { DayOfWeek } from '../enums/day-of-week.enum';\nimport { MonthIndex } from '../enums/month-index.enum';\nimport { isLeapYear } from './is-leap-year.function';\n\n// offset days of the week on the first day of each month for the NORMAL year\nconst NORMAL_YEAR_OFFSETS_IN_SMALL_CYCLE: number[] = [0, 3, 3, 6, 1, 4, 6, 2, 5, 0, 3, 5];\n\nexport function getWeekdayIndex(yearNumber: number, monthIndex: MonthIndex, dayNumber: number): DayOfWeek {\n const daysOffset: number = getDaysOffset(yearNumber, monthIndex);\n\n const yearIndexInSmallCycle: number = getYearIndexInSmallCycle(yearNumber);\n\n const dayIndex: number = dayNumber - 1;\n const weekdayIndexInSmallCycle: DayOfWeek = SMALL_CALENDAR_CYCLE_WEEKDAYS_SEQUENCE[yearIndexInSmallCycle];\n\n const weekdayIndex: DayOfWeek = (weekdayIndexInSmallCycle + daysOffset + dayIndex) % DAYS_IN_WEEK;\n\n return weekdayIndex;\n}\n\nfunction getYearIndexInSmallCycle(yearNumber: number): number {\n const yearIndex: number = yearNumber - 1;\n\n const yearIndexInBigCycle: number = yearIndex % BIG_CALENDAR_CYCLE_SIZE_IN_YEARS;\n\n const centuryInBigCycle: number = Math.floor(yearIndexInBigCycle / 100);\n\n const yearIndexRestoredWithBigCycle: number = 4 * centuryInBigCycle + (yearIndexInBigCycle % 100);\n\n const yearIndexInSmallCycle: number = yearIndexRestoredWithBigCycle % SMALL_CALENDAR_CYCLE_SIZE_IN_YEARS;\n\n return yearIndexInSmallCycle;\n}\n\nfunction getDaysOffset(yearNumber: number, monthIndex: MonthIndex): number {\n const offset: number = NORMAL_YEAR_OFFSETS_IN_SMALL_CYCLE[monthIndex];\n\n const isLeapYearAndFebruaryEnded: boolean = isLeapYear(yearNumber) && monthIndex > MonthIndex.February;\n\n // increase offset, because february in leap year has 29 days\n return isLeapYearAndFebruaryEnded ? offset + 1 : offset;\n}\n","import { DAYS_IN_WEEK } from '../constants/days-in-week.const';\nimport { DayOfWeek } from '../enums/day-of-week.enum';\n\nexport function getWeekdayRelativeToNewWeekStart(\n weekday: DayOfWeek,\n oldWeekStart: DayOfWeek,\n newWeekStart: DayOfWeek\n): DayOfWeek {\n const delta: number = oldWeekStart - newWeekStart;\n\n const newWeekdayPosition: number = weekday + delta;\n\n if (newWeekdayPosition >= DAYS_IN_WEEK) {\n return newWeekdayPosition % DAYS_IN_WEEK;\n }\n\n if (newWeekdayPosition < 0) {\n return newWeekdayPosition + DAYS_IN_WEEK;\n }\n\n return newWeekdayPosition;\n}\n","import { CalendarDay } from '../interfaces/calendar-day.interface';\n\nexport function isCalendarDaysEqual(first: CalendarDay, second: CalendarDay): boolean {\n return first.year === second.year && first.month === second.month && first.day === second.day;\n}\n","import { Pipe, PipeTransform } from '@angular/core';\nimport { isEmpty } from '@bimeister/utilities';\nimport { DAYS_IN_WEEK } from '../declarations/constants/days-in-week.const';\nimport { DayOfWeek } from '../declarations/enums/day-of-week.enum';\nimport { getCurrentCalendarDay } from '../declarations/functions/get-current-calendar-day.function';\nimport { getDaysCountInMonth } from '../declarations/functions/get-days-count-in-month.function';\nimport { getWeekdayIndex } from '../declarations/functions/get-weekday-index.function';\nimport { getWeekdayRelativeToNewWeekStart } from '../declarations/functions/get-weekday-relative-to-new-week-start.function';\nimport { isCalendarDaysEqual } from '../declarations/functions/is-calendar-days-equal.function';\nimport { isLeapYear } from '../declarations/functions/is-leap-year.function';\nimport { CalendarDay } from '../declarations/interfaces/calendar-day.interface';\nimport { CalendarMonth } from '../declarations/interfaces/calendar-month.interface';\nimport { MonthDayCell } from '../declarations/interfaces/month-day-cell.interface';\nimport { CalendarConfigService } from '../services/calendar-config.service';\n\nconst currentCalendarDay: CalendarDay = getCurrentCalendarDay();\n\nfunction getMonthCells(daysCount: number, startDay: DayOfWeek, calendarMonth: CalendarMonth): MonthDayCell[][] {\n const weeksCount: number = Math.ceil((daysCount + Number(startDay)) / DAYS_IN_WEEK);\n\n return Array.from({ length: weeksCount }, (_: unknown, weekIndex: number) =>\n Array.from({ length: DAYS_IN_WEEK }, (__: unknown, weekdayIndex: number) => {\n const dayIndex: number = weekIndex * DAYS_IN_WEEK + weekdayIndex;\n const dayNumber: number = dayIndex + 1;\n const day: number = dayNumber - startDay;\n\n const isValidDay: boolean = day > 0 && dayNumber <= startDay + daysCount;\n\n if (!isValidDay) {\n return {\n isEmpty: true,\n index: dayIndex,\n isCurrent: false,\n };\n }\n\n const calendarDay: CalendarDay = { ...calendarMonth, day };\n const isCurrent: boolean = isCalendarDaysEqual(calendarDay, currentCalendarDay);\n\n return {\n isEmpty: false,\n index: dayIndex,\n isCurrent,\n calendarDay,\n };\n })\n );\n}\n\n@Pipe({\n name: 'monthDayCells',\n pure: true,\n})\nexport class MonthDayCellsPipe implements PipeTransform {\n private readonly startWeekday: DayOfWeek;\n\n constructor(calendarConfigService: CalendarConfigService) {\n this.startWeekday = calendarConfigService.startWeekday;\n }\n\n public transform(calendarMonth: CalendarMonth): MonthDayCell[][] {\n if (isEmpty(calendarMonth)) {\n return [];\n }\n\n const { year, month }: CalendarMonth = calendarMonth;\n\n const defaultMonthStart: DayOfWeek = getWeekdayIndex(year, month, 1);\n\n const relativeMonthStart: DayOfWeek = getWeekdayRelativeToNewWeekStart(\n defaultMonthStart,\n DayOfWeek.Sunday,\n this.startWeekday\n );\n\n const daysCount: number = getDaysCountInMonth(month, isLeapYear(year));\n\n return getMonthCells(daysCount, relativeMonthStart, calendarMonth);\n }\n}\n","import { CalendarDay } from '../interfaces/calendar-day.interface';\n\nexport function getCalendarDayFromDate(date: Date): CalendarDay {\n return {\n year: date.getFullYear(),\n month: date.getMonth(),\n day: date.getDate(),\n };\n}\n","import { Pipe, PipeTransform } from '@angular/core';\nimport { isNil } from '@bimeister/utilities';\nimport { getCalendarDayFromDate } from '../declarations/functions/get-calendar-day-from-date.function';\nimport { CalendarDay } from '../declarations/interfaces/calendar-day.interface';\n\n@Pipe({\n name: 'isDaySelected',\n pure: true,\n})\nexport class IsDaySelectedPipe implements PipeTransform {\n public transform({ day, month, year }: CalendarDay, selectedDates: Date[]): boolean {\n const foundDate: Date | undefined = selectedDates.find((selectedDate: Date) => {\n const selectedDay: CalendarDay = getCalendarDayFromDate(selectedDate);\n\n return selectedDay.day === day && selectedDay.month === month && selectedDay.year === year;\n });\n\n return !isNil(foundDate);\n }\n}\n","import { CalendarDay } from '../interfaces/calendar-day.interface';\nimport { DayPositionInDateRange } from '../types/day-position-in-date-range.type';\nimport { getDateFromCalendarDay } from './get-date-from-calendar-day.function';\n\nexport function getDayPositionInDateRange(\n calendarDay: CalendarDay,\n selectedDates: Date[]\n): DayPositionInDateRange | null {\n if (selectedDates.length !== 2) {\n return null;\n }\n\n const inputTime: number = getDateFromCalendarDay(calendarDay).getTime();\n\n const [rangeBegin, rangeEnd]: number[] = selectedDates.map((item: Date) => item.getTime());\n\n if (inputTime === rangeBegin) {\n return 'first';\n }\n\n if (inputTime === rangeEnd) {\n return 'last';\n }\n\n const isInRange: boolean = inputTime > rangeBegin && inputTime < rangeEnd;\n\n return isInRange ? 'inside' : null;\n}\n","import { Pipe, PipeTransform } from '@angular/core';\nimport { getDayPositionInDateRange } from '../declarations/functions/get-day-position-in-date-range.function';\nimport { CalendarDay } from '../declarations/interfaces/calendar-day.interface';\nimport { DayPositionInDateRange } from '../declarations/types/day-position-in-date-range.type';\n\n@Pipe({\n name: 'dayPositionInDateRange',\n pure: true,\n})\nexport class DayPositionInDateRangePipe implements PipeTransform {\n public transform(calendarDay: CalendarDay, selectedDates: Date[]): DayPositionInDateRange | null {\n return getDayPositionInDateRange(calendarDay, selectedDates);\n }\n}\n","import { Pipe, PipeTransform } from '@angular/core';\nimport { DayOfWeek } from '../declarations/enums/day-of-week.enum';\nimport { getDayPositionInDateRange } from '../declarations/functions/get-day-position-in-date-range.function';\nimport { getDaysCountInMonth } from '../declarations/functions/get-days-count-in-month.function';\nimport { getWeekdayIndex } from '../declarations/functions/get-weekday-index.function';\nimport { getWeekdayRelativeToNewWeekStart } from '../declarations/functions/get-weekday-relative-to-new-week-start.function';\nimport { isLeapYear } from '../declarations/functions/is-leap-year.function';\nimport { CalendarDay } from '../declarations/interfaces/calendar-day.interface';\nimport { CalendarMonth } from '../declarations/interfaces/calendar-month.interface';\nimport { DayPositionInDateRange } from '../declarations/types/day-position-in-date-range.type';\nimport { CalendarConfigService } from '../services/calendar-config.service';\n\n@Pipe({\n name: 'isEmptyDayInDateRange',\n pure: true,\n})\nexport class IsEmptyDayInDateRangePipe implements PipeTransform {\n private readonly startWeekday: DayOfWeek;\n\n constructor(calendarConfigService: CalendarConfigService) {\n this.startWeekday = calendarConfigService.startWeekday;\n }\n\n public transform(dayIndex: number, calendarMonth: CalendarMonth, selectedDates: Date[]): boolean {\n const { year, month }: CalendarMonth = calendarMonth;\n\n if (selectedDates.length !== 2) {\n return false;\n }\n\n const defaultMonthStart: DayOfWeek = getWeekdayIndex(year, month, 1);\n const relativeMonthStart: DayOfWeek = getWeekdayRelativeToNewWeekStart(\n defaultMonthStart,\n DayOfWeek.Sunday,\n this.startWeekday\n );\n\n const isEmptyAtBegin: boolean = dayIndex < relativeMonthStart;\n const firstMonthDay: CalendarDay = { year, month, day: 1 };\n const firstMonthDayPosition: DayPositionInDateRange = getDayPositionInDateRange(firstMonthDay, selectedDates);\n const firstMonthDayIsNotFirst: boolean = ['last', 'inside'].includes(firstMonthDayPosition);\n\n if (isEmptyAtBegin && firstMonthDayIsNotFirst) {\n return true;\n }\n\n const daysCount: number = getDaysCountInMonth(month, isLeapYear(year));\n const isEmptyAtEnd: boolean = dayIndex + 1 >= relativeMonthStart + daysCount;\n const lastMonthDay: CalendarDay = { year, month, day: daysCount };\n const lastMonthDayPosition: DayPositionInDateRange = getDayPositionInDateRange(lastMonthDay, selectedDates);\n const lastMonthDayIsNotLast: boolean = ['first', 'inside'].includes(lastMonthDayPosition);\n\n if (isEmptyAtEnd && lastMonthDayIsNotLast) {\n return true;\n }\n\n return false;\n }\n}\n","import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, ViewEncapsulation } from '@angular/core';\nimport { isNil } from '@bimeister/utilities';\nimport { getDateFromCalendarDay } from '../../declarations/functions/get-date-from-calendar-day.function';\nimport { CalendarDay } from '../../declarations/interfaces/calendar-day.interface';\nimport { CalendarMonth } from '../../declarations/interfaces/calendar-month.interface';\n\n@Component({\n selector: 'pupa-calendar-month',\n templateUrl: './calendar-month.component.html',\n styleUrls: ['./calendar-month.component.scss'],\n encapsulation: ViewEncapsulation.Emulated,\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class CalendarMonthComponent {\n @Input() public calendarMonth: CalendarMonth;\n @Input() public isCurrent: boolean = false;\n @Input() public selectedDates: Date[] = [];\n\n @Output() public readonly selectDate: EventEmitter<Date> = new EventEmitter<Date>();\n\n public readonly currentDayNumber: number = new Date().getDate();\n\n public handleSelectDay(calendarDay: CalendarDay | null): void {\n if (isNil(calendarDay)) {\n return;\n }\n\n this.selectDate.emit(getDateFromCalendarDay(calendarDay));\n }\n}\n","<pupa-calendar-week *ngFor=\"let week of calendarMonth | monthDayCells\">\n <ng-container *ngFor=\"let cell of week; let isLast = last\">\n <ng-container\n *ngIf=\"cell.isEmpty\"\n [ngTemplateOutlet]=\"emptyWeekPart\"\n [ngTemplateOutletContext]=\"{ isLast: isLast, cellIndex: cell.index }\"\n >\n </ng-container>\n\n <ng-container\n *ngIf=\"!cell.isEmpty\"\n [ngTemplateOutlet]=\"weekPart\"\n [ngTemplateOutletContext]=\"{ isLast: isLast, cell: cell }\"\n >\n </ng-container>\n </ng-container>\n</pupa-calendar-week>\n\n<ng-template #weekPart let-cell=\"cell\" let-isLast=\"isLast\">\n <ng-container *pupaLet=\"cell.calendarDay | dayPositionInDateRange : selectedDates as positionInRange\">\n <pupa-calendar-cell [positionInRange]=\"positionInRange\">\n <pupa-calendar-day\n (select)=\"handleSelectDay(cell.calendarDay)\"\n [value]=\"cell.calendarDay.day\"\n [isCurrent]=\"isCurrent && cell.isCurrent\"\n [isSelected]=\"cell.calendarDay | isDaySelected : selectedDates\"\n ></pupa-calendar-day>\n </pupa-calendar-cell>\n\n <pupa-calendar-cell-separator\n *ngIf=\"!isLast\"\n [isInRange]=\"positionInRange === 'inside' || positionInRange === 'first'\"\n ></pupa-calendar-cell-separator>\n </ng-container>\n</ng-template>\n\n<ng-template #emptyWeekPart let-cellIndex=\"cellIndex\" let-isLast=\"isLast\">\n <ng-container *pupaLet=\"cellIndex | isEmptyDayInDateRange : calendarMonth : selectedDates as isInRange\">\n <pupa-calendar-cell-empty [isInRange]=\"isInRange\"></pupa-calendar-cell-empty>\n\n <pupa-calendar-cell-separator *ngIf=\"!isLast\" [isInRange]=\"isInRange\"></pupa-calendar-cell-separator>\n </ng-container>\n</ng-template>\n","import { DAYS_IN_WEEK } from '../constants/days-in-week.const';\nimport { DayOfWeek } from '../enums/day-of-week.enum';\nimport { getWeekdayRelativeToNewWeekStart } from './get-weekday-relative-to-new-week-start.function';\n\nexport function getWeeksCountInMonth(yearNumber: number, monthNumber: number, startWeekday: DayOfWeek): number {\n const monthIndex: number = monthNumber - 1;\n\n const startDate: Date = new Date(yearNumber, monthIndex, 1);\n const endDate: Date = new Date(yearNumber, monthIndex + 1, 0);\n\n const startMonthWeekday: DayOfWeek = getWeekdayRelativeToNewWeekStart(\n startDate.getDay(),\n DayOfWeek.Sunday,\n startWeekday\n );\n\n const daysInMonth: