UNPKG

@ipi-soft/ng-components

Version:

Custom Angular Components

809 lines (805 loc) 141 kB
import * as i0 from '@angular/core'; import { EventEmitter, Component, ChangeDetectionStrategy, Input, Output, Injectable, ViewChild, HostListener } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import * as i1$1 from '@ipi-soft/ng-components/services'; import { MobileOS } from '@ipi-soft/ng-components/services'; import { IpiButtonComponent } from '@ipi-soft/ng-components/button'; import * as i1 from '@angular/cdk/platform'; import { normalizePassiveListenerOptions } from '@angular/cdk/platform'; import { hasModifierKey } from '@angular/cdk/keycodes'; class CalendarCell { constructor(value, displayValue, ariaLabel, enabled, compareValue = value, rawValue) { this.value = value; this.displayValue = displayValue; this.ariaLabel = ariaLabel; this.enabled = enabled; this.compareValue = compareValue; this.rawValue = rawValue; } } let calendarBodyId = 1; class CalendarBody { constructor(ngZone, platform, elementRef) { this.ngZone = ngZone; this.platform = platform; this.elementRef = elementRef; /** The number of columns in the table. */ this.numCols = 7; /** The cell number of the active cell in the table. */ this.activeCell = 0; /** Whether a range is being selected. */ this.isRange = false; /** * The aspect ratio (width / height) to use for the cells in the table. This aspect ratio will be * maintained even as the table resizes. */ this.cellAspectRatio = 1; /** Start of the preview range. */ this.previewStart = null; /** End of the preview range. */ this.previewEnd = null; /** Emits when a new value is selected. */ this.selectedValueChange = new EventEmitter(); /** Emits when the preview has changed as a result of a user action. */ this.previewChange = new EventEmitter(); this.activeDateChange = new EventEmitter(); /** Emits the date at the possible start of a drag event. */ this.dragStarted = new EventEmitter(); /** Emits the date at the conclusion of a drag, or null if mouse was not released on a date. */ this.dragEnded = new EventEmitter(); this.id = `calendar-body-${calendarBodyId++}`; this.startDateLabelId = `${this.id}-start-date`; this.endDateLabelId = `${this.id}-end-date`; /** * Used to focus the active cell after change detection has run. */ this.focusActiveCellAfterViewChecked = true; this.didDragSinceMouseDown = false; this.activeCapturingEventOptions = normalizePassiveListenerOptions({ passive: false, capture: true, }); this.passiveCapturingEventOptions = normalizePassiveListenerOptions({ passive: true, capture: true, }); this.passiveEventOptions = normalizePassiveListenerOptions({ passive: true }); this.trackRow = (row) => row; /** * Event handler for when the user enters an element * inside the calendar body (e.g. by hovering in or focus). */ this.enterHandler = (event) => { if (this.skipNextFocus && event.type === 'focus') { this.skipNextFocus = false; return; } // We only need to hit the zone when we're selecting a range. if (event.target && this.isRange) { const cell = this.getCellFromElement(event.target); if (!cell?.enabled) { return; } if (cell) { this.ngZone.run(() => this.previewChange.emit({ value: cell.enabled ? cell : null, event })); } } }; this.touchmoveHandler = (event) => { if (!this.isRange) return; const target = this.getActualTouchTarget(event); const cell = target ? this.getCellFromElement(target) : null; if (!cell?.enabled) { return; } if (target !== event.target) { this.didDragSinceMouseDown = true; } // If the initial target of the touch is a date cell, prevent default so // that the move is not handled as a scroll. if (this.getCellElement(event.target)) { event.preventDefault(); } this.ngZone.run(() => this.previewChange.emit({ value: cell?.enabled ? cell : null, event })); }; /** * Event handler for when the user's pointer leaves an element * inside the calendar body (e.g. by hovering out or blurring). */ this.leaveHandler = (event) => { // We only need to hit the zone when we're selecting a range. if (this.previewEnd !== null && this.isRange) { if (event.type !== 'blur') { this.didDragSinceMouseDown = true; } const cell = this.getCellFromElement(event.target); if (!cell?.enabled) { return; } // Only reset the preview end value when leaving cells. This looks better, because // we have a gap between the cells and the rows and we don't want to remove the // range just for it to show up again when the user moves a few pixels to the side. if (event.target && cell && !(event.relatedTarget && this.getCellFromElement(event.relatedTarget))) { this.ngZone.run(() => this.previewChange.emit({ value: null, event })); } } }; /** * Triggered on mousedown or touchstart on a date cell. * Respsonsible for starting a drag sequence. */ this.mousedownHandler = (event) => { if (!this.isRange) return; this.didDragSinceMouseDown = false; // Begin a drag if a cell within the current range was targeted. const cell = event.target && this.getCellFromElement(event.target); if (!cell || !this.isCellInRange(cell.compareValue)) { return; } this.ngZone.run(() => { this.dragStarted.emit({ value: cell.rawValue, event, }); }); }; /** Triggered on mouseup anywhere. Respsonsible for ending a drag sequence. */ this.mouseupHandler = (event) => { if (!this.isRange) return; const cellElement = this.getCellElement(event.target); if (!cellElement) { // Mouseup happened outside of datepicker. Cancel drag. this.ngZone.run(() => { this.dragEnded.emit({ value: null, event }); }); return; } // if (cellElement.closest('.calendar-body') !== this.elementRef.nativeElement) { // // Mouseup happened inside a different month instance. // // Allow it to handle the event. // return; // } this.ngZone.run(() => { const cell = this.getCellFromElement(cellElement); this.dragEnded.emit({ value: cell?.rawValue ?? null, event }); }); }; /** Triggered on touchend anywhere. Respsonsible for ending a drag sequence. */ this.touchendHandler = (event) => { const target = this.getActualTouchTarget(event); if (target) { this.mouseupHandler({ target }); } }; this.ngZone.runOutsideAngular(() => { const element = this.elementRef.nativeElement; element.addEventListener('touchmove', this.touchmoveHandler, this.activeCapturingEventOptions); element.addEventListener('mouseenter', this.enterHandler, this.passiveCapturingEventOptions); element.addEventListener('focus', this.enterHandler, this.passiveCapturingEventOptions); element.addEventListener('mouseleave', this.leaveHandler, this.passiveCapturingEventOptions); element.addEventListener('mousedown', this.mousedownHandler, this.passiveEventOptions); element.addEventListener('touchstart', this.mousedownHandler, this.passiveEventOptions); if (this.platform.isBrowser) { window.addEventListener('mouseup', this.mouseupHandler); window.addEventListener('touchend', this.touchendHandler); } }); } ngOnChanges(changes) { const columnChanges = changes['numCols']; const { rows, numCols } = this; if (changes['rows'] || columnChanges) { this.firstRowOffset = rows && rows.length && rows[0].length ? numCols - rows[0].length : 0; } if (changes['cellAspectRatio'] || columnChanges || !this.cellPadding) { this.cellPadding = `${(50 * this.cellAspectRatio) / numCols}%`; } if (columnChanges || !this.cellWidth) { this.cellWidth = `${100 / numCols}%`; } } ngAfterViewChecked() { if (this.focusActiveCellAfterViewChecked) { this.focusActiveCell(); this.focusActiveCellAfterViewChecked = false; } } ngOnDestroy() { const element = this.elementRef.nativeElement; element.removeEventListener('touchmove', this.touchmoveHandler, this.activeCapturingEventOptions); element.removeEventListener('mouseenter', this.enterHandler, this.passiveCapturingEventOptions); element.removeEventListener('focus', this.enterHandler, this.passiveCapturingEventOptions); element.removeEventListener('mouseleave', this.leaveHandler, this.passiveCapturingEventOptions); element.removeEventListener('mousedown', this.mousedownHandler, this.passiveEventOptions); element.removeEventListener('touchstart', this.mousedownHandler, this.passiveEventOptions); if (this.platform.isBrowser) { window.removeEventListener('mouseup', this.mouseupHandler); window.removeEventListener('touchend', this.touchendHandler); } } /** Called when a cell is clicked. */ cellClicked(cell, event) { // Ignore "clicks" that are actually canceled drags (eg the user dragged // off and then went back to this cell to undo). if (this.didDragSinceMouseDown) { return; } if (cell.enabled) { this.selectedValueChange.emit({ value: cell.value, event }); } } emitActiveDateChange(cell, event) { if (cell.enabled) { this.activeDateChange.emit({ value: cell.value, event }); } } /** Returns whether a cell should be marked as selected. */ isSelected(value) { return this.startValue === value || this.endValue === value; } /** Returns whether a cell is active. */ isActiveCell(rowIndex, colIndex) { let cellNumber = rowIndex * this.numCols + colIndex; // Account for the fact that the first row may not have as many cells. if (rowIndex) { cellNumber -= this.firstRowOffset; } return cellNumber == this.activeCell; } focusActiveCell(movePreview = true) { const activeCell = this.elementRef.nativeElement.querySelector('.active'); if (activeCell) { if (!movePreview) { this.skipNextFocus = true; } activeCell.focus(); } } /** Focuses the active cell after change detection has run and the microtask queue is empty. */ scheduleFocusActiveCellAfterViewChecked() { this.focusActiveCellAfterViewChecked = true; } /** Gets whether a value is the start of the main range. */ isRangeStart(value) { return this.isStart(value, this.startValue, this.endValue); } /** Gets whether a value is the end of the main range. */ isRangeEnd(value) { return this.isEnd(value, this.startValue, this.endValue); } /** Gets whether a value is within the currently-selected range. */ isCellInRange(value) { return this.isInRange(value, this.startValue, this.endValue, this.isRange); } /** Gets whether a value is the start of the preview range. */ isPreviewStart(value) { return this.isStart(value, this.previewStart, this.previewEnd); } /** Gets whether a value is the end of the preview range. */ isPreviewEnd(value) { return this.isEnd(value, this.previewStart, this.previewEnd); } /** Gets whether a value is inside the preview range. */ isInPreview(value) { return this.isInRange(value, this.previewStart, this.previewEnd, this.isRange); } /** Gets ids of aria descriptions for the start and end of a date range. */ getDescribedby(value) { if (!this.isRange) { return null; } if (this.startValue === value && this.endValue === value) { return `${this.startDateLabelId} ${this.endDateLabelId}`; } else if (this.startValue === value) { return this.startDateLabelId; } else if (this.endValue === value) { return this.endDateLabelId; } return null; } /** Finds the CalendarCell that corresponds to a DOM node. */ getCellFromElement(element) { const cell = this.getCellElement(element); if (cell) { const row = cell.getAttribute('data-row'); const col = cell.getAttribute('data-col'); if (row && col) { return this.rows[parseInt(row)][parseInt(col)]; } } return null; } /** * Gets the date table cell element that is or contains the specified element. * Or returns null if element is not part of a date cell. */ getCellElement(element) { let cell; if (this.isTableCell(element)) { cell = element; } else if (this.isTableCell(element.parentNode)) { cell = element.parentNode; } else if (this.isTableCell(element.parentNode?.parentNode)) { cell = element.parentNode.parentNode; } return cell?.getAttribute('data-row') != null ? cell : null; } /** Checks whether a node is a table cell element. */ isTableCell(node) { return node?.nodeName === 'TD'; } /** Checks whether a value is the start of a range. */ isStart(value, start, end) { return end !== null && start !== end && value < end && value === start; } /** Checks whether a value is the end of a range. */ isEnd(value, start, end) { return start !== null && start !== end && value >= start && value === end; } isInRange(value, start, end, rangeEnabled) { return (rangeEnabled && start !== null && end !== null && start !== end && value >= start && value <= end); } /** * Extracts the element that actually corresponds to a touch event's location * (rather than the element that initiated the sequence of touch events). */ getActualTouchTarget(event) { const touchLocation = event.changedTouches[0]; return document.elementFromPoint(touchLocation.clientX, touchLocation.clientY); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.4", ngImport: i0, type: CalendarBody, deps: [{ token: i0.NgZone }, { token: i1.Platform }, { token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.1.4", type: CalendarBody, isStandalone: true, selector: "[calendar-body]", inputs: { label: "label", rows: "rows", todayValue: "todayValue", startValue: "startValue", endValue: "endValue", labelMinRequiredCells: "labelMinRequiredCells", numCols: "numCols", activeCell: "activeCell", isRange: "isRange", cellAspectRatio: "cellAspectRatio", comparisonStart: "comparisonStart", comparisonEnd: "comparisonEnd", previewStart: "previewStart", previewEnd: "previewEnd", startDateAccessibleName: "startDateAccessibleName", endDateAccessibleName: "endDateAccessibleName" }, outputs: { selectedValueChange: "selectedValueChange", previewChange: "previewChange", activeDateChange: "activeDateChange", dragStarted: "dragStarted", dragEnded: "dragEnded" }, host: { classAttribute: "calendar-body" }, usesOnChanges: true, ngImport: i0, template: "@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", styles: ["tr{height:40px}.calendar-body-label{height:0;line-height:0;text-align:start}.calendar-body-cell{width:100%;height:100%;position:absolute;top:0;left:0;outline:none;text-align:center;color:var(--ipi-calendar-body-cell-color, #0B1222);background:var(--ipi-datepicker-calendar-body-cell-background-color, none);border:var(--ipi-calendar-body-cell-border, none);margin:0}.calendar-body-hidden-label{display:none}.calendar-body-cell-container{position:relative;height:0;line-height:0}.calendar-body-cell:before,.calendar-body-cell:after,.calendar-body-cell-preview{height:100%;width:100%;display:block;position:absolute;top:0;left:0;content:\"\";box-sizing:border-box;z-index:0}.calendar-body-cell{border-radius:4px;transition:border-radius .1s ease-out}.calendar-body-cell:hover{color:var(--ipi-calendar-body-cell-hover-color, white);background-color:var(--ipi-calendar-body-cell-hover-background-color, #FFD2C6)}.today{border:var(--ipi-calendar-body-today-cell-border, 1px solid #FFD2C6)}.in-preview,.calendar-body-cell.in-preview,.calendar-body-cell.in-range.in-preview{background-color:var(--ipi-calendar-body-cell-in-preview-background-color, #FFD2C6);border-left:0px;border-right:0px;border-radius:0;border-top:var(--ipi-calendar-body-cell-in-preview-border, 1px dashed #F96138);border-bottom:var(--ipi-calendar-body-cell-in-preview-border, 1px dashed #F96138)}.calendar-body-cell.in-preview.preview-end{border-top-right-radius:20px;border-bottom-right-radius:20px;border-right:var(--ipi-calendar-body-cell-preview-end-border, 1px dashed #F96138);background-color:var(--ipi-calendar-body-cell-preview-end-background-color, #FFD2C6);color:var(--ipi-calendar-body-cell-preview-end-color, #FFFFFF)}.calendar-body-cell.in-preview.preview-start{border-top-left-radius:20px;border-bottom-left-radius:20px;border-left:var(--ipi-calendar-body-cell-preview-start-border, 1px dashed #F96138);background-color:var(--ipi-calendar-body-cell-preview-start-background-color, #FFD2C6);color:var(--ipi-calendar-body-cell-preview-start-color, #FFFFFF)}.calendar-body-cell:hover .in-preview,.calendar-body-cell:hover .in-range{border-radius:0 20px 20px 0/0px 20px 20px 0px}.active:focus{background-color:var(--ipi-calendar-body-active-cell-background-color, #FFD2C6)}.calendar-body-cell.in-range{background-color:var(--ipi-calendar-body-cell-in-range-background-color, #FFF2EF);border-left:0px;border-right:0px;border-radius:0;border-top:var(--ipi-calendar-body-cell-in-range-border, 1px solid #F96138);border-bottom:var(--ipi-calendar-body-cell-in-range-border, 1px solid #F96138)}.in-range.active{background-color:var(--ipi-calendar-body-cell-in-range-active-background-color, #FFD2C6)}.in-range.range-start{border-top-left-radius:20px;border-bottom-left-radius:20px;color:var(--ipi-calendar-body-cell-range-start-color, #FFFFFF);border-left:var(--ipi-calendar-body-cell-range-start-border, 1px solid #F96138);background-color:var(--ipi-calendar-body-cell-range-start-background-color, #F96138)}.in-range.range-end{border-top-right-radius:20px;border-bottom-right-radius:20px;color:var(--ipi-calendar-body-cell-range-end-color, #FFFFFF);border-right:var(--ipi-datepicker-calendar-body-cell-end-start-border, 1px solid #F96138);background-color:var(--ipi-datepicker-calendar-body-cell-end-start-background-color, #F96138)}.calendar-body-cell.in-range.range-end,.calendar-body-cell.in-range.range-start{background-color:var(--ipi-datepicker-calendar-body-cell-end-start-background-color, #F96138)}.active.in-range.range-end,.active.in-range.range-start{background-color:var(--ipi-calendar-body-cell-active-in-range-start-end-background-color, #FFD2C6)}.calendar-body-cell.selected{color:var(--ipi-calendar-body-cell-selected-color, #FFFFFF);background-color:var(--ipi-calendar-body-cell-selected-background-color, #F96138)}.disabled{cursor:none;border:none;border-radius:0;pointer-events:none;background-color:var(--ipi-calendar-body-cell-disabled-background-color, #E0DFDE)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.4", ngImport: i0, type: CalendarBody, decorators: [{ type: Component, args: [{ selector: '[calendar-body]', host: { 'class': 'calendar-body', }, changeDetection: ChangeDetectionStrategy.OnPush, template: "@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", styles: ["tr{height:40px}.calendar-body-label{height:0;line-height:0;text-align:start}.calendar-body-cell{width:100%;height:100%;position:absolute;top:0;left:0;outline:none;text-align:center;color:var(--ipi-calendar-body-cell-color, #0B1222);background:var(--ipi-datepicker-calendar-body-cell-background-color, none);border:var(--ipi-calendar-body-cell-border, none);margin:0}.calendar-body-hidden-label{display:none}.calendar-body-cell-container{position:relative;height:0;line-height:0}.calendar-body-cell:before,.calendar-body-cell:after,.calendar-body-cell-preview{height:100%;width:100%;display:block;position:absolute;top:0;left:0;content:\"\";box-sizing:border-box;z-index:0}.calendar-body-cell{border-radius:4px;transition:border-radius .1s ease-out}.calendar-body-cell:hover{color:var(--ipi-calendar-body-cell-hover-color, white);background-color:var(--ipi-calendar-body-cell-hover-background-color, #FFD2C6)}.today{border:var(--ipi-calendar-body-today-cell-border, 1px solid #FFD2C6)}.in-preview,.calendar-body-cell.in-preview,.calendar-body-cell.in-range.in-preview{background-color:var(--ipi-calendar-body-cell-in-preview-background-color, #FFD2C6);border-left:0px;border-right:0px;border-radius:0;border-top:var(--ipi-calendar-body-cell-in-preview-border, 1px dashed #F96138);border-bottom:var(--ipi-calendar-body-cell-in-preview-border, 1px dashed #F96138)}.calendar-body-cell.in-preview.preview-end{border-top-right-radius:20px;border-bottom-right-radius:20px;border-right:var(--ipi-calendar-body-cell-preview-end-border, 1px dashed #F96138);background-color:var(--ipi-calendar-body-cell-preview-end-background-color, #FFD2C6);color:var(--ipi-calendar-body-cell-preview-end-color, #FFFFFF)}.calendar-body-cell.in-preview.preview-start{border-top-left-radius:20px;border-bottom-left-radius:20px;border-left:var(--ipi-calendar-body-cell-preview-start-border, 1px dashed #F96138);background-color:var(--ipi-calendar-body-cell-preview-start-background-color, #FFD2C6);color:var(--ipi-calendar-body-cell-preview-start-color, #FFFFFF)}.calendar-body-cell:hover .in-preview,.calendar-body-cell:hover .in-range{border-radius:0 20px 20px 0/0px 20px 20px 0px}.active:focus{background-color:var(--ipi-calendar-body-active-cell-background-color, #FFD2C6)}.calendar-body-cell.in-range{background-color:var(--ipi-calendar-body-cell-in-range-background-color, #FFF2EF);border-left:0px;border-right:0px;border-radius:0;border-top:var(--ipi-calendar-body-cell-in-range-border, 1px solid #F96138);border-bottom:var(--ipi-calendar-body-cell-in-range-border, 1px solid #F96138)}.in-range.active{background-color:var(--ipi-calendar-body-cell-in-range-active-background-color, #FFD2C6)}.in-range.range-start{border-top-left-radius:20px;border-bottom-left-radius:20px;color:var(--ipi-calendar-body-cell-range-start-color, #FFFFFF);border-left:var(--ipi-calendar-body-cell-range-start-border, 1px solid #F96138);background-color:var(--ipi-calendar-body-cell-range-start-background-color, #F96138)}.in-range.range-end{border-top-right-radius:20px;border-bottom-right-radius:20px;color:var(--ipi-calendar-body-cell-range-end-color, #FFFFFF);border-right:var(--ipi-datepicker-calendar-body-cell-end-start-border, 1px solid #F96138);background-color:var(--ipi-datepicker-calendar-body-cell-end-start-background-color, #F96138)}.calendar-body-cell.in-range.range-end,.calendar-body-cell.in-range.range-start{background-color:var(--ipi-datepicker-calendar-body-cell-end-start-background-color, #F96138)}.active.in-range.range-end,.active.in-range.range-start{background-color:var(--ipi-calendar-body-cell-active-in-range-start-end-background-color, #FFD2C6)}.calendar-body-cell.selected{color:var(--ipi-calendar-body-cell-selected-color, #FFFFFF);background-color:var(--ipi-calendar-body-cell-selected-background-color, #F96138)}.disabled{cursor:none;border:none;border-radius:0;pointer-events:none;background-color:var(--ipi-calendar-body-cell-disabled-background-color, #E0DFDE)}\n"] }] }], ctorParameters: () => [{ type: i0.NgZone }, { type: i1.Platform }, { type: i0.ElementRef }], propDecorators: { label: [{ type: Input }], rows: [{ type: Input }], todayValue: [{ type: Input }], startValue: [{ type: Input }], endValue: [{ type: Input }], labelMinRequiredCells: [{ type: Input }], numCols: [{ type: Input }], activeCell: [{ type: Input }], isRange: [{ type: Input }], cellAspectRatio: [{ type: Input }], comparisonStart: [{ type: Input }], comparisonEnd: [{ type: Input }], previewStart: [{ type: Input }], previewEnd: [{ type: Input }], startDateAccessibleName: [{ type: Input }], endDateAccessibleName: [{ type: Input }], selectedValueChange: [{ type: Output }], previewChange: [{ type: Output }], activeDateChange: [{ type: Output }], dragStarted: [{ type: Output }], dragEnded: [{ type: Output }] } }); class IpiDatepickerService { constructor() { this.DAYS_PER_WEEK = 7; this.yearsPerRow = 4; this.yearsPerPage = 24; this.dateLocale = 'en-US'; this.formatter = new Intl.DateTimeFormat(this.dateLocale, { month: '2-digit', day: '2-digit', year: 'numeric', }); this.dateFormats = { display: { dateInput: { year: 'numeric', month: 'numeric', day: 'numeric' }, monthLabel: { month: 'short' }, monthYearLabel: { year: 'numeric', month: 'short' }, dateA11yLabel: { year: 'numeric', month: 'long', day: 'numeric' }, monthYearA11yLabel: { year: 'numeric', month: 'long' }, }, }; } today() { return new Date(); } getDate(date) { return date.getDate(); } getMonth(date) { return date.getMonth(); } getYear(date) { return date.getFullYear(); } getDayOfWeek(date) { return date.getDay(); } getDateLocale() { return this.dateLocale; } setDateLocale(value) { this.formatter = new Intl.DateTimeFormat(value, { month: '2-digit', day: '2-digit', year: 'numeric', }); this.dateLocale = value; } getMonthNames(style) { const dtf = new Intl.DateTimeFormat(this.dateLocale, { month: style, timeZone: 'utc' }); return this.range(12, i => this.format(dtf, new Date(2017, i, 1))); } getFirstDayOfWeek() { if (typeof Intl !== 'undefined' && Intl.Locale) { const locale = new Intl.Locale(this.dateLocale); // Some browsers implement a `getWeekInfo` method while others have a `weekInfo` getter. // Note that this isn't supported in all browsers so we need to null check it. const firstDay = (locale.getWeekInfo?.() || locale.weekInfo)?.firstDay ?? 0; // `weekInfo.firstDay` is a number between 1 and 7 where, starting from Monday, // whereas our representation is 0 to 6 where 0 is Sunday so we need to normalize it. return firstDay === 7 ? 0 : firstDay; } // Default to Sunday if the browser doesn't provide the week information. return 0; } getValidDateOrNull(obj) { return this.isDateInstance(obj) && this.isValid(obj) ? obj : null; } getDayOfWeekNames(style) { const dtf = new Intl.DateTimeFormat(this.dateLocale, { weekday: style, timeZone: 'utc' }); return this.range(7, i => this.format(dtf, new Date(2017, 0, i + 1))); } getDateNames() { const dtf = new Intl.DateTimeFormat(this.dateLocale, { day: 'numeric', timeZone: 'utc' }); return this.range(31, i => this.format(dtf, new Date(2017, 0, i + 1))); } getYearName(date) { const dtf = new Intl.DateTimeFormat(this.dateLocale, { year: 'numeric', timeZone: 'utc' }); return this.format(dtf, date); } getNumDaysInMonth(date) { return this.getDate(this.createDateWithOverflow(this.getYear(date), this.getMonth(date) + 1, 0)); } createDate(year, month, date) { // Check for invalid month and date (except upper bound on date which we have to check after // creating the Date). if (month < 0 || month > 11) { throw Error(`Invalid month index "${month}". Month index has to be between 0 and 11.`); } if (date < 1) { throw Error(`Invalid date "${date}". Date has to be greater than 0.`); } let result = this.createDateWithOverflow(year, month, date); // Check that the date wasn't above the upper bound for the month, causing the month to overflow if (result.getMonth() != month) { throw Error(`Invalid date "${date}" for month with index "${month}".`); } return result; } addCalendarDays(date, days) { return this.createDateWithOverflow(this.getYear(date), this.getMonth(date), this.getDate(date) + days); } addCalendarMonths(date, months) { let newDate = this.createDateWithOverflow(this.getYear(date), this.getMonth(date) + months, this.getDate(date)); // It's possible to wind up in the wrong month if the original month has more days than the new // month. In this case we want to go to the last day of the desired month. // Note: the additional + 12 % 12 ensures we end up with a positive number, since JS % doesn't // guarantee this. if (this.getMonth(newDate) != (((this.getMonth(date) + months) % 12) + 12) % 12) { newDate = this.createDateWithOverflow(this.getYear(newDate), this.getMonth(newDate), 0); } return newDate; } addCalendarYears(date, years) { return this.addCalendarMonths(date, years * 12); } setTime(date, hours, minutes) { if (!this.isValid(date)) { throw new Error("Invalid date provided"); } if (hours < 0 || hours > 23) { throw new Error("Invalid hours provided, must be between 0 and 23"); } if (minutes < 0 || minutes > 59) { throw new Error("Invalid minutes provided, must be between 0 and 59"); } const updatedDate = new Date(date); updatedDate.setHours(hours); updatedDate.setMinutes(minutes); return updatedDate; } sameDate(first, second) { if (first && second) { let firstValid = this.isValid(first); let secondValid = this.isValid(second); if (firstValid && secondValid) { return !this.compareDate(first, second); } return firstValid == secondValid; } return first == second; } /** * Compares two dates. * @param first The first date to compare. * @param second The second date to compare. * @returns 0 if the dates are equal, a number less than 0 if the first date is earlier, * a number greater than 0 if the first date is later. */ compareDate(first, second) { return (this.getYear(first) - this.getYear(second) || this.getMonth(first) - this.getMonth(second) || this.getDate(first) - this.getDate(second)); } deserialize(value) { if (value == null || (this.isDateInstance(value) && this.isValid(value))) { return value; } return this.invalid(); } clampDate(date, min, max) { if (min && this.compareDate(date, min) < 0) { return min; } if (max && this.compareDate(date, max) > 0) { return max; } return date; } formatDate(date, displayFormat) { if (!this.isValid(date)) { throw Error('DatepickerService: Cannot format invalid date.'); } const dtf = new Intl.DateTimeFormat(this.dateLocale, { ...displayFormat, timeZone: 'utc' }); return this.format(dtf, date); } minValidator(minDate) { return (control) => { const controlValue = control.value; const min = this.getValidDateOrNull(minDate); return !min || !controlValue || this.compareDate(min, controlValue) <= 0 ? null : { 'datepickerMin': { min, actual: controlValue } }; }; } maxValidator(maxDate) { return (control) => { const controlValue = control.value; const max = this.getValidDateOrNull(maxDate); return !max || !controlValue || this.compareDate(max, controlValue) >= 0 ? null : { 'datepickerMax': { max, actual: controlValue } }; }; } /** Creates a date but allows the month and date to overflow. */ createDateWithOverflow(year, month, date) { // Passing the year to the constructor causes year numbers <100 to be converted to 19xx. // To work around this we use `setFullYear` and `setHours` instead. const d = new Date(); d.setFullYear(year, month, date); d.setHours(0, 0, 0, 0); return d; } format(dtf, date) { // Passing the year to the constructor causes year numbers <100 to be converted to 19xx. // To work around this we use `setUTCFullYear` and `setUTCHours` instead. const d = new Date(); d.setUTCFullYear(date.getFullYear(), date.getMonth(), date.getDate()); d.setUTCHours(date.getHours(), date.getMinutes(), date.getSeconds(), date.getMilliseconds()); return dtf.format(d); } isDateInstance(obj) { return obj instanceof Date; } isValid(date) { return !isNaN(date.getTime()); } invalid() { return new Date(NaN); } getActiveOffset(activeDate, minDate, maxDate) { const activeYear = this.getYear(activeDate); return this.positiveModulo(activeYear - this.getStartingYear(minDate, maxDate), this.yearsPerPage); } getStartingYear(minDate, maxDate) { let startingYear = 0; if (maxDate) { const maxYear = this.getYear(maxDate); startingYear = maxYear - this.yearsPerPage + 1; } else if (minDate) { startingYear = this.getYear(minDate); } return startingYear; } positiveModulo(a, b) { return ((a % b) + b) % b; } isSameMultiYearView(date1, date2, minDate, maxDate) { const year1 = this.getYear(date1); const year2 = this.getYear(date2); const startingYear = this.getStartingYear(minDate, maxDate); return (Math.floor((year1 - startingYear) / this.yearsPerPage) === Math.floor((year2 - startingYear) / this.yearsPerPage)); } range(length, valueFunction) { const valuesArray = Array(length); for (let i = 0; i < length; i++) { valuesArray[i] = valueFunction(i); } return valuesArray; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.4", ngImport: i0, type: IpiDatepickerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.1.4", ngImport: i0, type: IpiDatepickerService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.4", ngImport: i0, type: IpiDatepickerService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); class IpiCalendarYearView { constructor(datePickerService, changeDetectorRef) { this.datePickerService = datePickerService; this.changeDetectorRef = changeDetectorRef; /** Emits when a new month is selected. */ this.selectedChange = new EventEmitter(); /** Emits the selected month. This doesn't imply a change on the selected date */ this.monthSelected = new EventEmitter(); /** Emits when any date is activated. */ this.activeDateChange = new EventEmitter(); this.dateFormats = { dateInput: null, display: { dateInput: { year: 'numeric', month: 'numeric', day: 'numeric' }, monthLabel: { month: 'short' }, monthYearLabel: { year: 'numeric', month: 'short' }, dateA11yLabel: { year: 'numeric', month: 'long', day: 'numeric' }, monthYearA11yLabel: { year: 'numeric', month: 'long' }, }, }; this._activeDate = this.datePickerService.today(); } get activeDate() { return this._activeDate; } set activeDate(value) { let oldActiveDate = this._activeDate; const validDate = this.datePickerService.getValidDateOrNull(this.datePickerService.deserialize(value)) || this.datePickerService.today(); this._activeDate = this.datePickerService.clampDate(validDate, this.minDate, this.maxDate); if (this.datePickerService.getYear(oldActiveDate) !== this.datePickerService.getYear(this._activeDate)) { this.init(); } } get selected() { return this._selected; } set selected(value) { if (value instanceof DateRange) { this._selected = value; } else { this._selected = this.datePickerService.getValidDateOrNull(this.datePickerService.deserialize(value)); } this.setSelectedMonth(value); } get minDate() { return this._minDate; } set minDate(value) { this._minDate = this.datePickerService.getValidDateOrNull(this.datePickerService.deserialize(value)); } get maxDate() { return this._maxDate; } set maxDate(value) { this._maxDate = this.datePickerService.getValidDateOrNull(this.datePickerService.deserialize(value)); } ngAfterContentInit() { this.init(); } /** Initializes this year view. */ init() { this.setSelectedMonth(this.selected); this.todayMonth = this.getMonthInCurrentYear(this.datePickerService.today()); this.yearLabel = this.datePickerService.getYearName(this.activeDate); let monthNames = this.datePickerService.getMonthNames('short'); // First row of months only contains 5 elements so we can fit the year label on the same row. this.months = [[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11]] .map(row => row.map(month => this.createCellForMonth(month, monthNames[month]))); this.changeDetectorRef.markForCheck(); } /** Handles when a new month is selected. */ onMonthSelected(event) { const month = event.value; const selectedMonth = this.datePickerService.createDate(this.datePickerService.getYear(this.activeDate), month, 1); this.monthSelected.emit(selectedMonth); const selectedDate = this.getDateFromMonth(month); this.selectedChange.emit(selectedDate); } /** * Takes the index of a calendar body cell wrapped in an event as argument. For the date that * corresponds to the given cell, set `activeDate` to that date and fire `activeDateChange` with * that date. * * This function is used to match each component's model of the active date with the calendar * body cell that was focused. It updates its value of `activeDate` synchronously and updates the * parent's value asynchronously via the `activeDateChange` event. The child component receives an * updated value asynchronously via the `activeCell` Input. */ updateActiveDate(event) { const month = event.value; const oldActiveDate = this._activeDate; this.activeDate = this.getDateFromMonth(month); if (this.datePickerService.compareDate(oldActiveDate, this.activeDate)) { this.activeDateChange.emit(this.activeDate); } } /** Handles keydown events on the calendar body when calendar is in year view. */ handleCalendarBodyKeydown(event) { const oldActiveDate = this._activeDate; switch (event.code) { case 'ArrowLeft': this.activeDate = this.datePickerService.addCalendarMonths(this._activeDate, -1); break; case 'ArrowRight': this.activeDate = this.datePickerService.addCalendarMonths(this._activeDate, 1); break; case 'ArrowUp': this.activeDate = this.datePickerService.addCalendarMonths(this._activeDate, -4); break; case 'ArrowDown': this.activeDate = this.datePickerService.addCalendarMonths(this._activeDate, 4); break; case 'Home': this.activeDate = this.datePickerService.addCalendarMonths(this._activeDate, -this.datePickerService.getMonth(this._activeDate)); break; case 'End': this.activeDate = this.datePickerService.addCalendarMonths(this._activeDate, 11 - this.datePickerService.getMonth(this._activeDate)); break; case 'PageUp': this.activeDate = this.datePickerService.addCalendarYears(this._activeDate, event.altKey ? -10 : -1); break; case 'PageDown': this.activeDate = this.datePickerService.addCalendarYears(this._activeDate, event.altKey ? 10 : 1); break; case 'Enter': case 'Space': // Note that we only prevent the default action here while the selection happens in // `keyup` below. We can't do the selection here, because it can cause the calendar to // reopen if focus is restored immediately. We also can't call `preventDefault` on `keyup`