UNPKG

@ipi-soft/ng-components

Version:

Custom Angular Components

1 lines 187 kB
{"version":3,"file":"ipi-soft-ng-components-datepicker.mjs","sources":["../../../../projects/ipi-soft/ng-components/datepicker/src/components/calendar-body/calendar-body.component.ts","../../../../projects/ipi-soft/ng-components/datepicker/src/components/calendar-body/calendar-body.component.html","../../../../projects/ipi-soft/ng-components/datepicker/src/datepicker-service.ts","../../../../projects/ipi-soft/ng-components/datepicker/src/components/year-view/year-view.component.ts","../../../../projects/ipi-soft/ng-components/datepicker/src/components/year-view/year-view.component.html","../../../../projects/ipi-soft/ng-components/datepicker/src/datepicker-selection-strategy.component.ts","../../../../projects/ipi-soft/ng-components/datepicker/src/components/month-view/month-view-calendar.component.ts","../../../../projects/ipi-soft/ng-components/datepicker/src/components/month-view/month-view-calendar.component.html","../../../../projects/ipi-soft/ng-components/datepicker/src/components/multi-year-view/multi-year-view.component.ts","../../../../projects/ipi-soft/ng-components/datepicker/src/components/multi-year-view/multi-year-view.component.html","../../../../projects/ipi-soft/ng-components/datepicker/src/components/calendar/calendar.component.ts","../../../../projects/ipi-soft/ng-components/datepicker/src/components/calendar/calendar.component.html","../../../../projects/ipi-soft/ng-components/datepicker/src/datepicker.component.ts","../../../../projects/ipi-soft/ng-components/datepicker/src/datepicker.component.html","../../../../projects/ipi-soft/ng-components/datepicker/ipi-soft-ng-components-datepicker.ts"],"sourcesContent":["import { Component, ElementRef, SimpleChanges, ChangeDetectionStrategy, EventEmitter, Input, Output, NgZone } from '@angular/core';\n\nimport { Platform, normalizePassiveListenerOptions } from '@angular/cdk/platform';\n\nimport { DateSelectionEvent } from './../../datepicker.component';\n\nexport class CalendarCell<D = any> {\n\n constructor(\n public value: number,\n public displayValue: string,\n public ariaLabel: string,\n public enabled: boolean,\n public compareValue = value,\n public rawValue?: D,\n ) {}\n}\n\nlet calendarBodyId = 1;\n\n@Component({\n selector: '[calendar-body]',\n host: {\n 'class': 'calendar-body',\n },\n templateUrl: 'calendar-body.component.html',\n styleUrl: 'calendar-body.component.css',\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\n\nexport class CalendarBody {\n\n constructor(\n private ngZone: NgZone,\n private platform: Platform,\n private elementRef: ElementRef,\n ) {\n this.ngZone.runOutsideAngular(() => {\n const element = this.elementRef.nativeElement;\n\n element.addEventListener('touchmove', this.touchmoveHandler, this.activeCapturingEventOptions);\n\n element.addEventListener('mouseenter', this.enterHandler, this.passiveCapturingEventOptions);\n element.addEventListener('focus', this.enterHandler, this.passiveCapturingEventOptions);\n element.addEventListener('mouseleave', this.leaveHandler, this.passiveCapturingEventOptions);\n\n element.addEventListener('mousedown', this.mousedownHandler, this.passiveEventOptions);\n element.addEventListener('touchstart', this.mousedownHandler, this.passiveEventOptions);\n\n if (this.platform.isBrowser) {\n window.addEventListener('mouseup', this.mouseupHandler);\n window.addEventListener('touchend', this.touchendHandler);\n }\n });\n }\n\n /** The label for the table. (e.g. \"Jan 2017\"). */\n @Input() label!: string;\n\n /** The cells to display in the table. */\n @Input() rows!: CalendarCell[][];\n\n /** The value in the table that corresponds to today. */\n @Input() todayValue!: number;\n\n /** Start value of the selected date range. */\n @Input() startValue!: number;\n\n /** End value of the selected date range. */\n @Input() endValue!: number;\n\n /** The minimum number of free cells needed to fit the label in the first row. */\n @Input() labelMinRequiredCells!: number;\n\n /** The number of columns in the table. */\n @Input() numCols: number = 7;\n\n /** The cell number of the active cell in the table. */\n @Input() activeCell: number = 0;\n\n /** Whether a range is being selected. */\n @Input() isRange: boolean = false;\n\n /**\n * The aspect ratio (width / height) to use for the cells in the table. This aspect ratio will be\n * maintained even as the table resizes.\n */\n @Input() cellAspectRatio: number = 1;\n\n /** Start of the comparison range. */\n @Input() comparisonStart!: number | null;\n\n /** End of the comparison range. */\n @Input() comparisonEnd!: number | null;\n\n /** Start of the preview range. */\n @Input() previewStart: number | null = null;\n\n /** End of the preview range. */\n @Input() previewEnd: number | null = null;\n\n /** ARIA Accessible name of the StartDate */\n @Input() startDateAccessibleName!: string | null;\n\n /** ARIA Accessible name of the EndDate */\n @Input() endDateAccessibleName!: string | null;\n\n /** Emits when a new value is selected. */\n @Output() selectedValueChange = new EventEmitter<DateSelectionEvent<number>>();\n\n /** Emits when the preview has changed as a result of a user action. */\n @Output() previewChange = new EventEmitter<DateSelectionEvent<CalendarCell | null>>();\n\n @Output() activeDateChange = new EventEmitter<DateSelectionEvent<number>>();\n\n /** Emits the date at the possible start of a drag event. */\n @Output() dragStarted = new EventEmitter<DateSelectionEvent<Date>>();\n\n /** Emits the date at the conclusion of a drag, or null if mouse was not released on a date. */\n @Output() dragEnded = new EventEmitter<DateSelectionEvent<Date | null>>();\n\n /** The number of blank cells to put at the beginning for the first row. */\n public firstRowOffset!: number;\n\n /** Padding for the individual date cells. */\n public cellPadding!: string;\n\n private id = `calendar-body-${calendarBodyId++}`;\n public startDateLabelId = `${this.id}-start-date`;\n public endDateLabelId = `${this.id}-end-date`;\n\n /** Width of an individual cell. */\n public cellWidth!: string;\n\n /**\n * Used to skip the next focus event when rendering the preview range.\n * We need a flag like this, because some browsers fire focus events asynchronously.\n */\n private skipNextFocus!: boolean;\n\n /**\n * Used to focus the active cell after change detection has run.\n */\n private focusActiveCellAfterViewChecked = true;\n\n private didDragSinceMouseDown = false;\n\n private activeCapturingEventOptions = normalizePassiveListenerOptions({\n passive: false,\n capture: true,\n });\n\n private passiveCapturingEventOptions = normalizePassiveListenerOptions({\n passive: true,\n capture: true,\n });\n\n private passiveEventOptions = normalizePassiveListenerOptions({ passive: true });\n\n public ngOnChanges(changes: SimpleChanges): void {\n const columnChanges = changes['numCols'];\n const {rows, numCols} = this;\n\n if (changes['rows'] || columnChanges) {\n this.firstRowOffset = rows && rows.length && rows[0].length ? numCols - rows[0].length : 0;\n }\n\n if (changes['cellAspectRatio'] || columnChanges || !this.cellPadding) {\n this.cellPadding = `${(50 * this.cellAspectRatio) / numCols}%`;\n }\n\n if (columnChanges || !this.cellWidth) {\n this.cellWidth = `${100 / numCols}%`;\n }\n }\n\n public ngAfterViewChecked(): void {\n if (this.focusActiveCellAfterViewChecked) {\n this.focusActiveCell();\n this.focusActiveCellAfterViewChecked = false;\n }\n }\n\n public ngOnDestroy(): void {\n const element = this.elementRef.nativeElement;\n\n element.removeEventListener('touchmove', this.touchmoveHandler, this.activeCapturingEventOptions);\n\n element.removeEventListener('mouseenter', this.enterHandler, this.passiveCapturingEventOptions);\n element.removeEventListener('focus', this.enterHandler, this.passiveCapturingEventOptions);\n element.removeEventListener('mouseleave', this.leaveHandler, this.passiveCapturingEventOptions);\n\n element.removeEventListener('mousedown', this.mousedownHandler, this.passiveEventOptions);\n element.removeEventListener('touchstart', this.mousedownHandler, this.passiveEventOptions);\n\n if (this.platform.isBrowser) {\n window.removeEventListener('mouseup', this.mouseupHandler);\n window.removeEventListener('touchend', this.touchendHandler);\n }\n }\n\n public trackRow = (row: CalendarCell[]) => row;\n\n /** Called when a cell is clicked. */\n public cellClicked(cell: CalendarCell, event: MouseEvent): void {\n // Ignore \"clicks\" that are actually canceled drags (eg the user dragged\n // off and then went back to this cell to undo).\n if (this.didDragSinceMouseDown) {\n return;\n }\n\n if (cell.enabled) {\n this.selectedValueChange.emit({ value: cell.value, event });\n }\n }\n\n public emitActiveDateChange(cell: CalendarCell, event: FocusEvent): void {\n if (cell.enabled) {\n this.activeDateChange.emit({ value: cell.value, event });\n }\n }\n\n /** Returns whether a cell should be marked as selected. */\n public isSelected(value: number): boolean {\n return this.startValue === value || this.endValue === value;\n }\n\n /** Returns whether a cell is active. */\n public isActiveCell(rowIndex: number, colIndex: number): boolean {\n let cellNumber = rowIndex * this.numCols + colIndex;\n\n // Account for the fact that the first row may not have as many cells.\n if (rowIndex) {\n cellNumber -= this.firstRowOffset;\n }\n\n return cellNumber == this.activeCell;\n }\n\n public focusActiveCell(movePreview = true): void {\n const activeCell: HTMLElement | null = this.elementRef.nativeElement.querySelector(\n '.active',\n );\n\n if (activeCell) {\n if (!movePreview) {\n this.skipNextFocus = true;\n }\n\n activeCell.focus();\n }\n }\n\n /** Focuses the active cell after change detection has run and the microtask queue is empty. */\n public scheduleFocusActiveCellAfterViewChecked(): void {\n this.focusActiveCellAfterViewChecked = true;\n }\n\n /** Gets whether a value is the start of the main range. */\n public isRangeStart(value: number): boolean {\n return this.isStart(value, this.startValue, this.endValue);\n }\n\n /** Gets whether a value is the end of the main range. */\n public isRangeEnd(value: number): boolean {\n return this.isEnd(value, this.startValue, this.endValue);\n }\n\n /** Gets whether a value is within the currently-selected range. */\n public isCellInRange(value: number): boolean {\n return this.isInRange(value, this.startValue, this.endValue, this.isRange);\n }\n\n /** Gets whether a value is the start of the preview range. */\n public isPreviewStart(value: number): boolean {\n return this.isStart(value, this.previewStart, this.previewEnd);\n }\n\n /** Gets whether a value is the end of the preview range. */\n public isPreviewEnd(value: number): boolean {\n return this.isEnd(value, this.previewStart, this.previewEnd);\n }\n\n /** Gets whether a value is inside the preview range. */\n public isInPreview(value: number): boolean {\n return this.isInRange(value, this.previewStart, this.previewEnd, this.isRange);\n }\n\n /** Gets ids of aria descriptions for the start and end of a date range. */\n public getDescribedby(value: number): string | null {\n if (!this.isRange) {\n return null;\n }\n\n if (this.startValue === value && this.endValue === value) {\n return `${this.startDateLabelId} ${this.endDateLabelId}`;\n } else if (this.startValue === value) {\n return this.startDateLabelId;\n } else if (this.endValue === value) {\n return this.endDateLabelId;\n }\n\n return null;\n }\n\n /**\n * Event handler for when the user enters an element\n * inside the calendar body (e.g. by hovering in or focus).\n */\n private enterHandler = (event: Event) => {\n if (this.skipNextFocus && event.type === 'focus') {\n this.skipNextFocus = false;\n return;\n }\n\n // We only need to hit the zone when we're selecting a range.\n if (event.target && this.isRange) {\n const cell = this.getCellFromElement(event.target as HTMLElement);\n\n if (!cell?.enabled) {\n return;\n }\n \n if (cell) {\n this.ngZone.run(() => this.previewChange.emit({ value: cell.enabled ? cell : null, event }));\n }\n }\n };\n\n private touchmoveHandler = (event: TouchEvent) => {\n if (!this.isRange) return;\n\n const target = this.getActualTouchTarget(event);\n const cell = target ? this.getCellFromElement(target as HTMLElement) : null;\n\n if (!cell?.enabled) {\n return;\n }\n\n if (target !== event.target) {\n this.didDragSinceMouseDown = true;\n }\n\n // If the initial target of the touch is a date cell, prevent default so\n // that the move is not handled as a scroll.\n if (this.getCellElement(event.target as HTMLElement)) {\n event.preventDefault();\n }\n\n this.ngZone.run(() => this.previewChange.emit({value: cell?.enabled ? cell : null, event}));\n };\n\n /**\n * Event handler for when the user's pointer leaves an element\n * inside the calendar body (e.g. by hovering out or blurring).\n */\n private leaveHandler = (event: Event) => {\n // We only need to hit the zone when we're selecting a range.\n if (this.previewEnd !== null && this.isRange) {\n if (event.type !== 'blur') {\n this.didDragSinceMouseDown = true;\n }\n\n const cell = this.getCellFromElement(event.target as HTMLElement);\n\n if (!cell?.enabled) {\n return;\n }\n\n // Only reset the preview end value when leaving cells. This looks better, because\n // we have a gap between the cells and the rows and we don't want to remove the\n // range just for it to show up again when the user moves a few pixels to the side.\n if (\n event.target &&\n cell &&\n !(\n (event as MouseEvent).relatedTarget &&\n this.getCellFromElement((event as MouseEvent).relatedTarget as HTMLElement)\n )\n ) {\n this.ngZone.run(() => this.previewChange.emit({ value: null, event }));\n }\n }\n };\n\n /**\n * Triggered on mousedown or touchstart on a date cell.\n * Respsonsible for starting a drag sequence.\n */\n private mousedownHandler = (event: Event) => {\n if (!this.isRange) return;\n\n this.didDragSinceMouseDown = false;\n // Begin a drag if a cell within the current range was targeted.\n const cell = event.target && this.getCellFromElement(event.target as HTMLElement);\n\n if (!cell || !this.isCellInRange(cell.compareValue)) {\n return;\n }\n\n this.ngZone.run(() => {\n this.dragStarted.emit({\n value: cell.rawValue,\n event,\n });\n });\n };\n\n /** Triggered on mouseup anywhere. Respsonsible for ending a drag sequence. */\n private mouseupHandler = (event: Event) => {\n if (!this.isRange) return;\n\n const cellElement = this.getCellElement(event.target as HTMLElement);\n if (!cellElement) {\n // Mouseup happened outside of datepicker. Cancel drag.\n this.ngZone.run(() => {\n this.dragEnded.emit({value: null, event});\n });\n return;\n }\n\n // if (cellElement.closest('.calendar-body') !== this.elementRef.nativeElement) {\n // // Mouseup happened inside a different month instance.\n // // Allow it to handle the event.\n // return;\n // }\n\n this.ngZone.run(() => {\n const cell = this.getCellFromElement(cellElement);\n\n this.dragEnded.emit({value: cell?.rawValue ?? null, event});\n });\n };\n\n /** Triggered on touchend anywhere. Respsonsible for ending a drag sequence. */\n private touchendHandler = (event: TouchEvent) => {\n const target = this.getActualTouchTarget(event);\n\n if (target) {\n this.mouseupHandler({ target } as unknown as Event);\n }\n };\n\n /** Finds the CalendarCell that corresponds to a DOM node. */\n private getCellFromElement(element: HTMLElement): CalendarCell | null {\n const cell = this.getCellElement(element);\n\n if (cell) {\n const row = cell.getAttribute('data-row');\n const col = cell.getAttribute('data-col');\n\n if (row && col) {\n return this.rows[parseInt(row)][parseInt(col)];\n }\n }\n\n return null;\n }\n\n /**\n * Gets the date table cell element that is or contains the specified element.\n * Or returns null if element is not part of a date cell.\n */\n private getCellElement(element: HTMLElement): HTMLElement | null {\n let cell: HTMLElement | undefined;\n if (this.isTableCell(element)) {\n cell = element;\n } else if (this.isTableCell(element.parentNode)) {\n cell = element.parentNode as HTMLElement;\n } else if (this.isTableCell(element.parentNode?.parentNode)) {\n cell = element.parentNode!.parentNode as HTMLElement;\n }\n\n return cell?.getAttribute('data-row') != null ? cell : null;\n }\n\n /** Checks whether a node is a table cell element. */\n private isTableCell(node: Node | undefined | null): node is HTMLTableCellElement {\n return node?.nodeName === 'TD';\n }\n\n /** Checks whether a value is the start of a range. */\n private isStart(value: number, start: number | null, end: number | null): boolean {\n return end !== null && start !== end && value < end && value === start;\n }\n\n /** Checks whether a value is the end of a range. */\n private isEnd(value: number, start: number | null, end: number | null): boolean {\n return start !== null && start !== end && value >= start && value === end;\n }\n\n private isInRange(\n value: number,\n start: number | null,\n end: number | null,\n rangeEnabled: boolean,\n ): boolean {\n return (\n rangeEnabled &&\n start !== null &&\n end !== null &&\n start !== end &&\n value >= start &&\n value <= end\n );\n }\n\n\n /**\n * Extracts the element that actually corresponds to a touch event's location\n * (rather than the element that initiated the sequence of touch events).\n */\n private getActualTouchTarget(event: TouchEvent): Element | null {\n const touchLocation = event.changedTouches[0];\n return document.elementFromPoint(touchLocation.clientX, touchLocation.clientY);\n }\n\n}\n","@if (firstRowOffset < labelMinRequiredCells) {\n <tr aria-hidden=\"true\">\n <td class=\"calendar-body-label\"\n [attr.colspan]=\"numCols\"\n [style.paddingTop]=\"cellPadding\"\n [style.paddingBottom]=\"cellPadding\">\n {{ label }}\n </td>\n </tr>\n}\n\n@for (row of rows; track trackRow(row); let rowIndex = $index) {\n <tr role=\"row\">\n @if (rowIndex === 0 && firstRowOffset) {\n <td\n class=\"calendar-body-label\"\n [attr.colspan]=\"firstRowOffset\"\n [style.paddingTop]=\"cellPadding\"\n [style.paddingBottom]=\"cellPadding\">\n {{ firstRowOffset >= labelMinRequiredCells ? label : ''}}\n </td>\n }\n\n @for (item of row; track item; let colIndex = $index) {\n <td\n role=\"gridcell\"\n class=\"calendar-body-cell-container\"\n [style.width]=\"cellWidth\"\n [style.paddingTop]=\"cellPadding\"\n [style.paddingBottom]=\"cellPadding\"\n [attr.data-row]=\"rowIndex\"\n [attr.data-col]=\"colIndex\"\n >\n <button\n type=\"button\"\n class=\"calendar-body-cell\"\n [tabindex]=\"isActiveCell(rowIndex, colIndex) ? 0 : -1\"\n [class.disabled]=\"!item.enabled\"\n [class.selected]=\"isSelected(item.compareValue)\"\n [class.active]=\"isActiveCell(rowIndex, colIndex)\"\n [class.today]=\"todayValue === item.compareValue\"\n [class.range-start]=\"isRangeStart(item.compareValue)\"\n [class.range-end]=\"isRangeEnd(item.compareValue)\"\n [class.in-range]=\"isCellInRange(item.compareValue)\"\n [class.in-preview]=\"isInPreview(item.compareValue)\"\n [class.preview-start]=\"isPreviewStart(item.compareValue)\"\n [class.preview-end]=\"isPreviewEnd(item.compareValue)\"\n [attr.aria-label]=\"item.ariaLabel\"\n [attr.aria-disabled]=\"!item.enabled || null\"\n [attr.aria-pressed]=\"isSelected(item.compareValue)\"\n [attr.aria-current]=\"todayValue === item.compareValue ? 'date' : null\"\n [attr.aria-describedby]=\"getDescribedby(item.compareValue)\"\n (click)=\"cellClicked(item, $event)\"\n (focus)=\"emitActiveDateChange(item, $event)\">\n <span class=\"calendar-body-cell-content focus-indicator\">\n {{ item.displayValue }}\n </span>\n\n <span class=\"calendar-body-cell-preview\" aria-hidden=\"true\"></span>\n </button>\n </td>\n }\n </tr>\n}\n\n<span [id]=\"startDateLabelId\" class=\"calendar-body-hidden-label\">{{ startDateAccessibleName }}</span>\n\n<span [id]=\"endDateLabelId\" class=\"calendar-body-hidden-label\">{{ endDateAccessibleName }}</span>\n","import { Injectable } from '@angular/core';\n\nimport { ValidatorFn, AbstractControl, ValidationErrors } from '@angular/forms';\n\n@Injectable({\n providedIn: 'root'\n})\n\nexport class IpiDatepickerService {\n\n public DAYS_PER_WEEK = 7;\n\n public yearsPerRow = 4;\n public yearsPerPage = 24;\n\n private dateLocale = 'en-US';\n\n public formatter = new Intl.DateTimeFormat(this.dateLocale, {\n month: '2-digit',\n day: '2-digit',\n year: 'numeric',\n });\n\n public dateFormats = { \n display: {\n dateInput: { year: 'numeric', month: 'numeric', day: 'numeric' },\n monthLabel: { month: 'short' },\n monthYearLabel: { year: 'numeric', month: 'short' },\n dateA11yLabel: { year: 'numeric', month: 'long', day: 'numeric' },\n monthYearA11yLabel: { year: 'numeric', month: 'long' },\n },\n }\n\n public today(): Date {\n return new Date();\n }\n\n public getDate(date: Date): number {\n return date.getDate();\n }\n\n public getMonth(date: Date): number {\n return date.getMonth();\n }\n\n public getYear(date: Date): number {\n return date.getFullYear();\n }\n\n public getDayOfWeek(date: Date): number {\n return date.getDay();\n }\n\n public getDateLocale(): string {\n return this.dateLocale;\n }\n\n public setDateLocale(value: string): void {\n this.formatter = new Intl.DateTimeFormat(value, {\n month: '2-digit',\n day: '2-digit',\n year: 'numeric',\n })\n\n this.dateLocale = value;\n }\n\n public getMonthNames(style: 'long' | 'short' | 'narrow'): string[] {\n const dtf = new Intl.DateTimeFormat(this.dateLocale, { month: style, timeZone: 'utc' });\n\n return this.range(12, i => this.format(dtf, new Date(2017, i, 1)));\n }\n\n public getFirstDayOfWeek(): number {\n if (typeof Intl !== 'undefined' && Intl.Locale) {\n const locale = new Intl.Locale(this.dateLocale) as {\n getWeekInfo?: () => {firstDay: number};\n weekInfo?: {firstDay: number};\n };\n\n // Some browsers implement a `getWeekInfo` method while others have a `weekInfo` getter.\n // Note that this isn't supported in all browsers so we need to null check it.\n const firstDay = (locale.getWeekInfo?.() || locale.weekInfo)?.firstDay ?? 0;\n\n // `weekInfo.firstDay` is a number between 1 and 7 where, starting from Monday,\n // whereas our representation is 0 to 6 where 0 is Sunday so we need to normalize it.\n return firstDay === 7 ? 0 : firstDay;\n }\n\n // Default to Sunday if the browser doesn't provide the week information.\n return 0;\n }\n\n public getValidDateOrNull(obj: unknown): Date | null {\n return this.isDateInstance(obj) && this.isValid(obj as Date) ? (obj as Date) : null;\n }\n\n public getDayOfWeekNames(style: 'long' | 'short' | 'narrow'): string[] {\n const dtf = new Intl.DateTimeFormat(this.dateLocale, { weekday: style, timeZone: 'utc'});\n\n return this.range(7, i => this.format(dtf, new Date(2017, 0, i + 1)));\n }\n\n public getDateNames(): string[] {\n const dtf = new Intl.DateTimeFormat(this.dateLocale, {day: 'numeric', timeZone: 'utc'});\n\n return this.range(31, i => this.format(dtf, new Date(2017, 0, i + 1)));\n }\n\n public getYearName(date: Date): string {\n const dtf = new Intl.DateTimeFormat(this.dateLocale, {year: 'numeric', timeZone: 'utc'});\n\n return this.format(dtf, date);\n }\n\n public getNumDaysInMonth(date: Date): number {\n return this.getDate(\n this.createDateWithOverflow(this.getYear(date), this.getMonth(date) + 1, 0),\n );\n }\n\n public createDate(year: number, month: number, date: number): Date {\n // Check for invalid month and date (except upper bound on date which we have to check after\n // creating the Date).\n if (month < 0 || month > 11) {\n throw Error(`Invalid month index \"${month}\". Month index has to be between 0 and 11.`);\n }\n\n if (date < 1) {\n throw Error(`Invalid date \"${date}\". Date has to be greater than 0.`);\n }\n\n let result = this.createDateWithOverflow(year, month, date);\n // Check that the date wasn't above the upper bound for the month, causing the month to overflow\n if (result.getMonth() != month) {\n throw Error(`Invalid date \"${date}\" for month with index \"${month}\".`);\n }\n\n return result;\n }\n\n public addCalendarDays(date: Date, days: number): Date {\n return this.createDateWithOverflow(\n this.getYear(date),\n this.getMonth(date),\n this.getDate(date) + days,\n );\n }\n\n public addCalendarMonths(date: Date, months: number): Date {\n let newDate = this.createDateWithOverflow(\n this.getYear(date),\n this.getMonth(date) + months,\n this.getDate(date),\n );\n\n // It's possible to wind up in the wrong month if the original month has more days than the new\n // month. In this case we want to go to the last day of the desired month.\n // Note: the additional + 12 % 12 ensures we end up with a positive number, since JS % doesn't\n // guarantee this.\n if (this.getMonth(newDate) != (((this.getMonth(date) + months) % 12) + 12) % 12) {\n newDate = this.createDateWithOverflow(this.getYear(newDate), this.getMonth(newDate), 0);\n }\n\n return newDate;\n }\n\n public addCalendarYears(date: Date, years: number): Date {\n return this.addCalendarMonths(date, years * 12);\n }\n\n public setTime(date: Date, hours: number, minutes: number): Date {\n if (!this.isValid(date)) {\n throw new Error(\"Invalid date provided\");\n }\n\n if (hours < 0 || hours > 23) {\n throw new Error(\"Invalid hours provided, must be between 0 and 23\");\n }\n\n if (minutes < 0 || minutes > 59) {\n throw new Error(\"Invalid minutes provided, must be between 0 and 59\");\n }\n\n const updatedDate = new Date(date);\n\n updatedDate.setHours(hours);\n updatedDate.setMinutes(minutes);\n\n return updatedDate;\n }\n\n public sameDate(first: Date | null, second: Date | null): boolean {\n if (first && second) {\n let firstValid = this.isValid(first);\n let secondValid = this.isValid(second);\n\n if (firstValid && secondValid) {\n return !this.compareDate(first, second);\n }\n\n return firstValid == secondValid;\n }\n\n return first == second;\n }\n\n /**\n * Compares two dates.\n * @param first The first date to compare.\n * @param second The second date to compare.\n * @returns 0 if the dates are equal, a number less than 0 if the first date is earlier,\n * a number greater than 0 if the first date is later.\n */\n public compareDate(first: Date, second: Date): number {\n return (\n this.getYear(first) - this.getYear(second) ||\n this.getMonth(first) - this.getMonth(second) ||\n this.getDate(first) - this.getDate(second)\n );\n }\n\n public deserialize(value: any): Date | null {\n if (value == null || (this.isDateInstance(value) && this.isValid(value))) {\n return value;\n }\n return this.invalid();\n }\n\n public clampDate(date: Date, min?: Date | null, max?: Date | null): Date {\n if (min && this.compareDate(date, min) < 0) {\n return min;\n }\n\n if (max && this.compareDate(date, max) > 0) {\n return max;\n }\n\n return date;\n }\n\n public formatDate(date: Date, displayFormat: Object): string {\n if (!this.isValid(date)) {\n throw Error('DatepickerService: Cannot format invalid date.');\n }\n\n const dtf = new Intl.DateTimeFormat(this.dateLocale, {...displayFormat, timeZone: 'utc'});\n return this.format(dtf, date);\n }\n\n public minValidator(minDate: Date): ValidatorFn {\n return (control: AbstractControl): ValidationErrors | null => {\n const controlValue = control.value;\n \n const min = this.getValidDateOrNull(minDate);\n\n return !min || !controlValue || this.compareDate(min, controlValue) <= 0\n ? null\n : { 'datepickerMin': { min, actual: controlValue } };\n };\n }\n\n public maxValidator(maxDate: Date): ValidatorFn {\n return (control: AbstractControl): ValidationErrors | null => {\n const controlValue = control.value;\n\n const max = this.getValidDateOrNull(maxDate);\n\n return !max || !controlValue || this.compareDate(max, controlValue) >= 0\n ? null\n : { 'datepickerMax': { max, actual: controlValue } };\n };\n }\n\n /** Creates a date but allows the month and date to overflow. */\n private createDateWithOverflow(year: number, month: number, date: number) {\n // Passing the year to the constructor causes year numbers <100 to be converted to 19xx.\n // To work around this we use `setFullYear` and `setHours` instead.\n const d = new Date();\n\n d.setFullYear(year, month, date);\n d.setHours(0, 0, 0, 0);\n\n return d;\n }\n\n private format(dtf: Intl.DateTimeFormat, date: Date) {\n // Passing the year to the constructor causes year numbers <100 to be converted to 19xx.\n // To work around this we use `setUTCFullYear` and `setUTCHours` instead.\n const d = new Date();\n\n d.setUTCFullYear(date.getFullYear(), date.getMonth(), date.getDate());\n d.setUTCHours(date.getHours(), date.getMinutes(), date.getSeconds(), date.getMilliseconds());\n\n return dtf.format(d);\n }\n\n private isDateInstance(obj: any) {\n return obj instanceof Date;\n }\n\n private isValid(date: Date) {\n return !isNaN(date.getTime());\n }\n\n private invalid(): Date {\n return new Date(NaN);\n }\n\n public getActiveOffset<D>(\n activeDate: Date,\n minDate: Date | null,\n maxDate: Date | null,\n ): number {\n const activeYear = this.getYear(activeDate);\n\n return this.positiveModulo(activeYear - this.getStartingYear(minDate, maxDate), this.yearsPerPage);\n }\n\n private getStartingYear<D>(\n minDate: Date| null,\n maxDate: Date | null,\n ): number {\n let startingYear = 0;\n\n if (maxDate) {\n const maxYear = this.getYear(maxDate);\n \n startingYear = maxYear - this.yearsPerPage + 1;\n } else if (minDate) {\n startingYear = this.getYear(minDate);\n }\n\n return startingYear;\n }\n \n public positiveModulo(a: number, b: number): number {\n return ((a % b) + b) % b;\n }\n\n public isSameMultiYearView<D>(\n date1: Date,\n date2: Date,\n minDate: Date | null,\n maxDate: Date | null,\n ): boolean {\n const year1 = this.getYear(date1);\n const year2 = this.getYear(date2);\n\n const startingYear = this.getStartingYear(minDate, maxDate);\n\n return (Math.floor((year1 - startingYear) / this.yearsPerPage) === Math.floor((year2 - startingYear) / this.yearsPerPage));\n }\n\n private range<T>(length: number, valueFunction: (index: number) => T): T[] {\n const valuesArray = Array(length);\n \n for (let i = 0; i < length; i++) {\n valuesArray[i] = valueFunction(i);\n }\n\n return valuesArray;\n }\n\n}\n","import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, Output, ViewChild } from '@angular/core';\n\nimport { FormsModule, ReactiveFormsModule } from '@angular/forms';\n\nimport { Subscription } from 'rxjs';\n\nimport { DateRange } from './../../datepicker.component';\n\nimport { CalendarBody, CalendarCell } from './../calendar-body/calendar-body.component';\n\nimport { IpiDatepickerService } from './../../datepicker-service';\n\n@Component({\n selector: 'ipi-calendar-year-view',\n templateUrl: './year-view.component.html',\n styleUrls: ['./year-view.component.css'],\n changeDetection: ChangeDetectionStrategy.OnPush,\n imports: [\n FormsModule,\n CalendarBody,\n ReactiveFormsModule,\n ],\n})\n\nexport class IpiCalendarYearView {\n\n constructor(\n public datePickerService: IpiDatepickerService,\n private changeDetectorRef: ChangeDetectorRef) {\n this._activeDate = this.datePickerService.today();\n }\n\n /** The body of calendar table */\n @ViewChild(CalendarBody) CalendarBody!: CalendarBody;\n\n @Input()\n public get activeDate(): Date {\n return this._activeDate;\n }\n public set activeDate(value: Date) {\n let oldActiveDate = this._activeDate;\n\n const validDate =\n this.datePickerService.getValidDateOrNull(this.datePickerService.deserialize(value)) ||\n this.datePickerService.today();\n\n this._activeDate = this.datePickerService.clampDate(validDate, this.minDate, this.maxDate);\n\n if (this.datePickerService.getYear(oldActiveDate) !== this.datePickerService.getYear(this._activeDate)) {\n this.init();\n }\n }\n\n @Input()\n public get selected(): DateRange| Date | null {\n return this._selected;\n }\n public set selected(value: DateRange | Date | null) {\n if (value instanceof DateRange) {\n this._selected = value;\n } else {\n this._selected = this.datePickerService.getValidDateOrNull(this.datePickerService.deserialize(value));\n }\n\n this.setSelectedMonth(value);\n }\n\n @Input()\n public get minDate(): Date | null {\n return this._minDate;\n }\n public set minDate(value: Date | null) {\n this._minDate = this.datePickerService.getValidDateOrNull(this.datePickerService.deserialize(value));\n }\n\n @Input()\n public get maxDate(): Date | null {\n return this._maxDate;\n }\n public set maxDate(value: Date | null) {\n this._maxDate = this.datePickerService.getValidDateOrNull(this.datePickerService.deserialize(value));\n }\n\n /** Emits when a new month is selected. */\n @Output() selectedChange: EventEmitter<Date> = new EventEmitter<Date>();\n\n /** Emits the selected month. This doesn't imply a change on the selected date */\n @Output() monthSelected: EventEmitter<Date> = new EventEmitter<Date>();\n\n /** Emits when any date is activated. */\n @Output() activeDateChange: EventEmitter<Date> = new EventEmitter<Date>();\n\n /** Grid of calendar cells representing the months of the year. */\n public months!: CalendarCell[][];\n\n /**\n * The month in this year that the selected Date falls on.\n * Null if the selected Date is in a different year.\n */\n public selectedMonth!: number | null;\n\n /** The label for this year (e.g. \"2017\"). */\n public yearLabel!: string;\n\n /** The month in this year that today falls on. Null if today is in a different year. */\n public todayMonth!: number | null;\n\n private _activeDate: Date;\n private _minDate!: Date | null;\n private _maxDate!: Date | null;\n private _selected!: DateRange | Date | null;\n\n /** Flag used to filter out space/enter keyup events that originated outside of the view. */\n private selectionKeyPressed!: boolean;\n\n private dateFormats = {\n dateInput: null,\n display: {\n dateInput: { year: 'numeric', month: 'numeric', day: 'numeric' },\n monthLabel: { month: 'short' },\n monthYearLabel: { year: 'numeric', month: 'short' },\n dateA11yLabel: { year: 'numeric', month: 'long', day: 'numeric' },\n monthYearA11yLabel: { year: 'numeric', month: 'long' },\n },\n }\n\n public ngAfterContentInit(): void {\n this.init();\n }\n\n /** Initializes this year view. */\n public init(): void {\n this.setSelectedMonth(this.selected);\n\n this.todayMonth = this.getMonthInCurrentYear(this.datePickerService.today());\n\n this.yearLabel = this.datePickerService.getYearName(this.activeDate);\n\n let monthNames = this.datePickerService.getMonthNames('short');\n\n // First row of months only contains 5 elements so we can fit the year label on the same row.\n this.months = [ [0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11]]\n .map(row => row.map(month => this.createCellForMonth(month, monthNames[month])));\n this.changeDetectorRef.markForCheck();\n }\n\n /** Handles when a new month is selected. */\n public onMonthSelected(event: any): void {\n const month = event.value;\n\n const selectedMonth = this.datePickerService.createDate(\n this.datePickerService.getYear(this.activeDate),\n month,\n 1,\n );\n\n this.monthSelected.emit(selectedMonth);\n\n const selectedDate = this.getDateFromMonth(month);\n this.selectedChange.emit(selectedDate);\n }\n\n /**\n * Takes the index of a calendar body cell wrapped in an event as argument. For the date that\n * corresponds to the given cell, set `activeDate` to that date and fire `activeDateChange` with\n * that date.\n *\n * This function is used to match each component's model of the active date with the calendar\n * body cell that was focused. It updates its value of `activeDate` synchronously and updates the\n * parent's value asynchronously via the `activeDateChange` event. The child component receives an\n * updated value asynchronously via the `activeCell` Input.\n */\n public updateActiveDate(event: any): void {\n const month = event.value;\n const oldActiveDate = this._activeDate;\n\n this.activeDate = this.getDateFromMonth(month);\n\n if (this.datePickerService.compareDate(oldActiveDate, this.activeDate)) {\n this.activeDateChange.emit(this.activeDate);\n }\n }\n\n /** Handles keydown events on the calendar body when calendar is in year view. */\n public handleCalendarBodyKeydown(event: KeyboardEvent): void {\n const oldActiveDate = this._activeDate;\n\n switch (event.code) {\n case 'ArrowLeft':\n this.activeDate = this.datePickerService.addCalendarMonths(this._activeDate, -1);\n break;\n case 'ArrowRight':\n this.activeDate = this.datePickerService.addCalendarMonths(this._activeDate, 1);\n break;\n case 'ArrowUp':\n this.activeDate = this.datePickerService.addCalendarMonths(this._activeDate, -4);\n break;\n case 'ArrowDown':\n this.activeDate = this.datePickerService.addCalendarMonths(this._activeDate, 4);\n break;\n case 'Home':\n this.activeDate = this.datePickerService.addCalendarMonths(\n this._activeDate,\n -this.datePickerService.getMonth(this._activeDate),\n );\n break;\n case 'End':\n this.activeDate = this.datePickerService.addCalendarMonths(\n this._activeDate,\n 11 - this.datePickerService.getMonth(this._activeDate),\n );\n break;\n case 'PageUp':\n this.activeDate = this.datePickerService.addCalendarYears(\n this._activeDate,\n event.altKey ? -10 : -1,\n );\n break;\n case 'PageDown':\n this.activeDate = this.datePickerService.addCalendarYears(\n this._activeDate,\n event.altKey ? 10 : 1,\n );\n break;\n case 'Enter':\n case 'Space':\n // Note that we only prevent the default action here while the selection happens in\n // `keyup` below. We can't do the selection here, because it can cause the calendar to\n // reopen if focus is restored immediately. We also can't call `preventDefault` on `keyup`\n // because it's too late (see #23305).\n this.selectionKeyPressed = true;\n break;\n default:\n // Don't prevent default or focus active cell on keys that we don't explicitly handle.\n return;\n }\n\n if (this.datePickerService.compareDate(oldActiveDate, this.activeDate)) {\n this.activeDateChange.emit(this.activeDate);\n this.focusActiveCellAfterViewChecked();\n }\n\n // Prevent unexpected default actions such as form submission.\n event.preventDefault();\n }\n\n /** Handles keyup events on the calendar body when calendar is in year view. */\n public handleCalendarBodyKeyup(event: KeyboardEvent): void {\n if (event.code === 'Space' || event.code === 'Enter') {\n if (this.selectionKeyPressed) {\n this.onMonthSelected({ value: this.datePickerService.getMonth(this._activeDate), event });\n }\n\n this.selectionKeyPressed = false;\n }\n }\n\n /** Focuses the active cell after the microtask queue is empty. */\n public focusActiveCell(): void {\n this.CalendarBody.focusActiveCell();\n }\n\n /** Schedules the matCalendarBody to focus the active cell after change detection has run */\n public focusActiveCellAfterViewChecked(): void {\n this.CalendarBody.scheduleFocusActiveCellAfterViewChecked();\n }\n\n /**\n * Gets the month in this year that the given Date falls on.\n * Returns null if the given Date is in another year.\n */\n private getMonthInCurrentYear(date: Date | null): number | null {\n return date && this.datePickerService.getYear(date) == this.datePickerService.getYear(this.activeDate)\n ? this.datePickerService.getMonth(date)\n : null;\n }\n\n /**\n * Takes a month and returns a new date in the same day and year as the currently active date.\n * The returned date will have the same month as the argument date.\n */\n private getDateFromMonth(month: number): Date {\n const normalizedDate = this.datePickerService.createDate(\n this.datePickerService.getYear(this.activeDate),\n month,\n 1,\n );\n\n const daysInMonth = this.datePickerService.getNumDaysInMonth(normalizedDate);\n\n return this.datePickerService.createDate(\n this.datePickerService.getYear(this.activeDate),\n month,\n Math.min(this.datePickerService.getDate(this.activeDate), daysInMonth),\n );\n }\n\n /** Creates an MatCalendarCell for the given month. */\n private createCellForMonth(month: number, monthName: string) {\n const date = this.datePickerService.createDate(this.datePickerService.getYear(this.activeDate), month, 1);\n const ariaLabel = this.datePickerService.formatDate(date, this.dateFormats.display.monthYearA11yLabel);\n\n return new CalendarCell(\n month,\n monthName.toLocaleUpperCase(),\n ariaLabel,\n this.shouldEnableMonth(month),\n );\n }\n\n /** Whether the given month is enabled. */\n private shouldEnableMonth(month: number) {\n const activeYear = this.datePickerService.getYear(this.activeDate);\n\n if (month === undefined || month === null || this.isYearAndMonthAfterMaxDate(activeYear, month) || this.isYearAndMonthBeforeMinDate(activeYear, month)) {\n return false;\n }\n\n return true;\n }\n\n /**\n * Tests whether the combination month/year is after this.maxDate, considering\n * just the month and year of this.maxDate\n */\n private isYearAndMonthAfterMaxDate(year: number, month: number) {\n if (this.maxDate) {\n const maxYear = this.datePickerService.getYear(this.maxDate);\n const maxMonth = this.datePickerService.getMonth(this.maxDate);\n\n return year > maxYear || (year === maxYear && month > maxMonth);\n }\n\n return false;\n }\n\n /**\n * Tests whether the combination month/year is before this.minDate, considering\n * just the month and year of this.minDate\n */\n private isYearAndMonthBeforeMinDate(year: number, month: number) {\n if (this.minDate) {\n const minYear = this.datePickerService.getYear(this.minDate);\n const minMonth = this.datePickerService.getMonth(this.minDate);\n\n return year < minYear || (year === minYear && month < minMonth);\n }\n\n return false;\n }\n\n /** Sets the currently-selected month based on a model value. */\n private setSelectedMonth(value: DateRange | Date | null): void {\n if (value instanceof DateRange) {\n this.selectedMonth = \n this.getMonthInCurrentYear(value.start) || this.getMonthInCurrentYear(value.end);\n } else {\n this.selectedMonth = this.getMonthInCurrentYear(value);\n }\n }\n\n}\n","<table class=\"calendar-table\" role=\"grid\">\n <thead aria-hidden=\"true\" class=\"calendar-table-header\">\n <tr>\n <th class=\"calendar-table-header-divider\" colspan=\"4\"></th>\n </tr>\n </thead>\n\n <tbody calendar-body\n [label]=\"yearLabel\"\n [rows]=\"months\"\n [todayValue]=\"todayMonth!\"\n [startValue]=\"selectedMonth!\"\n [endValue]=\"selectedMonth!\"\n [labelMinRequiredCells]=\"2\"\n [numCols]=\"4\"\n [cellAspectRatio]=\"4 / 7\"\n [activeCell]=\"datePickerService.getMonth(activeDate)\"\n (selectedValueChange)=\"onMonthSelected($event)\"\n (activeDateChange)=\"updateActiveDate($event)\"\n (keyup)=\"handleCalendarBodyKeyup($event)\"\n (keydown)=\"handleCalendarBodyKeydown($event)\">\n </tbody>\n</table>\n","import { Injectable } from \"@angular/core\";\n\nimport { IpiDatepickerService } from './datepicker-service';\n\nimport { DateRange } from './datepicker.component';\n\n@Injectable({\n providedIn: 'root'\n})\nexport class DefaultCalendarRangeStrategy {\n constructor(private datepickerService: IpiDatepickerService) {}\n\n public selectionFinished(date: Date, currentRange: DateRange): DateRange {\n let { start, end } = currentRange;\n\n if (start == null) {\n start = date;\n } else if (end == null && date && this.datepickerService.compareDate(date, start) >= 0) {\n end = date;\n } else {\n start = date;\n end = null;\n }\n\n return new DateRange(start, end);\n }\n\n public createPreview(activeDate: Date | null, currentRange: DateRange): DateRange {\n let start: Date | null = null;\n let end: Date | null = null;\n\n if (currentRange.start && !currentRange.end && activeDate) {\n start = currentRange.start;\n end = activeDate;\n }\n\n return new DateRange(start, end);\n }\n\n public createDrag(dragOrigin: Date, originalRange: DateRange, newDate: Date): DateRange | null {\n let start = originalRange.start;\n let end = originalRange.end;\n\n if (!start || !end) {\n // Can't drag from an incomplete range.\n return null;\n }\n\n const service = this.datepickerService;\n\n const isRange = service.compareDate(start, end) !== 0;\n const diffYears = service.getYear(newDate) - service.getYear(dragOrigin);\n const diffMonths = service.getMonth(newDate) - service.getMonth(dragOrigin);\n const diffDays = service.getDate(newDate) - service.getDate(dragOrigin);\n\n if (isRange && service.sameDate(dragOrigin, originalRange.start)) {\n start = newDate;\n // when selecting start date of range move only start date\n if (service.compareDate(newDate, end) > 0) {\n // if start date becomes after end date, move whole range\n end = service.addCalendarYears(end, diffYears);\n end = service.addCalendarMonths(end, diffMonths);\n end = service.addCalendarDays(end, diffDays);\n }\n } else if (isRange && service.sameDate(dragOrigin, originalRange.end)) {\n // when selecting end date of range move only end date\n end = newDate;\n if (service.compareDate(newDate, start) < 0) {\n // if end date becomes before start date, move whole range\n start = service.addCalendarYears(start, diffYears);\n start = service.addCalendarMonths(start, diffMonths);\n start = service.addCalendarDays(start, diffDays);\n }\n } else { \n // moving whole range if dragged from a middle date\n start = service.addCalendarYears(start, diffYears);\n start = service.addCalendarMonths(start, diffMonths);\n start = service.addCalendarDays(start, diffDays);\n end = service.addCalendarYears(end, diffYears);\n end = service.addCalendarMonths(end, diffMonths);\n end = service.addCalendarDays(end, diffDays);\n }\n\n return new DateRange(start, end);\n }\n}","import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, Output, SimpleChanges, ViewChild } from '@angular/core';\n\nimport { hasModifierKey } from '@angular/cdk/keycodes';\n\nimport { IpiDatepickerService } from './../../datepicker-service';\n\nimport { CalendarBody, CalendarCell } from './../calendar-body/calendar-body.component';\n\nimport { DateRange, DateSelectionEvent } from './../../datepicker.comp