discourse/plugins/discourse-local-dates/assets/javascripts/lib/local-date-builder.js.es6
Krzysztof Kotlarek 82b7e34f30
FEATURE: local dates range on click (#14355)
This PR is introducing 2 changes.
1. Date popup is displayed on click instead on hover
2. If the range is given then the whole range is always displayed for both startDate and endDate
3. For range, short time is displayed for end if the range is < 24 hours
2021-09-20 09:23:18 +10:00

295 lines
8.1 KiB
JavaScript

import DateWithZoneHelper from "./date-with-zone-helper";
import I18n from "I18n";
import { renderIcon } from "discourse-common/lib/icon-library";
const DATETIME_FORMAT = "LLL";
const DATE_FORMAT = "LL";
const FULL_DATETIME_FORMAT = "LLLL";
const TIME_FORMAT = "h:mm A";
const DAY_OF_THE_WEEK_FORMAT = "dddd";
const RANGE_SEPARATOR = "→";
const TIME_ICON = "clock";
const SHORT_FORMAT_DAYS_PERIOD = 1;
export default class LocalDateBuilder {
constructor(params = {}, localTimezone) {
this.time = params.time;
this.date = params.date;
this.recurring = params.recurring;
this.timezones = Array.from(
new Set((params.timezones || []).filter(Boolean))
);
this.timezone = params.timezone || "UTC";
this.calendar =
typeof params.calendar === "undefined" ? true : params.calendar;
this.displayedTimezone = params.displayedTimezone;
this.format = params.format || (this.time ? DATETIME_FORMAT : DATE_FORMAT);
this.countdown = params.countdown;
this.duration = params.duration;
this.localTimezone = localTimezone;
}
build() {
const [year, month, day] = this.date.split("-").map((x) => parseInt(x, 10));
const [hour, minute, second] = (this.time || "")
.split(":")
.map((x) => (x ? parseInt(x, 10) : undefined));
let displayedTimezone;
if (this.time) {
displayedTimezone = this.displayedTimezone || this.localTimezone;
} else {
displayedTimezone =
this.displayedTimezone || this.timezone || this.localTimezone;
}
let localDate = new DateWithZoneHelper({
year,
month: month ? month - 1 : null,
day,
hour,
minute,
second,
timezone: this.timezone,
localTimezone: this.localTimezone,
});
if (this.recurring && moment().isAfter(localDate.datetime)) {
const type = this.recurring.split(".")[1];
const repetitionsForType = localDate.unitRepetitionsBetweenDates(
this.recurring,
moment.tz(this.localTimezone)
);
localDate = localDate.add(repetitionsForType, type);
}
const previews = this._generatePreviews(localDate, displayedTimezone);
return {
pastEvent:
!this.recurring &&
moment.tz(this.localTimezone).isAfter(localDate.datetime),
formated: this._applyFormatting(localDate, displayedTimezone),
previews,
textPreview: this._generateTextPreviews(previews),
};
}
_generateTextPreviews(previews) {
return previews
.map((preview) => {
const formatedZone = this._zoneWithoutPrefix(preview.timezone);
return `${formatedZone} ${preview.formated}`;
})
.join(", ");
}
_generatePreviews(localDate, displayedTimezone) {
const previewedTimezones = [];
const timezones = this.timezones.filter(
(timezone) => !this._isEqualZones(timezone, this.localTimezone)
);
previewedTimezones.push({
timezone: this._zoneWithoutPrefix(this.localTimezone),
current: true,
formated: this._createDateTimeRange(
DateWithZoneHelper.fromDatetime(
localDate.datetime,
localDate.timezone,
this.localTimezone
),
this.time,
this.duration
),
});
if (
this.timezone &&
displayedTimezone === this.localTimezone &&
this.timezone !== displayedTimezone &&
!this._isEqualZones(displayedTimezone, this.timezone) &&
!this.timezones.any((t) => this._isEqualZones(t, this.timezone))
) {
timezones.unshift(this.timezone);
}
timezones.forEach((timezone) => {
if (this._isEqualZones(timezone, displayedTimezone)) {
return;
}
if (this._isEqualZones(timezone, this.localTimezone)) {
timezone = this.localTimezone;
}
previewedTimezones.push({
timezone: this._zoneWithoutPrefix(timezone),
formated: this._createDateTimeRange(
DateWithZoneHelper.fromDatetime(
localDate.datetime,
localDate.timezone,
timezone
),
this.time,
this.duration
),
});
});
return previewedTimezones.uniqBy("timezone");
}
_isEqualZones(timezoneA, timezoneB) {
if ((timezoneA || timezoneB) && (!timezoneA || !timezoneB)) {
return false;
}
if (timezoneA.includes(timezoneB) || timezoneB.includes(timezoneA)) {
return true;
}
return (
moment.tz(timezoneA).utcOffset() === moment.tz(timezoneB).utcOffset()
);
}
_createDateTimeRange(startRange, time, duration) {
const [startDate, endDate] = this._calculateDatesForRange(
startRange,
time,
duration
);
let formatElements = [
startDate.format(`${DAY_OF_THE_WEEK_FORMAT}, ${DATE_FORMAT}`),
this._optionalTimeIcon(startDate, endDate),
startDate.format(TIME_FORMAT),
];
if (endDate) {
formatElements = formatElements.concat([
RANGE_SEPARATOR,
endDate.format(this._endDateFormat(startDate, endDate)),
]);
}
return formatElements.filter(Boolean).join(" ");
}
_shortFormat(startDate, endDate) {
return (
endDate.datetime.diff(startDate.datetime, "days") <
SHORT_FORMAT_DAYS_PERIOD
);
}
_optionalTimeIcon(startDate, endDate) {
if (!endDate || this._shortFormat(startDate, endDate)) {
return `<br />${renderIcon("string", TIME_ICON)}`;
}
}
_endDateFormat(startDate, endDate) {
return this._shortFormat(startDate, endDate)
? TIME_FORMAT
: FULL_DATETIME_FORMAT;
}
_calculateDatesForRange(date, time, duration) {
// if a time has been given we do not attempt to automatically create a range
// instead we show only one date with a format showing the time
if (time && !duration) {
return [date];
}
const dates = [
date,
duration ? date.add(duration, "minutes") : date.add(24, "hours"),
];
return duration < 0 ? dates.reverse() : dates;
}
_applyFormatting(localDate, displayedTimezone) {
if (this.countdown) {
const diffTime = moment.tz(this.localTimezone).diff(localDate.datetime);
if (diffTime < 0) {
return moment.duration(diffTime).humanize();
} else {
return I18n.t("discourse_local_dates.relative_dates.countdown.passed");
}
}
const sameTimezone = this._isEqualZones(
displayedTimezone,
this.localTimezone
);
if (this.calendar) {
const inCalendarRange = moment
.tz(this.localTimezone)
.isBetween(
localDate.subtract(2, "day").datetime,
localDate.add(1, "day").datetime.endOf("day")
);
if (inCalendarRange && sameTimezone) {
const date = localDate.datetimeWithZone(this.localTimezone);
if (date.hours() === 0 && date.minutes() === 0) {
return date.format("dddd");
}
return date.calendar(
moment.tz(localDate.timezone),
this._calendarFormats(this.time ? this.time : null)
);
}
}
if (!sameTimezone) {
return this._formatWithZone(localDate, displayedTimezone, this.format);
}
return localDate.format(this.format);
}
_calendarFormats(time) {
return {
sameDay: this._translateCalendarKey(time, "today"),
nextDay: this._translateCalendarKey(time, "tomorrow"),
lastDay: this._translateCalendarKey(time, "yesterday"),
sameElse: "L",
};
}
_translateCalendarKey(time, key) {
const translated = I18n.t(`discourse_local_dates.relative_dates.${key}`, {
time: "LT",
});
if (time) {
return translated
.split("LT")
.map((w) => `[${w}]`)
.join("LT");
} else {
return `[${translated.replace(" LT", "")}]`;
}
}
_formatTimezone(timezone) {
return timezone.replace("_", " ").replace("Etc/", "").split("/");
}
_zoneWithoutPrefix(timezone) {
const [part1, part2] = this._formatTimezone(timezone);
return part2 || part1;
}
_formatWithZone(localDate, displayedTimezone, format) {
let formated = localDate.datetimeWithZone(displayedTimezone).format(format);
return `${formated} (${this._zoneWithoutPrefix(displayedTimezone)})`;
}
}