/**
* @module EventView/Parts/DateTime
* @local TimePartManager
* @local TimePart
*/
import {
DATE_FORMAT,
DATE_HOUR_FORMAT,
DATE_TIME_FORMAT,
} from '../../../constants/constants.dates.js';
import { EMPTY_STRING } from '../../../constants/constants.js';
import { MelHtml } from '../../../html/JsHtml/MelHtml.js';
import { GuestsPart } from './guestspart.js';
import { CLASS_ALL_DAY } from './parts.constants.js';
import { FakePart, Parts } from './parts.js';
/**
* @class
* @classdesc Gère la partie lié à la date et à l'heure
*/
export class TimePartManager {
/**
*
* @param {external:jQuery} $start_date Champ date de début
* @param {external:jQuery} $start_time Champ heure de début
* @param {external:jQuery} $start_select Champ visuel de l'heure de début
* @param {external:jQuery} $end_date Champ date de fin
* @param {external:jQuery} $end_time Champ haure de fin
* @param {external:jQuery} $end_select Champ visuel de l'heure de fin
* @param {external:jQuery} $allDay Checkbox "journée entière"
*/
constructor(
$start_date,
$start_time,
$start_select,
$end_date,
$end_time,
$end_select,
$allDay,
) {
/**
* Partie qui gère l'heure de début
* @type {TimePart}
* @member
*/
this.start = new TimePart($start_time, $start_select);
/**
* Partie qui gère l'heure de fin
* @type {TimePart}
* @member
*/
this.end = new TimePart($end_time, $end_select);
/**
* Champ date de début
* @package
* @type {external:jQuery}
* @member
*/
this._$start_date = $start_date;
/**
* Champ date de fin
* @package
* @type {external:jQuery}
* @member
*/
this._$end_date = $end_date;
/**
* Checkbox journée entière
* @type {external:jQuery}
* @member
*/
this.$allDay = $allDay;
/**
* Différence de temps entre la date de début et la date de fin
* @type {number}
* @member
* @readonly
*/
this.base_diff = 0;
/**
* Date de début
* @type {external:moment}
* @member
* @readonly
*/
this.date_start = null;
/**
* Date de fin
* @type {external:moment}
* @member
* @readonly
*/
this.date_end = null;
/**
* Si l'évènement est une journée entière ou non
* @type {boolean}
* @readonly
* @member
*/
this.is_all_day = false;
Object.defineProperties(this, {
date_start: {
get: () => {
return moment(
`${this._$start_date.val()} ${this.start._$fakeField.val()}`,
DATE_TIME_FORMAT,
);
},
},
date_end: {
get: () => {
return moment(
`${this._$end_date.val()} ${this.end._$fakeField.val()}`,
DATE_TIME_FORMAT,
);
},
},
is_all_day: {
get: () => this.$allDay?.prop?.('checked') ?? false,
},
});
}
/**
* Initialise les champs.
* @param {*} event Evènement du plugin `Calendar`
*/
init(event) {
//Journée entière
if (event.allDay) {
this.start.init(moment().startOf('day'), this.base_diff);
this.end.init(moment().startOf('day'), this.base_diff);
this.$allDay[0].checked = true;
} else {
//Si une date n'éxiste pas
if (!event.start || !event.end) {
const now = moment();
if (!event.start) event.start = now;
if (!event.end) {
event.end = moment(event.start).add(TimePart.INTERVAL, 'm');
if (
event.start.format(DATE_FORMAT) !== event.end.format(DATE_FORMAT)
) {
event.end = moment(event.start).endOf('d');
if (
event.start.format(DATE_TIME_FORMAT) ===
event.end.format(DATE_TIME_FORMAT)
)
event.start = event.start.remove(1, 'm');
}
}
}
//On initialise les champs
this.start.init(moment(event.start), this.base_diff);
this.end.reinit(
moment(event.end),
this.base_diff,
moment(event.start).format(DATE_FORMAT) ===
moment(event.end).format(DATE_FORMAT)
? moment(event.start).format(DATE_HOUR_FORMAT)
: null,
);
this.$allDay[0].checked = false;
}
//Gestion des évènements
if (
($._data(this.start._$fakeField[0], 'events')?.change?.length ?? 0) <= 1
) {
this.start._$fakeField.on('change', () => {
//cal.event_times_changed();
this._end_date_updated();
// GuestsPart.UpdateDispos(
// this.date_start,
// this.date_end,
// this.$allDay.prop('checked'),
// );
// GuestsPart.UpdateFreeBusy(this);
});
}
if (
($._data(this.end._$fakeField[0], 'events')?.change?.length ?? 0) <= 1
) {
this.end._$fakeField.on('change', () => {
cal.event_times_changed();
GuestsPart.UpdateDispos(
this.date_start,
this.date_end,
this.$allDay.prop('checked'),
);
GuestsPart.UpdateFreeBusy(this);
});
}
//debugger;
if (($._data(this.$allDay[0], 'events')?.change?.length ?? 0) === 0) {
this.$allDay.on('change', this._on_all_day_changed.bind(this));
}
// if ($._data(this._$end_date[0], 'events').change.length > 1) {
// delete $._data(this._$end_date[0], 'events').change[1];
// }
// if ($._data(this._$start_date[0], 'events').change.length > 1) {
// delete $._data(this._$start_date[0], 'events').change[1];
// }
this._$end_date
.off('change')
.datepicker('option', 'onSelect', this._end_date_updated.bind(this))
.change(this._end_date_updated.bind(this));
this._$start_date
.off('change')
.datepicker('option', 'onSelect', this._start_date_updated.bind(this))
.change(this._start_date_updated.bind(this));
this.$allDay.change();
if (this.date_start >= this.date_end) {
this._$end_date.change();
}
const _base_diff =
moment(
`${this._$end_date.val()} ${this.end._$fakeField.val()}`,
DATE_TIME_FORMAT,
) -
moment(
`${this._$start_date.val()} ${this.start._$fakeField.val()}`,
DATE_TIME_FORMAT,
); ///60/1000;
this.base_diff = _base_diff;
// Object.defineProperty(this, 'base_diff', {
// get() {
// return _base_diff;
// },
// });
}
/**
* Action à faire lorsque la date est invalide
* @param {external:jQuery} $field Champ concerné
* @param {TimePartManager.WORDS} word Fin ou début ?
* @returns {boolean}
* @package
*/
_invalid_action_date($field, word) {
if (!moment($field.val(), DATE_FORMAT).isValid()) {
rcmail.display_message(`Le date de ${word} n'est pas valide !`, 'error');
$field.focus();
return true;
}
return false;
}
/**
* Action à faire lorsque l'heure est invalide
* @param {TimePart} part Partie de l'heure concerné
* @param {TimePartManager.WORDS} word Fin ou début ?
* @returns {boolean}
* @package
*/
_invalid_action_time(part, word) {
if (!part.is_valid()) {
rcmail.display_message(`L'heure de ${word} n'est pas valide !`, 'error');
part._$fakeField.focus();
return true;
}
return false;
}
/**
* Si les champs ont les valeurs attendus
* @returns {boolean}
*/
is_valid() {
return (
this.start.is_valid() &&
this.end.is_valid() &&
moment(this._$start_date.val(), DATE_FORMAT).isValid() &&
moment(this._$end_date.val(), DATE_FORMAT).isValid()
);
}
/**
* Action à faire lorsque les champs sont invalides
*/
invalid_action() {
this._invalid_action_date(this._$start_date, TimePartManager.WORDS.start) ||
this._invalid_action_date(this._$end_date, TimePartManager.WORDS.end) ||
this._invalid_action_time(this.start, TimePartManager.WORDS.start) ||
this._invalid_action_time(this.end, TimePartManager.WORDS.end);
}
_update_diff() {
let end = moment(
`${this._$end_date.val()} ${this.end._$fakeField.val()}`,
DATE_TIME_FORMAT,
);
let start = moment(
`${this._$start_date.val()} ${this.start._$fakeField.val()}`,
DATE_TIME_FORMAT,
);
const tmp = end - start;
if (tmp > 0) this.base_diff = tmp < 0 ? -tmp : tmp;
}
_start_date_updated(update_dispos = true) {
let start = moment(
`${this._$start_date.val()} ${this.start._$fakeField.val()}`,
DATE_TIME_FORMAT,
);
let end = moment(start).add(this.base_diff);
this._$end_date.val(end.format(DATE_FORMAT));
if (update_dispos) {
GuestsPart.UpdateDispos(start, end, this.$allDay.prop('checked'));
GuestsPart.UpdateFreeBusy(this);
cal.event_times_changed();
}
}
_end_date_updated() {
let end = null;
this._update_diff();
if (this.date_end <= this.date_start) {
if (
moment(
`${this.date_end.format(DATE_FORMAT)} ${this.date_start.format(DATE_HOUR_FORMAT)}`,
DATE_TIME_FORMAT,
) >= this.date_start
) {
end = moment(
`${this.date_end.format(DATE_FORMAT)} ${this.date_start.format(DATE_HOUR_FORMAT)}`,
DATE_TIME_FORMAT,
).add(TimePart.INTERVAL, 'm');
} else this._start_date_updated(false);
}
if (!end) end = this.date_end;
this._reinit_end_date(end);
GuestsPart.UpdateDispos(
this.date_start,
this.date_end,
this.$allDay.prop('checked'),
);
GuestsPart.UpdateFreeBusy(this);
cal.event_times_changed();
}
_reinit_end_date(end) {
const is_same_day =
this.date_start.startOf('day').format(DATE_FORMAT) ===
moment(end).startOf('day').format(DATE_FORMAT);
this.end.reinit(
end,
this.base_diff,
is_same_day ? this.date_start.format(DATE_HOUR_FORMAT) : null,
);
}
/**
* Lorsque l'un des champs est modifié, mets à jours les champ de sauvegarde et réinitialise les selects si besoin.
* @package
* @returns {void}
*/
_update_date() {
//On évite de le faire plusieurs fois pour éviter les appels serveurs
if (this._update_date.started === true) return;
else this._update_date.started = true;
//Prendre en compte le champs personnalisé
if (
this.start._$fakeField.val() === '-1' ||
this.end._$fakeField.val() === '-1'
) {
this._update_date.started = false;
return;
}
let base_diff =
/* this.is_all_day &&
start.format() === moment(start).startOf('day').format()
? moment(start).endOf('day') - moment(start).startOf('day')
:*/ this.base_diff;
if (base_diff <= 0) base_diff = 3600 * 1000;
let start = moment(
`${this._$start_date.val()} ${this.start._$fakeField.val()}`,
DATE_TIME_FORMAT,
);
let end = moment(start).add(this.base_diff);
if (this._$end_date.val() !== end.format(DATE_FORMAT)) {
this._$end_date.val(end.format(DATE_FORMAT));
}
if (start >= end) {
//Gestion du cas ou la date de début dépasse la date de fin
end = moment(start).add(base_diff);
this.end.reinit(end, base_diff, start.format(DATE_HOUR_FORMAT));
this._$end_date.val(end.format(DATE_FORMAT)).change();
//Si la date n'éxiste pas, on la réjoute dans le select
if (!(this.end._$fakeField.val() || false)) {
end = start.add(1, 'd').startOf('d').add(base_diff);
this._$end_date.val(end.format(DATE_FORMAT));
this.end._$fakeField.val(end.format(DATE_HOUR_FORMAT));
this._update_date.started = false;
this._update_date();
}
} else {
//On réinitialise le select, seulement si on en a besoin
let is_same_day =
moment(start).startOf('day').format(DATE_FORMAT) ===
moment(end).startOf('day').format(DATE_FORMAT);
let need_reinit =
this.end._$fakeField.children().first().val() !==
(is_same_day ? start.format(DATE_HOUR_FORMAT) : '00:00');
if (need_reinit)
this.end.reinit(
end,
this.base_diff,
is_same_day ? start.format(DATE_HOUR_FORMAT) : null,
);
}
GuestsPart.UpdateDispos(start, end, this.$allDay.prop('checked'));
GuestsPart.UpdateFreeBusy(this);
this._update_date.started = false;
}
/**
* Lorsque la journée entière est coché ou décoché
* @param {Event} e
*/
_on_all_day_changed(e) {
if ($(e.currentTarget)[0].checked) {
this.start._$fakeField
.css('display', 'none')
.parent()
.parent()
.addClass(CLASS_ALL_DAY);
this.end._$fakeField
.css('display', 'none')
.parent()
.parent()
.addClass(CLASS_ALL_DAY);
} else {
this.start._$fakeField
.css('display', EMPTY_STRING)
.parent()
.parent()
.removeClass(CLASS_ALL_DAY);
this.end._$fakeField
.css('display', EMPTY_STRING)
.parent()
.parent()
.removeClass(CLASS_ALL_DAY);
}
}
/**
* Met à jour le select en ajoutant une option qui n'éxiste pas.
* @param {string} select Id du select
* @param {string} value Valeur à ajouter. Le format doit être 'HH:mm'
* @static
* @see {@link TimePart.UpdateOption}
*/
static UpdateOption(select, value) {
TimePart.UpdateOption(select, value);
}
}
/**
* Enumération qui contient les mots des actions invalides
* @static
* @enum {string}
* @readonly
*/
TimePartManager.WORDS = {
start: 'début',
end: 'fin',
};
/**
* @class
* @classdesc Gère la partie lié à l'heure
* @extends FakePart
* @frommodule EventView/Parts
*/
class TimePart extends FakePart {
/**
*
* @param {external:jQuery} $time_field Champ de sauvegarde
* @param {external:jQuery} $time_select Champ visuel
*/
constructor($time_field, $time_select) {
super($time_field, $time_select, Parts.MODE.change);
}
/**
* Intialise les champs
* @param {string} val Date au format 'HH:mm'
* @param {number} base_interval Interval entre la date de fin et la date de début
* @param {?string} min Date au format 'HH:mm'. Le select ne pourra pas avoir une valeur inférieur à cette date. Optionnel
* @override
*/
init(val, base_interval, min = null) {
if (this._$fakeField.children().length === 0) {
if (min) {
min = min.split(':');
min = moment().startOf('day').add(+min[0], 'H').add(+min[1], 'm');
}
let base = moment().startOf('day');
let next = moment().startOf('day').add(1, 'd');
let formatted_text = null;
while (base < next) {
if (!min || base > min) {
formatted_text = base.format(DATE_HOUR_FORMAT);
this._$fakeField.append(
MelHtml.start
.option({ value: formatted_text })
.text(formatted_text)
.end()
.generate(),
);
}
base = base.add(TimePart.INTERVAL, 'm');
}
}
//Gestion de la valeur du champ
val = this._toTimeMoment(val);
min = min ? this._toTimeMoment(min) : null;
//Si la valeur est inférieur à la valeur minimum, on ajoute l'interval de base
const formatted =
!!min && val <= min
? moment(min).add(base_interval).format(DATE_HOUR_FORMAT)
: val.format(DATE_HOUR_FORMAT);
TimePart.UpdateOption(this._$fakeField.attr('id'), formatted);
$(`#${this._$fakeField.attr('id')}`).val(formatted);
//Si la valeur du champ de sauvegarde est différente de la valeur du champ visuel, on met à jours le champ de sauvegarde
if (this._$field.val() !== $(`#${this._$fakeField.attr('id')}`).val()) {
this._$field.val(formatted);
}
//Ajout de l'option "Personalisé"
if (this._$fakeField.find('option[value="-1"]').length === 0) {
this._$fakeField.append(
MelHtml.start
.option({ value: -1 })
.text('Personnalisé')
.end()
.generate(),
);
}
}
/**
* Retourne une heure au format 'DD/MM/YYYY HH:mm' en objet `moment`en utilisant la date d'aujourd'hui pour extraire 'DD/MM/YYYY'.
* @param {string} val Date au format 'HH:mm'
* @returns {external:moment}
*/
_toTimeMoment(val) {
return moment(
`${moment().startOf('year').format(DATE_FORMAT)} ${val.format(DATE_HOUR_FORMAT)}`,
);
}
/**
* Vide le select puis appelle `init`
* @param {string} val Date au format 'HH:mm'
* @param {number} base_interval Interval entre la date de fin et la date de début
* @param {?string} min Date au format 'HH:mm'. Le select ne pourra pas avoir une valeur inférieur à cette date. Optionnel
*/
reinit(val, base_interval, min) {
this._$fakeField.empty();
this.init(val, base_interval, min);
}
/**
* Action qui sera appelé lorsque le champ change de valeur.
* @param {Event} e
* @override
*/
onChange(e) {
const val = $(e.currentTarget).val();
if (val === '-1') {
$(`#fake-${this._$fakeField.attr('id')}`)
.css('display', '')
.val(this._$field.val())
.off('change')
.on('change', (e) => {
const val = $(e.currentTarget).val();
if (val.includes(':') && moment(val, DATE_HOUR_FORMAT).isValid()) {
$(`#fake-${this._$fakeField.attr('id')}`)
.css('display', 'none')
.off('change');
this.init(moment(val, DATE_HOUR_FORMAT), TimePart.INTERVAL);
this._$fakeField.css('display', '').val(val).change();
}
});
this._$fakeField.css('display', 'none');
} else this._$field.val(val);
}
/**
* Si le champ est valide ou non
* @returns {boolean}
*/
is_valid() {
return this._$fakeField.val() !== EMPTY_STRING;
}
/**
* Met à jour le select en ajoutant une option qui n'éxiste pas.
* @param {string} select Id du select
* @param {string} value Valeur à ajouter. Le format doit être 'HH:mm'
* @static
*/
static UpdateOption(select, value) {
if ($(`#${select} [value="${value}"]`).length === 0) {
const current = moment(value, DATE_HOUR_FORMAT);
for (const e of $(`#${select} option`)) {
if (current < moment($(e).val(), DATE_HOUR_FORMAT)) {
$(e).before($('<option>').val(value).text(value));
break;
}
}
}
}
}
/**
* Interval de temps par défaut
* @static
* @readonly
* @type {number}
* @default 15
*/
TimePart.INTERVAL = 15;