@edugouvfr/ngx-dsfr-ext
Version:
NgxDsfrExt est une extension au package @edugouvfr/ngx-dsfr (portage Angular des éléments DSFR)
898 lines • 136 kB
JavaScript
import { Component, ElementRef, EventEmitter, Input, Output, ViewChild, ViewEncapsulation, } from '@angular/core';
import { DsfrFormSelectModule, DsfrI18nPipe, Language, } from '@edugouvfr/ngx-dsfr';
import { Subscription } from 'rxjs';
import { DateUtils } from '../shared/utils/date/date-utils';
import { MESSAGES_EN } from './i18n/messages.en';
import { MESSAGES_FR } from './i18n/messages.fr';
import * as i0 from "@angular/core";
import * as i1 from "@edugouvfr/ngx-dsfr";
/**
* Implémente la saisie d'une date via un input ou via un calendrier affichant les jours et les mois.
* Validator est implémenté uniquement par rapport à min et max
* @since 0.10
* @author pfontanet
*/
export class DsfrCalendarComponent {
/** @internal */
constructor(i18n, langService, loggerService) {
this.i18n = i18n;
this.langService = langService;
this.loggerService = loggerService;
/**
* Valeur initial de la date.
* La valeur doit prendre la forme dd/mm/yyyy pour la locale fr et mm/dd/yyyy pour la locale en.
*/
this.value = '';
/** Force le focus sur la date courante à l'initialisation du composant */
this.autofocus = false;
/** Id de l'élément correspondant au label de la grille de dates */
this.ariaLabelledBy = '';
/** Emission de la date au format DsfrDateEvent */
this.dateChange = new EventEmitter();
/** Evènement au changement d'affichage du calendrier (changement mois ou année) */
this.calendarChange = new EventEmitter();
this.monthSelectOptions = [];
this.yearSelectOptions = [];
this.currentMonthIndex = new Date().getMonth();
this.currentYear = new Date().getFullYear();
this.translatedMonthAbbreviations = [];
this.translatedMonths = [];
this.prevMonthLabel = '';
this.nextMonthLabel = '';
this.monthSelectLabel = '';
this.yearSelectLabel = '';
this.isMonoYear = false;
this.isMonoMonth = false;
this.rows = new Array(6);
this.columns = new Array(7);
/* Node */
this.dayCells = [];
this.focusDay = new Date();
this.selectedDay = new Date(0, 0, 1);
this.lastDate = -1;
this._min = '';
this._max = '';
this.subscription = new Subscription();
this.i18n.extendsLabelsBundle(Language.EN, MESSAGES_EN);
this.i18n.extendsLabelsBundle(Language.FR, MESSAGES_FR);
this.prevCodeLang = langService.lang;
const langChange$ = i18n.completeLangChange$.subscribe((code) => this.onLangChange(code));
this.subscription.add(langChange$);
}
/**
* @return Le code de la langue en cours
*/
get codeLang() {
return this.langService.lang;
}
get min() {
return this._min;
}
get max() {
return this._max;
}
get isEnglishLocale() {
return this.langService.lang === 'en';
}
/**
* Date minimale inclue. Doit être au format IS0-8601 : AAAA-MM-JJ.
* Ce paramètre peut être utilisé seul ou avec la propriété max.
*/
set min(value) {
if (value && !this.isValidIso(value)) {
this.loggerService.warn(`dsfr-date-picker: La propriété min a un format incorrecte. Attendu: 'YYYY-MM-DD'. Reçu: ${value}`);
}
else if (value && this.isValidIso(value)) {
this._min = value;
this.computeYearSelectOptions();
}
}
/**
* Date maximale inclue. Doit être au format IS0-8601 : AAAA-MM-JJ.
* Ce paramètre peut être utilisé seul ou avec la propriété min.
*/
set max(value) {
if (value && !this.isValidIso(value)) {
this.loggerService.warn(`dsfr-date-picker: La propriété max a un format incorrecte. Attendu: 'YYYY-MM-DD'. Reçu: ${value}`);
}
else if (value && this.isValidIso(value)) {
this._max = value;
this.computeYearSelectOptions();
}
}
/** @internal */
ngOnInit() {
this.updateMonthAndYearSelects();
this.computeMonoYearAndMonoMonth();
}
/** @internal */
ngOnDestroy() {
this.subscription.unsubscribe();
}
/** @internal */
ngAfterViewInit() {
const tableCells = this.tbody.nativeElement.getElementsByTagName('td');
this.dayCells = Array.from(tableCells);
this.computeMonthSelectOptions();
this.computeYearSelectOptions();
this.initializeCalendar();
}
/** @internal */
ngOnChanges() {
const validDate = this.reformatValue(this.value);
if (validDate) {
this.setNewValue(validDate, true);
this.computeMonoYearAndMonoMonth();
this.computeMonthSelectOptions();
this.computeYearSelectOptions();
this.initializeCalendar();
}
}
/**
* Permet de forcer le focus sur la date active, la dernière date focusée ou la date du jour par défaut.
*
* Cette méthode est notamment utilisée dans la gestion du calendrier embarqué dans une modale.
*/
focusDate() {
for (let i = 0; i < this.dayCells.length; i++) {
const dayNode = this.dayCells[i];
const day = this.getDayFromDataDateAttribute(dayNode);
if (this.isSameDay(day, this.focusDay) && this.inMinMaxRange(day)) {
dayNode.tabIndex = 0;
dayNode.focus();
}
if (this.isSameDay(day, new Date())) {
dayNode.setAttribute('aria-current', 'date');
}
}
}
/**********************
* Event Handlers
**********************/
handleCancelButton(event) {
let flagToPreventDefault = false;
switch (event.type) {
case 'keydown':
const keyEvent = event;
switch (keyEvent.key) {
case 'Tab':
const reachedBoundary = this.getReachedBoundary();
if (reachedBoundary !== undefined) {
this.autoFocusBoundaryDate(reachedBoundary);
flagToPreventDefault = true;
break;
}
if (!this.inMinMaxRange(this.focusDay)) {
this.nextMonth.nativeElement.focus();
flagToPreventDefault = true;
break;
}
break;
default:
break;
}
break;
case 'click':
flagToPreventDefault = true;
break;
default:
break;
}
if (flagToPreventDefault) {
event.stopPropagation();
event.preventDefault();
}
}
handleNextMonthButton(event) {
let flagToPreventDefault = false;
switch (event.type) {
case 'keydown':
const keyEvent = event;
switch (keyEvent.key) {
case 'Enter':
this.moveToNextMonth();
this.updateTableCellsTabIndex(false);
flagToPreventDefault = true;
break;
case 'Tab':
const reachedBoundary = this.getReachedBoundary();
if (reachedBoundary !== undefined) {
this.autoFocusBoundaryDate(reachedBoundary);
flagToPreventDefault = true;
}
break;
}
break;
case 'click':
this.moveToNextMonth();
this.updateTableCellsTabIndex(false);
break;
default:
break;
}
if (flagToPreventDefault) {
event.stopPropagation();
event.preventDefault();
}
}
handlePreviousMonthButton(event) {
let flagToPreventDefault = false;
switch (event.type) {
case 'keydown':
const keyEvent = event;
switch (keyEvent.key) {
case 'Enter':
this.moveToPreviousMonth();
this.updateTableCellsTabIndex(false);
flagToPreventDefault = true;
break;
}
break;
case 'click':
this.moveToPreviousMonth();
this.updateTableCellsTabIndex(false);
flagToPreventDefault = true;
break;
default:
break;
}
if (flagToPreventDefault) {
event.stopPropagation();
event.preventDefault();
}
}
handleMonthSelectKeydown(event) {
let flagToPreventDefault = false;
switch (event.key) {
case 'Up':
case 'ArrowUp':
this.moveToPreviousMonth();
this.updateTableCellsTabIndex(false);
flagToPreventDefault = true;
break;
case 'Down':
case 'ArrowDown':
this.moveToNextMonth();
this.updateTableCellsTabIndex(false);
flagToPreventDefault = true;
break;
case 'Left':
case 'ArrowLeft':
case 'Right':
case 'ArrowRight':
flagToPreventDefault = true;
}
if (flagToPreventDefault) {
event.stopPropagation();
event.preventDefault();
}
}
handleYearSelectKeydown(event) {
let flagToPreventDefault = false;
switch (event.key) {
case 'Up':
case 'ArrowUp':
this.moveToPreviousYear();
this.updateTableCellsTabIndex(false);
flagToPreventDefault = true;
break;
case 'Down':
case 'ArrowDown':
this.moveToNextYear();
this.updateTableCellsTabIndex(false);
flagToPreventDefault = true;
break;
case 'Left':
case 'ArrowLeft':
case 'Right':
case 'ArrowRight':
flagToPreventDefault = true;
}
if (flagToPreventDefault) {
event.stopPropagation();
event.preventDefault();
}
}
handleDayClick(event) {
if (!this.isDayDisabled(event.currentTarget)) {
this.updateCalendarValue(event.currentTarget);
}
event.stopPropagation();
event.preventDefault();
}
handleDayKeyDown(event) {
let flagToPreventDefault = false;
switch (event.key) {
case ' ':
case 'Enter':
if (this.inMinMaxRange(this.focusDay)) {
this.updateCalendarValue(event.currentTarget);
flagToPreventDefault = true;
}
break;
case 'Right':
case 'ArrowRight':
this.moveFocusToNextDay();
flagToPreventDefault = true;
break;
case 'Left':
case 'ArrowLeft':
this.moveFocusToPreviousDay();
flagToPreventDefault = true;
break;
case 'Down':
case 'ArrowDown':
this.moveFocusToNextWeek();
flagToPreventDefault = true;
break;
case 'Up':
case 'ArrowUp':
this.moveFocusToPreviousWeek();
flagToPreventDefault = true;
break;
case 'PageUp':
if (event.shiftKey) {
this.moveToPreviousYear();
}
else {
this.moveToPreviousMonth();
}
this.updateTableCellsTabIndex();
flagToPreventDefault = true;
break;
case 'PageDown':
if (event.shiftKey) {
this.moveToNextYear();
}
else {
this.moveToNextMonth();
}
this.updateTableCellsTabIndex();
flagToPreventDefault = true;
break;
case 'Home':
this.moveFocusToFirstDayOfWeek();
flagToPreventDefault = true;
break;
case 'End':
this.moveFocusToLastDayOfWeek();
flagToPreventDefault = true;
break;
}
if (flagToPreventDefault) {
event.stopPropagation();
event.preventDefault();
}
}
/**
* Gestion de la sélection dans la liste déroulante des mois
*
* @param selectedMonthIndex Index du mois sélectionné (indexage commençant à 1)
*/
handleMonthSelect(selectedMonthIndex) {
const currentYear = this.focusDay.getFullYear();
const currentDay = this.focusDay.getDate();
const dayToFocus = this.computeDayToFocus(currentYear, selectedMonthIndex, currentDay);
const targetDate = new Date(currentYear, selectedMonthIndex, dayToFocus);
this.lastDate = targetDate.getDate();
this.focusDay = targetDate;
this.updateCalendar();
this.moveFocusToDay(targetDate);
}
/**
* Gestion de la sélection dans la liste déroulante des années
*
* @param selectedYear Année sélectionnée
*/
handleYearSelect(selectedYear) {
const currentMonth = this.focusDay.getMonth();
const currentDay = this.focusDay.getDate();
const dayToFocus = this.computeDayToFocus(selectedYear, currentMonth, currentDay);
const dateToFocus = new Date(selectedYear, currentMonth, dayToFocus);
this.lastDate = dateToFocus.getDate();
this.focusDay = dateToFocus;
this.updateCalendar();
this.moveFocusToDay(dateToFocus);
}
/**
* Permet de calculer si le jour a focus est bien disponible dans le mois et l'année de destination après sélection
* dans la liste déroulante.
*
* Dans le cas où l'on se trouve le 29 Février 2024 et où l'on change d'année grâce à la liste déroulante à 2023,
* le jour a focus devrait être le 28 Février 2023.
*
* @param currentYear Année cible
* @param selectedMonthIndex Mois cible
* @param currentDay Jour courant dans le calendrier
* @returns Le numéro du jour à focus
*/
computeDayToFocus(currentYear, selectedMonthIndex, currentDay) {
const numberOfDaysInMonth = this.getNumberOfDaysInMonth(currentYear, selectedMonthIndex);
const dayToFocus = numberOfDaysInMonth < currentDay ? numberOfDaysInMonth : currentDay;
return dayToFocus;
}
/**
* Formatage de la valeur dans la langue courante
* @param value Valeur au format ISO ou dans la langue courante
*/
reformatValue(value) {
if (value === undefined || value === null) {
return undefined;
}
const parsedDate = DateUtils.parse(this.codeLang, value);
const parsedIsoDate = DateUtils.parseDateIso(value);
if (parsedIsoDate === undefined && parsedDate === undefined) {
return undefined;
}
return DateUtils.format(this.codeLang, parsedIsoDate ?? parsedDate);
}
/**
* Gestion du changement de langue dans le composant.
*
* @param code Code de la locale à prendre en compte
*/
onLangChange(code) {
this.prevMonthLabel = this.i18n.t('calendar.prevMonth');
this.nextMonthLabel = this.i18n.t('calendar.nextMonth');
this.monthSelectLabel = this.i18n.t('calendar.monthSelect');
this.yearSelectLabel = this.i18n.t('calendar.yearSelect');
this.computeMonthSelectOptions();
this.computeYearSelectOptions();
this.updateMonthTranslations();
// Reformatage de l'input
const date = DateUtils.parse(this.prevCodeLang, this.value);
const newValue = DateUtils.format(code, date);
this.setNewValue(newValue);
this.prevCodeLang = code;
this.initializeCalendar();
}
/**
* Compare deux dates et retourne true si ces dernières sont les même.
*
* @param day1 Date 1 à comparer
* @param day2 Date 2 à comparer
* @returns true si les deux dates sont les même
*/
isSameDay(day1, day2) {
if (day1 === undefined || day2 === undefined) {
return false;
}
else {
return (day1.getFullYear() === day2.getFullYear() &&
day1.getMonth() === day2.getMonth() &&
day1.getDate() === day2.getDate());
}
}
/**
* Mets à jour l'ensemble du Date Picker
*/
updateCalendar() {
this.updateMonthAndYearSelects();
const firstDayOfMonth = new Date(this.focusDay.getFullYear(), this.focusDay.getMonth(), 1);
const currentMonthStartingColumn = this.getCurrentMonthStartingColumn(firstDayOfMonth);
const firstDisplayedDayOnCalendar = firstDayOfMonth.setDate(firstDayOfMonth.getDate() - currentMonthStartingColumn);
let dateOfCellForGridLoop = new Date(firstDisplayedDayOnCalendar);
for (let i = 0; i < this.dayCells.length; i++) {
const isDayFromCurrentMonth = dateOfCellForGridLoop.getMonth() !== this.focusDay.getMonth();
this.updateCell(this.dayCells[i], isDayFromCurrentMonth, dateOfCellForGridLoop);
dateOfCellForGridLoop.setDate(dateOfCellForGridLoop.getDate() + 1);
}
this.calendarChange.emit(new Date(this.currentYear, this.currentMonthIndex));
}
/**
* Détermine la colonne à partir de laquelle le mois commence dans le calendrier.
*
* Cette colonne est dépendante du jour de la semaine mais également de la locale : le calendrier anglais a comme
* première colonne le dimanche, contrairement au calendrier français pour lequel la première colonne est le lundi.
*
* @param firstDayOfMonth Index du premier jour de la semaine
* @returns Index de la colonne où commence le mois
*/
getCurrentMonthStartingColumn(firstDayOfMonth) {
if (this.isEnglishLocale) {
return firstDayOfMonth.getDay();
}
else if (firstDayOfMonth.getDay() - 1 === -1) {
return 6;
}
else {
return firstDayOfMonth.getDay() - 1;
}
}
/**
* Mise à jour des valeurs des deux listes déroulantes, mois et année.
*/
updateMonthAndYearSelects() {
this.currentMonthIndex = this.focusDay.getMonth();
this.currentYear = this.focusDay.getFullYear();
}
/**
* Mise à jour de l'état d'une cellule de la table en fonction de :
* - si elle se trouve dans le mois,
* - si elle se trouve en dehors des dates limites,
* - si elle est sélectionnée.
*
* @param tdElement Une cellule td du tableau
* @param outsideOfCurrentMonth boolean true si le jour fait partie du mois ou non
* @param cellDate jour correspondant à la cellule
*/
updateCell(tdElement, outsideOfCurrentMonth, cellDate) {
const numericDayOfTheMonth = cellDate.getDate();
const monthCalendarIndex = cellDate.getMonth() + 1;
const yyyy = cellDate.getFullYear().toString();
const mm = monthCalendarIndex <= 9 ? `0${monthCalendarIndex}` : monthCalendarIndex.toString();
const dd = numericDayOfTheMonth <= 9 ? `0${numericDayOfTheMonth}` : numericDayOfTheMonth.toString();
tdElement.tabIndex = -1;
tdElement.removeAttribute('aria-selected');
tdElement.classList.remove('disabled', 'out-of-month');
tdElement.removeAttribute('aria-disabled');
tdElement.setAttribute('data-date', `${yyyy}-${mm}-${dd}`);
if (!this.inMinMaxRange(cellDate)) {
tdElement.classList.add('disabled');
tdElement.setAttribute('aria-disabled', 'true');
}
else if (outsideOfCurrentMonth) {
tdElement.classList.add('out-of-month');
}
// daySpan est un span caché ('.fr-sr-only') permettant la vocalisation du jour de la semaine au focus d'une date.
// numberSpan est la partie visible dans la grille représentant le nombre du jour.
const [daySpan, numberSpan] = [
tdElement.getElementsByTagName('span')[0],
tdElement.getElementsByTagName('span')[1],
];
daySpan.textContent = `${this.i18n.t('calendar.days')[cellDate.getDay()]} `;
numberSpan.textContent = cellDate.getDate().toString();
if (this.isSameDay(cellDate, this.selectedDay)) {
tdElement.setAttribute('aria-selected', 'true');
}
if (this.isSameDay(cellDate, this.focusDay)) {
tdElement.tabIndex = 0;
}
}
/**
* Initie le changement de focus de la date.
*
* @param targetDate Date cible attendue
*/
moveFocusToDay(targetDate) {
const focusDayMemory = this.focusDay;
this.focusDay = targetDate;
if (focusDayMemory.getMonth() !== this.focusDay.getMonth() ||
focusDayMemory.getFullYear() !== this.focusDay.getFullYear()) {
this.updateCalendar();
}
this.updateTableCellsTabIndex();
}
/**
* Met à jour le tabindex de toutes les cellules de la grille.
*
* Nécessaire pour rendre un élement de la table focusable.
*
* @param autofocus Permet de forcer le focus sur l'élément défini comme étant à tabindex 0. Valeur par défaut à true.
*/
updateTableCellsTabIndex(autofocus = true) {
for (let i = 0; i < this.dayCells.length; i++) {
const dayNode = this.dayCells[i];
const day = this.getDayFromDataDateAttribute(dayNode);
dayNode.tabIndex = -1;
if (this.isSameDay(day, this.focusDay) && this.inMinMaxRange(day)) {
dayNode.tabIndex = 0;
if (autofocus) {
dayNode.focus();
}
}
}
}
/**
* Force le focus sur la limite min ou max d'un mois pour éviter que le focus soit indisponible.
*
* Exemple: L'utilisateur sélectionne le 17 Juillet 2024. Il décide ensuite de changer de mois via la liste
* déroulante et cible Octobre. Cependant, la date max donnée au date picker est le 10 Octobre 2024. Etant incapable
* de focus cette date au retour du focus dans la grille, la nouvelle date de focus bascule alors à la date max déterminée,
* cad. le 10 Octobre 2024.
*
* @param boundary type du boundary à prendre en compte, entre le min et le max
*/
autoFocusBoundaryDate(boundary) {
const boundaryDate = boundary === 'min' ? DateUtils.parseDateIso(this.min) : DateUtils.parseDateIso(this.max);
for (let i = 0; i < this.dayCells.length; i++) {
const dayNode = this.dayCells[i];
const day = this.getDayFromDataDateAttribute(dayNode);
if (boundaryDate && this.isSameDay(day, boundaryDate)) {
dayNode.tabIndex = 0;
dayNode.focus();
}
}
}
initializeCalendar() {
this.computeInitialDate();
this.lastDate = this.focusDay.getDate();
this.updateCalendar();
if (this.autofocus) {
this.focusDate();
}
}
getNumberOfDaysInMonth(year, monthIndex) {
return new Date(year, monthIndex + 1, 0).getDate();
}
/**
* Calcul la date cible en fonction de la date actuelle et du nombre de mois à ajouter ou enlever
* à cette dernière.
*
* @param currentDate Date actuelle
* @param monthsOffset Nombre de mois à ajouter ou enlever à la date actuelle
* @returns La date cible
*/
computeTargetDateWithMonthOffset(currentDate, monthsOffset) {
const getDays = (year, month) => new Date(year, month, 0).getDate();
const isPrev = monthsOffset < 0;
const numYears = Math.trunc(Math.abs(monthsOffset) / 12);
monthsOffset = Math.abs(monthsOffset) % 12;
const newYear = isPrev ? currentDate.getFullYear() - numYears : currentDate.getFullYear() + numYears;
const newMonth = isPrev ? currentDate.getMonth() - monthsOffset : currentDate.getMonth() + monthsOffset;
const newDate = new Date(newYear, newMonth, 1);
const daysInMonth = getDays(newDate.getFullYear(), newDate.getMonth() + 1);
this.lastDate = this.lastDate ? this.lastDate : currentDate.getDate();
if (this.lastDate > daysInMonth) {
newDate.setDate(daysInMonth);
}
else {
newDate.setDate(this.lastDate);
}
return newDate;
}
moveToNextYear() {
this.focusDay = this.computeTargetDateWithMonthOffset(this.focusDay, 12);
this.updateCalendar();
}
moveToPreviousYear() {
this.focusDay = this.computeTargetDateWithMonthOffset(this.focusDay, -12);
this.updateCalendar();
}
moveToNextMonth() {
this.focusDay = this.computeTargetDateWithMonthOffset(this.focusDay, 1);
this.updateCalendar();
}
moveToPreviousMonth() {
this.focusDay = this.computeTargetDateWithMonthOffset(this.focusDay, -1);
this.updateCalendar();
}
moveFocusToNextDay() {
const d = new Date(this.focusDay);
d.setDate(d.getDate() + 1);
if (this.inMinMaxRange(d)) {
this.lastDate = d.getDate();
this.moveFocusToDay(d);
}
}
moveFocusToNextWeek() {
const d = new Date(this.focusDay);
d.setDate(d.getDate() + 7);
if (this.inMinMaxRange(d)) {
this.lastDate = d.getDate();
this.moveFocusToDay(d);
}
}
moveFocusToPreviousDay() {
const d = new Date(this.focusDay);
d.setDate(d.getDate() - 1);
if (this.inMinMaxRange(d)) {
this.lastDate = d.getDate();
this.moveFocusToDay(d);
}
}
moveFocusToPreviousWeek() {
const d = new Date(this.focusDay);
d.setDate(d.getDate() - 7);
if (this.inMinMaxRange(d)) {
this.lastDate = d.getDate();
this.moveFocusToDay(d);
}
}
moveFocusToFirstDayOfWeek() {
const d = new Date(this.focusDay);
d.setDate(d.getDate() - d.getDay());
this.lastDate = d.getDate();
this.moveFocusToDay(d);
}
moveFocusToLastDayOfWeek() {
const d = new Date(this.focusDay);
d.setDate(d.getDate() + (6 - d.getDay()));
this.lastDate = d.getDate();
this.moveFocusToDay(d);
}
// Day methods
isDayDisabled(domNode) {
return domNode.classList.contains('disabled');
}
getDayFromDataDateAttribute(dayNode) {
const dataDate = dayNode.getAttribute('data-date');
const parts = dataDate ? dataDate.split('-') : [];
return parts.length >= 3 ? new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2])) : new Date();
}
// Textbox methods
/**
* Mets à jour la valeur du composant et émets un événement de type DsfrDateEvent.
* Cette emission peut être omise via le paramètre booléen skipEmission, notamment à l'initialisation du composant.
* @param value
* @private
*/
setNewValue(value, skipEmission = false) {
this.value = value;
const date = DateUtils.parse(this.codeLang, this.value);
if (!skipEmission) {
this.dateChange.emit({ value: this.value, date: date });
}
}
/**
* Valorise la date de l'input suite à la sélection d'une date dans le calendrier
* @param domNode cellule td du calendrier telle que 'data-date' contienne la date ISO, ex : data-date="2024-05-28".
* - i18n : le code de formatage fait appel à DateUtils
*/
updateCalendarValue(domNode) {
this.selectedDay = this.getDayFromDataDateAttribute(domNode);
this.focusDay = this.getDayFromDataDateAttribute(domNode);
this.updateCalendar();
const newValue = DateUtils.format(this.codeLang, this.selectedDay);
this.setNewValue(newValue);
}
/**
* Positionne la date du calendrier à partir de
* - i18n : la date est obtenue à partir de DateUtils
*/
computeInitialDate() {
const day = DateUtils.parse(this.codeLang, this.value);
const isOnBoundary = day ? this.isInBounds(day) : false;
if (day && isOnBoundary) {
this.focusDay = day;
this.selectedDay = new Date(this.focusDay);
}
else if (day && !isOnBoundary) {
this.focusDay = new Date();
this.selectedDay = new Date(day);
}
else {
// If not a valid date (MM/DD/YY) initialize with todays date
this.focusDay = new Date();
this.selectedDay = new Date(0, 0, 1);
}
}
/**
* @param date date
* @returns true si la data 'd' est dans l'intervalle [min, max] du cou
*/
inMinMaxRange(date) {
const minDate = this.min ? DateUtils.parseDateIso(this.min) : undefined;
const maxDate = this.max ? DateUtils.parseDateIso(this.max) : undefined;
return ((minDate === undefined || minDate < date || this.isSameDay(date, minDate)) &&
(maxDate === undefined || date < maxDate || this.isSameDay(date, maxDate)));
}
computeMonthSelectOptions() {
const translatedMonths = DateUtils.arr(this.i18n, this.loggerService, 'calendar.months');
this.monthSelectOptions = translatedMonths.map((monthLabel, i) => ({
label: monthLabel,
value: i, // Pour information, en JS, les mois sont indexés à partir de 0 contrairement aux jours et années
}));
}
computeYearSelectOptions() {
const currentYear = new Date().getFullYear();
const defaultMinOffset = 100;
const defaultMaxOffset = 100;
const minYear = this.min ? new Date(this.min).getFullYear() : undefined;
const maxYear = this.max ? new Date(this.max).getFullYear() : undefined;
const lowerBoundYear = minYear ?? currentYear - defaultMinOffset;
const higherBoundYear = maxYear ?? currentYear + defaultMaxOffset;
const options = [];
for (let year = lowerBoundYear; year <= higherBoundYear; year++) {
options.push({ label: year.toString(), value: year });
}
this.yearSelectOptions = options;
}
updateMonthTranslations() {
this.translatedMonths = DateUtils.arr(this.i18n, this.loggerService, 'calendar.months');
this.translatedMonthAbbreviations = DateUtils.arr(this.i18n, this.loggerService, 'calendar.monthsAbbr');
}
isValidIso(boundaryString) {
const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
return dateRegex.test(boundaryString);
}
/**
* Dans le cas d'un changement de mois ou d'année dans le calendrier, cette fonction détermine le type de butée atteinte
* dans le cas ou le nouveau focusDay se trouverait en dehors des dates min ou max, uniquement dans un mois contenant lesdits min ou max.
*
* Exemple: Le focusDay est le 20 Juin 2024 et la date max le 15 Juillet 2024. Si l'utilisateur navigue vers le mois de Juillet via la
* liste déroulante ou la flèche de "Mois suivant", getReachedBoundary retourne 'max', qui est le type de butée atteinte.
*
* @returns Retourne le type de butée atteinte (min ou max) ou undefined si aucune butée n'est atteinte ou qu'aucun focusDay n'est valide pour
* cette date.
*/
getReachedBoundary() {
const minDate = this.min ? DateUtils.parseDateIso(this.min) : undefined;
const maxDate = this.max ? DateUtils.parseDateIso(this.max) : undefined;
if (!this.min && !this.max) {
return undefined;
}
if (minDate &&
this.focusDay.getFullYear() === minDate.getFullYear() &&
this.focusDay.getMonth() === minDate.getMonth() &&
this.focusDay.getDate() < minDate.getDate()) {
return 'min';
}
if (maxDate &&
this.focusDay.getFullYear() === maxDate.getFullYear() &&
this.focusDay.getMonth() === maxDate.getMonth() &&
this.focusDay.getDate() > maxDate.getDate()) {
return 'max';
}
return undefined;
}
computeMonoYearAndMonoMonth() {
const hasMinAndMax = !!this.min && !!this.max;
this.isMonoYear =
hasMinAndMax &&
DateUtils.parseDateIso(this.min)?.getFullYear() === DateUtils.parseDateIso(this.max)?.getFullYear();
this.isMonoMonth =
hasMinAndMax &&
this.isMonoYear &&
DateUtils.parseDateIso(this.min)?.getMonth() === DateUtils.parseDateIso(this.max)?.getMonth();
}
/**
* Vérifie si la date active du composant se trouve dans les limites données du composant.
*
* @param date
* @returns Un booléen indiquant si la date se trouve bien entre les limites minimum et maximum données.
*/
isInBounds(date) {
const minDate = this.min ? DateUtils.parseDateIso(this.min) : undefined;
const maxDate = this.max ? DateUtils.parseDateIso(this.max) : undefined;
if (!minDate && !maxDate) {
return true;
}
else if (maxDate && !minDate) {
return date.getTime() < maxDate.getTime();
}
else if (minDate && !maxDate) {
return date.getTime() > minDate.getTime();
}
else {
return date.getTime() < maxDate.getTime() && date.getTime() > minDate.getTime();
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: DsfrCalendarComponent, deps: [{ token: i1.DsfrI18nService }, { token: i1.LangService }, { token: i1.LoggerService }], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.13", type: DsfrCalendarComponent, isStandalone: true, selector: "dsfr-ext-calendar, dsfrx-calendar", inputs: { value: "value", autofocus: "autofocus", ariaLabelledBy: "ariaLabelledBy", min: "min", max: "max" }, outputs: { dateChange: "dateChange", calendarChange: "calendarChange" }, viewQueries: [{ propertyName: "previousMonth", first: true, predicate: ["previousMonth"], descendants: true }, { propertyName: "nextMonth", first: true, predicate: ["nextMonth"], descendants: true }, { propertyName: "monthSelect", first: true, predicate: ["monthSelect"], descendants: true, read: ElementRef }, { propertyName: "yearSelect", first: true, predicate: ["yearSelect"], descendants: true, read: ElementRef }, { propertyName: "tbody", first: true, predicate: ["tbody"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<header class=\"header\">\n <button\n type=\"button\"\n class=\"prev-month fr-btn fr-btn--tertiary-no-outline fr-icon-arrow-left-line\"\n [title]=\"prevMonthLabel\"\n [attr.aria-label]=\"prevMonthLabel\"\n (click)=\"handlePreviousMonthButton($event)\"\n (keydown)=\"handlePreviousMonthButton($event)\"\n #previousMonth\n data-testid=\"datepicker-prev-month\"></button>\n\n <div class=\"header__select-container\">\n <dsfr-form-select\n placeHolder=\"Mois\"\n [value]=\"currentMonthIndex\"\n [options]=\"monthSelectOptions\"\n [label]=\"monthSelectLabel\"\n [labelSrOnly]=\"true\"\n [disabled]=\"isMonoMonth\"\n (keydown)=\"handleMonthSelectKeydown($event)\"\n (selectChange)=\"handleMonthSelect($event)\"\n #monthSelect\n data-testid=\"datepicker-month-select\"></dsfr-form-select>\n\n <div class=\"select-displayer\" aria-live=\"polite\">\n {{ translatedMonthAbbreviations[currentMonthIndex] }} \n @if (!isMonoMonth) {\n <span aria-hidden=\"true\" class=\"fr-icon-arrow-down-s-line\"></span>\n }\n </div>\n </div>\n\n <div class=\"header__select-container\">\n <dsfr-form-select\n ariaLabel=\"Ann\u00E9e\"\n placeHolder=\"Ann\u00E9e\"\n [value]=\"currentYear\"\n [options]=\"yearSelectOptions\"\n [label]=\"monthSelectLabel\"\n [labelSrOnly]=\"true\"\n [disabled]=\"isMonoYear\"\n (keydown)=\"handleYearSelectKeydown($event)\"\n (selectChange)=\"handleYearSelect($event)\"\n #yearSelect\n data-testid=\"datepicker-year-select\"></dsfr-form-select>\n\n <div class=\"select-displayer\" aria-live=\"polite\">\n {{ currentYear }} \n @if (!isMonoYear) {\n <span aria-hidden=\"true\" class=\"fr-icon-arrow-down-s-line\"></span>\n }\n </div>\n </div>\n\n <button\n type=\"button\"\n class=\"next-month fr-btn fr-btn--tertiary-no-outline fr-icon-arrow-right-line\"\n [title]=\"nextMonthLabel\"\n (click)=\"handleNextMonthButton($event)\"\n [attr.aria-label]=\"nextMonthLabel\"\n (keydown)=\"handleNextMonthButton($event)\"\n #nextMonth\n data-testid=\"datepicker-next-month\"></button>\n</header>\n\n<div class=\"table-wrap\">\n <table class=\"dates\" role=\"grid\" aria-labelledby=\"id-grid-label\">\n <thead>\n <tr>\n @if (isEnglishLocale) {\n <th scope=\"col\" abbr=\"{{ 'calendar.days[0]' | dsfrI18n }}\">{{ 'calendar.daysAbbr[0]' | dsfrI18n }}</th>\n }\n <th scope=\"col\" abbr=\"{{ 'calendar.days[1]' | dsfrI18n }}\">{{ 'calendar.daysAbbr[1]' | dsfrI18n }}</th>\n <th scope=\"col\" abbr=\"{{ 'calendar.days[2]' | dsfrI18n }}\">{{ 'calendar.daysAbbr[2]' | dsfrI18n }}</th>\n <th scope=\"col\" abbr=\"{{ 'calendar.days[3]' | dsfrI18n }}\">{{ 'calendar.daysAbbr[3]' | dsfrI18n }}</th>\n <th scope=\"col\" abbr=\"{{ 'calendar.days[4]' | dsfrI18n }}\">{{ 'calendar.daysAbbr[4]' | dsfrI18n }}</th>\n <th scope=\"col\" abbr=\"{{ 'calendar.days[5]' | dsfrI18n }}\">{{ 'calendar.daysAbbr[5]' | dsfrI18n }}</th>\n <th scope=\"col\" abbr=\"{{ 'calendar.days[6]' | dsfrI18n }}\">{{ 'calendar.daysAbbr[6]' | dsfrI18n }}</th>\n @if (!isEnglishLocale) {\n <th scope=\"col\" abbr=\"{{ 'calendar.days[0]' | dsfrI18n }}\">{{ 'calendar.daysAbbr[0]' | dsfrI18n }}</th>\n }\n </tr>\n </thead>\n\n <tbody #tbody>\n @for (row of rows; track $index) {\n <tr>\n @for (column of columns; track $index) {\n <td tabindex=\"-1\" (click)=\"handleDayClick($event)\" (keydown)=\"handleDayKeyDown($event)\" #td>\n <span class=\"fr-sr-only\"></span>\n <span>-1</span>\n </td>\n }\n </tr>\n }\n </tbody>\n </table>\n</div>\n", styles: ["dsfr-ext-calendar,dsfrx-calendar{display:block;width:320px;clear:both}.header{cursor:default;padding:.375rem;font-weight:700;text-transform:uppercase;display:flex;justify-content:space-around}.header__select-container{position:relative}.header__select-container:focus-within{outline-style:solid;outline-offset:2px;outline-width:2px;outline-color:#0a76f6}.header__select-container .fr-select-group{max-width:6.5rem}.header__select-container .fr-select-group select.fr-select{margin-top:0;opacity:0}.header__select-container .select-displayer{z-index:-1;position:absolute;inset:0;display:flex;align-items:center;justify-content:center}h2{margin:0;padding:0;display:inline-block;font-size:1em;text-transform:none;font-weight:700;border:none}button::-moz-focus-inner{border:0}.dates{width:320px}.dialog-ok-cancel-group{text-align:right;margin-top:1em;margin-bottom:1em;margin-right:1em}.dialog-ok-cancel-group button{margin-left:1em}table.dates{padding-left:1em;padding-right:1em;border:none;border-collapse:separate}table.dates th,table.dates td{text-align:center;color:var(--text-default-grey);background-color:var(--background-default-grey)}table.dates tr{border:1px solid black}table.dates td{padding:3px;margin:0;line-height:inherit;height:40px;width:40px;border:1px solid var(--blue-france-925-125);font-size:15px}table.dates td.disabled{padding:2px;border:none;height:41px;width:41px;color:var(--text-disabled-grey);background-color:var(--background-disabled-grey);font-style:italic}table.dates td.disabled:hover{cursor:not-allowed}table.dates td.out-of-month{background-color:var(--background-alt-grey);color:var(--text-mention-grey);border:none}table.dates td:focus:not(.disabled){padding:1px;outline:2px solid var(--border-active-blue-france)!important;background-color:var(--background-default-grey);color:var(--text-default-grey)}table.dates td:hover{padding:0;background-color:var(--background-action-low-blue-france-hover);color:var(--text-default-grey);cursor:pointer}table.dates td:hover:not(.disabled){padding:2px;border:1px solid var(--border-active-blue-france)}table.dates td[aria-selected]:not(.disabled){background-color:var(--background-action-high-blue-france);color:var(--text-inverted-grey)}\n"], dependencies: [{ kind: "ngmodule", type: DsfrFormSelectModule }, { kind: "component", type: i1.DsfrFormSelectComponent, selector: "dsfr-form-select", inputs: ["placeholder", "required", "ariaLabel", "ariaInvalid", "error", "valid", "message", "messageSeverity", "labelSrOnly", "placeHolder", "options", "compareWith"], outputs: ["selectChange"] }, { kind: "pipe", type: DsfrI18nPipe, name: "dsfrI18n" }], encapsulation: i0.ViewEncapsulation.None }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: DsfrCalendarComponent, decorators: [{
type: Component,
args: [{ selector: 'dsfr-ext-calendar, dsfrx-calendar', encapsulation: ViewEncapsulation.None, standalone: true, imports: [DsfrFormSelectModule, DsfrI18nPipe], template: "<header class=\"header\">\n <button\n type=\"button\"\n class=\"prev-month fr-btn fr-btn--tertiary-no-outline fr-icon-arrow-left-line\"\n [title]=\"prevMonthLabel\"\n [attr.aria-label]=\"prevMonthLabel\"\n (click)=\"handlePreviousMonthButton($event)\"\n (keydown)=\"handlePreviousMonthButton($event)\"\n #previousMonth\n data-testid=\"datepicker-prev-month\"></button>\n\n <div class=\"header__select-container\">\n <dsfr-form-select\n placeHolder=\"Mois\"\n [value]=\"currentMonthIndex\"\n [options]=\"monthSelectOptions\"\n [label]=\"monthSelectLabel\"\n [labelSrOnly]=\"true\"\n [disabled]=\"isMonoMonth\"\n (keydown)=\"handleMonthSelectKeydown($event)\"\n (selectChange)=\"handleMonthSelect($event)\"\n #monthSelect\n data-testid=\"datepicker-month-select\"></dsfr-form-select>\n\n <div class=\"select-displayer\" aria-live=\"polite\">\n {{ translatedMonthAbbreviations[currentMonthIndex] }} \n @if (!isMonoMonth) {\n <span aria-hidden=\"true\" class=\"fr-icon-arrow-down-s-line\"></span>\n }\n </div>\n </div>\n\n <div class=\"header__select-container\">\n <dsfr-form-select\n ariaLabel=\"Ann\u00E9e\"\n placeHolder=\"Ann\u00E9e\"\n [value]=\"currentYear\"\n [options]=\"yearSelectOptions\"\n [label]=\"monthSelectLabel\"\n [labelSrOnly]=\"true\"\n [disabled]=\"isMonoYear\"\n (keydown)=\"handleYearSelectKeydown($event)\"\n (selectChange)=\"handleYearSelect($event)\"\n #yearSelect\n data-testid=\"datepicker-year-select\"></dsfr-form-select>\n\n <div class=\"select-displayer\" aria-live=\"polite\">\n {{ currentYear }} \n @if (!isMonoYear) {\n <span aria-hidden=\"true\" class=\"fr-icon-arrow-down-s-line\"></span>\n }\n </div>\n </div>\n\n <button\n type=\"button\"\n class=\"next-month fr-btn fr-btn--tertiary-no-outline fr-icon-arrow-right-line\"\n [title]=\"nextMonthLabel\"\n (click)=\"handleNextMonthButton($event)\"\n [attr.aria-label]=\"nextMonthLabel\"\n (keydown)=\"handleNextMonthButton($event)\"\n #nextMonth\n data-testid=\"datepicker-next-month\"></button>\n</header>\n\n<div class=\"table-wrap\">\n <table class=\"dates\" role=\"grid\" aria-labelledby=\"id-grid-label\">\n <thead>\n <tr>\n @if (isEnglishLocale) {\n <th scope=\"col\" abbr=\"{{ 'calendar.days[0]' | dsfrI18n }}\">{{ 'calendar.daysAbbr[0]' | dsfrI18n }}</th>\n }\n <th scope=\"col\" abbr=\"{{ 'calendar.days[1]' | dsfrI18n }}\">{{ 'calendar.daysAbbr[1]' | dsfrI18n }}</th>\n <th scope=\"col\" abbr=\"{{ 'calendar.days[2]' | dsfrI18n }}\">{{ 'calendar.daysAbbr[2]' | dsfrI18n }}</th>\n <th scope=\"col\" abbr=\"{{ 'calendar.days[3]' | dsfrI18n }}\">{{ 'calendar.daysAbbr[3]' | dsfrI18n }}</th>\n <th scope=\"col\" abbr=\"{{ 'calendar.days[4]' | dsfrI18n }}\">{{ 'calendar.daysAbbr[4]' | dsfrI18n }}</th>\n <th scope=\"col\" abbr=\"{{ 'calendar.days[5]' | dsfrI18n }}\">{{ 'calendar.daysAbbr[5]' | dsfrI18n }}</th>\n <th scope=\"col\" abbr=\"{{ 'calendar.days[6]' | dsfrI18n }}\">{{ 'calendar.daysAbbr[6]' | dsfrI18n }}</th>\n @if (!isEnglishLocale) {\n <th scope=\"col\" abbr=\"{{ 'calendar.days[0]' | dsfrI18n }}\">{{ 'calendar.daysAbbr[0]' | dsfrI18n }}</th>\n }\n </tr>\n </thead>\n\n <tbody #tbody>\n @for (row of rows; track $index) {\n <tr>\n @for (column of columns; track $index) {\n <td tabindex=\"-1\" (click)=\"handleDayClick($event)\" (keydown)=\"handleDayKeyDown($event)\" #td>\n <span class=\"fr-sr-only\"></span>\n <span>-1</span>\n </td>\n }\n </tr>\n }\n </tbody>\n </table>\n</div>\n", styles: ["dsfr-ext-calendar,dsfrx-calendar{display:block;width:320px;clear:both}.header{cursor:default;padding:.375rem;font-weight:700;text-transform:uppercase;display:flex;justify-content:space-around}.header__select-container{position:relative}.header__select-container:focus-within{outline-style:solid;outline-offset:2px;outline-width:2px;outline-color:#0a76f6}.header__select-container .fr-select-group{max-width:6.5rem}.header__select-container .fr-select-group select.fr-select{margin-top:0;opacity:0}.header__select-container .select-displayer{z-index:-1;position:absolute;inset:0;display:flex;align-items:center;justify-content:center}h2{margin:0;padding:0;display:inline-block;font-size:1em;text-transform:none;font-weight:700;border:none}button::-moz-focus-inner{border:0}.dates{width:320px}.dialog-ok-cancel-group{text-align:right;margin-top:1em;margin-bottom:1em;margin-right:1em}.dialog-ok-cancel-group button{margin-left:1em}table.dates{padding-left:1em;padding-right:1em;border:none;border-collapse:separate}table.dates th,table.dates td{text-align:center;color:var(--text-default-grey);background-color:var(--background-default-grey)}table.dates tr{border:1px solid black}table.dates td{padding:3px;margin:0;line-height:inherit;height:40px;width:40px;border:1px solid var(--blue-france-925-125);font-size:15px}table.dates td.disabled{padding:2px;border:none;height:41px;width:41px;color:var(--text-disabled-grey);background-color:var(--background-disabled-grey);font-style:italic}table.dates td.disabled:hover{cursor:not-allowed}table.dates td.out-of-month{background-color:var(--background-alt-grey);color:var(--text-mention-grey);border:none}table.dates td:focus:not(.disabled){padding:1px;outline:2px solid var(--border-active-blue-france)!important;background-color:var(--background-default-grey);color:var(--text-default-grey)}table.dates td:hover{padding:0;background-color:var(--background-action-low-blue-france-hover);color:var(--text-default-grey);cursor:pointer}table.dates td:hover:not(.disabled){padding:2px;border:1px solid var(--border-active-blue-france)}table.dates td[aria-selected]:not(.disabled){background-color:var(--background-action-high-blue-france);color:var(--text-inverted-grey)}\n"] }]
}], ctorParameters: () => [{ type: i1.DsfrI18nService }, { type: i1.LangService }, { type: i1.LoggerService }], propDecorators: { value: [{
type: Input
}], autofocus: [{
type: Input
}], ariaLabelledBy: [{
type: Input,
args: [{ required: true }]
}], dateChange: [{
type: Output
}], calendarChange: [{
type: Output
}], previousMonth: [{
type: ViewChild,
args: ['previousMonth']
}], nextMonth: [{
type: ViewChild,
args: ['nextMonth']
}], monthSelect: [{
type: ViewChild,
args: ['monthSelect', { read: ElementRef }]
}], yearSelect: [{
type: ViewChild,
args: ['yearSelect', { read: ElementRef }]
}], tbody: [{
type: ViewChild,
args: ['tbody']
}], min: [{
type: Input
}], max: [{
type: Input
}] } });
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2FsZW5kYXIuY29tcG9uZW50LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vLi4vcHJvamVjdHMvbmd4LWRzZnItZXh0L3NyYy9saWIvY2FsZW5kYXIvY2FsZW5kYXIuY29tcG9uZW50LnRzIiwiLi4vLi4vLi4vLi4vLi4vcHJvamVjdHMvbmd4LWRzZnItZXh0L3NyYy9saWIvY2FsZW5kYXIvY2FsZW5kYXIuY29tcG9uZW50Lmh0bWwiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUVMLFNBQVMsRUFDVCxVQUFVLEVBQ1YsWUFBWSxFQUNaLEtBQUssRUFJTCxNQUFNLEVBQ04sU0FBUyxFQUNULGlCQUFpQixHQUNsQixNQUFNLGVBQWUsQ0FBQztBQUN2QixPQUFPLEVBQ0wsb0JBQW9CLEVBQ3BCLFlBQVksRUFJWixRQUFRLEdBRVQsTUFBTSxxQkFBcUIsQ0FBQztBQUM3QixPQUFPLEVBQUUsWUFBWSxFQUF