@ipi-soft/ng-components
Version:
Custom Angular Components
809 lines (805 loc) • 141 kB
JavaScript
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`