2020-03-11 09:43:55 -04:00
|
|
|
// discourse-skip-module
|
2018-11-26 08:20:32 -05:00
|
|
|
(function($) {
|
2018-11-28 10:19:25 -05:00
|
|
|
const DATE_TEMPLATE = `
|
|
|
|
<span>
|
|
|
|
<svg class="fa d-icon d-icon-globe-americas svg-icon" xmlns="http://www.w3.org/2000/svg">
|
|
|
|
<use xlink:href="#globe-americas"></use>
|
|
|
|
</svg>
|
|
|
|
<span class="relative-time"></span>
|
|
|
|
</span>
|
|
|
|
`;
|
|
|
|
|
|
|
|
const PREVIEW_TEMPLATE = `
|
|
|
|
<div class='preview'>
|
|
|
|
<span class='timezone'></span>
|
|
|
|
<span class='date-time'></span>
|
|
|
|
</div>
|
|
|
|
`;
|
|
|
|
|
|
|
|
function processElement($element, options = {}) {
|
|
|
|
clearTimeout(this.timeout);
|
|
|
|
|
|
|
|
const utc = moment().utc();
|
|
|
|
const dateTime = options.time
|
|
|
|
? `${options.date} ${options.time}`
|
|
|
|
: options.date;
|
|
|
|
|
|
|
|
let displayedTimezone;
|
|
|
|
if (options.time) {
|
|
|
|
displayedTimezone = options.displayedTimezone || moment.tz.guess();
|
|
|
|
} else {
|
|
|
|
displayedTimezone =
|
|
|
|
options.displayedTimezone || options.timezone || moment.tz.guess();
|
|
|
|
}
|
|
|
|
|
|
|
|
// if timezone given we convert date and time from given zone to Etc/UTC
|
2019-11-25 17:32:24 -05:00
|
|
|
let utcDateTime;
|
2018-11-28 10:19:25 -05:00
|
|
|
if (options.timezone) {
|
|
|
|
utcDateTime = _applyZoneToDateTime(dateTime, options.timezone);
|
|
|
|
} else {
|
|
|
|
utcDateTime = moment.utc(dateTime);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (utcDateTime < utc) {
|
|
|
|
// if event is in the past we want to bump it no next occurrence when
|
|
|
|
// recurring is set
|
|
|
|
if (options.recurring) {
|
2019-11-25 17:32:24 -05:00
|
|
|
utcDateTime = _applyRecurrence(utcDateTime, options);
|
2018-11-26 08:20:32 -05:00
|
|
|
} else {
|
2018-11-28 10:19:25 -05:00
|
|
|
$element.addClass("past");
|
2018-11-26 08:20:32 -05:00
|
|
|
}
|
2018-11-28 10:19:25 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// once we have the correct UTC date we want
|
|
|
|
// we adjust it to watching user timezone
|
|
|
|
const adjustedDateTime = utcDateTime.tz(displayedTimezone);
|
|
|
|
|
|
|
|
const previews = _generatePreviews(
|
|
|
|
adjustedDateTime.clone(),
|
|
|
|
displayedTimezone,
|
|
|
|
options
|
|
|
|
);
|
|
|
|
const textPreview = _generateTextPreview(previews);
|
|
|
|
const htmlPreview = _generateHtmlPreview(previews);
|
|
|
|
|
|
|
|
const formatedDateTime = _applyFormatting(
|
|
|
|
adjustedDateTime,
|
|
|
|
displayedTimezone,
|
|
|
|
options
|
|
|
|
);
|
|
|
|
|
|
|
|
$element
|
|
|
|
.html(DATE_TEMPLATE)
|
2019-03-26 11:31:48 -04:00
|
|
|
.attr("aria-label", textPreview)
|
2019-03-26 11:34:27 -04:00
|
|
|
.attr(
|
|
|
|
"data-html-tooltip",
|
|
|
|
`<div class="locale-dates-previews">${htmlPreview}</div>`
|
|
|
|
)
|
2018-11-28 10:19:25 -05:00
|
|
|
.addClass("cooked-date")
|
|
|
|
.find(".relative-time")
|
|
|
|
.text(formatedDateTime);
|
|
|
|
|
2019-11-18 04:04:07 -05:00
|
|
|
this.timeout = setTimeout(
|
|
|
|
() => processElement($element, options),
|
|
|
|
60 * 1000
|
|
|
|
);
|
2018-11-28 10:19:25 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
function _formatTimezone(timezone) {
|
|
|
|
return timezone
|
|
|
|
.replace("_", " ")
|
|
|
|
.replace("Etc/", "")
|
|
|
|
.split("/");
|
|
|
|
}
|
|
|
|
|
|
|
|
function _zoneWithoutPrefix(timezone) {
|
|
|
|
const parts = _formatTimezone(timezone);
|
|
|
|
return parts[1] || parts[0];
|
|
|
|
}
|
|
|
|
|
|
|
|
function _applyZoneToDateTime(dateTime, timezone) {
|
|
|
|
return moment.tz(dateTime, timezone).utc();
|
|
|
|
}
|
|
|
|
|
|
|
|
function _translateCalendarKey(time, key) {
|
|
|
|
const translated = I18n.t(`discourse_local_dates.relative_dates.${key}`, {
|
|
|
|
time: "LT"
|
|
|
|
});
|
2018-11-26 08:20:32 -05:00
|
|
|
|
2018-11-28 10:19:25 -05:00
|
|
|
if (time) {
|
|
|
|
return translated
|
|
|
|
.split("LT")
|
|
|
|
.map(w => `[${w}]`)
|
|
|
|
.join("LT");
|
|
|
|
} else {
|
|
|
|
return `[${translated.replace(" LT", "")}]`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function _calendarFormats(time) {
|
|
|
|
return {
|
|
|
|
sameDay: _translateCalendarKey(time, "today"),
|
|
|
|
nextDay: _translateCalendarKey(time, "tomorrow"),
|
|
|
|
lastDay: _translateCalendarKey(time, "yesterday"),
|
|
|
|
sameElse: "L"
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
function _isEqualZones(timezoneA, timezoneB) {
|
2019-11-22 13:43:37 -05:00
|
|
|
if ((timezoneA || timezoneB) && (!timezoneA || !timezoneB)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2019-11-22 11:14:27 -05:00
|
|
|
if (timezoneA.includes(timezoneB) || timezoneB.includes(timezoneA)) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2018-11-28 10:19:25 -05:00
|
|
|
return (
|
|
|
|
moment.tz(timezoneA).utcOffset() === moment.tz(timezoneB).utcOffset()
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
function _applyFormatting(dateTime, displayedTimezone, options) {
|
2019-08-24 12:39:20 -04:00
|
|
|
if (options.countdown) {
|
|
|
|
const diffTime = dateTime.diff(moment());
|
|
|
|
if (diffTime < 0) {
|
|
|
|
return I18n.t("discourse_local_dates.relative_dates.countdown.passed");
|
|
|
|
} else {
|
|
|
|
return moment.duration(diffTime).humanize();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-11-28 10:19:25 -05:00
|
|
|
const sameTimezone = _isEqualZones(displayedTimezone, moment.tz.guess());
|
|
|
|
const inCalendarRange = dateTime.isBetween(
|
|
|
|
moment().subtract(2, "days"),
|
2018-12-28 17:47:16 -05:00
|
|
|
moment()
|
|
|
|
.add(1, "days")
|
|
|
|
.endOf("day")
|
2018-11-28 10:19:25 -05:00
|
|
|
);
|
|
|
|
|
|
|
|
if (options.calendar && inCalendarRange) {
|
|
|
|
if (sameTimezone) {
|
|
|
|
if (options.time) {
|
|
|
|
dateTime = dateTime.calendar(null, _calendarFormats(options.time));
|
2018-11-26 08:20:32 -05:00
|
|
|
} else {
|
2018-11-28 10:19:25 -05:00
|
|
|
dateTime = dateTime.calendar(null, _calendarFormats(null));
|
2018-11-26 08:20:32 -05:00
|
|
|
}
|
2018-11-28 10:19:25 -05:00
|
|
|
} else {
|
|
|
|
dateTime = dateTime.format(options.format);
|
|
|
|
dateTime = dateTime.replace("TZ", "");
|
|
|
|
dateTime = `${dateTime} (${_zoneWithoutPrefix(displayedTimezone)})`;
|
2018-11-26 08:20:32 -05:00
|
|
|
}
|
2018-11-28 10:19:25 -05:00
|
|
|
} else {
|
|
|
|
if (options.time) {
|
|
|
|
dateTime = dateTime.format(options.format);
|
2018-11-26 08:20:32 -05:00
|
|
|
|
2018-11-28 10:19:25 -05:00
|
|
|
if (options.displayedTimezone && !sameTimezone) {
|
|
|
|
dateTime = dateTime.replace("TZ", "");
|
|
|
|
dateTime = `${dateTime} (${_zoneWithoutPrefix(displayedTimezone)})`;
|
2018-11-26 08:20:32 -05:00
|
|
|
} else {
|
2018-11-28 10:19:25 -05:00
|
|
|
dateTime = dateTime.replace(
|
|
|
|
"TZ",
|
|
|
|
_formatTimezone(displayedTimezone).join(": ")
|
|
|
|
);
|
2018-11-26 08:20:32 -05:00
|
|
|
}
|
2018-11-28 10:19:25 -05:00
|
|
|
} else {
|
|
|
|
dateTime = dateTime.format(options.format);
|
2018-11-27 09:17:23 -05:00
|
|
|
|
2018-11-28 10:19:25 -05:00
|
|
|
if (!sameTimezone) {
|
2018-11-26 08:20:32 -05:00
|
|
|
dateTime = dateTime.replace("TZ", "");
|
2018-11-27 05:52:02 -05:00
|
|
|
dateTime = `${dateTime} (${_zoneWithoutPrefix(displayedTimezone)})`;
|
2018-11-26 08:20:32 -05:00
|
|
|
} else {
|
2018-11-28 10:19:25 -05:00
|
|
|
dateTime = dateTime.replace(
|
|
|
|
"TZ",
|
|
|
|
_zoneWithoutPrefix(displayedTimezone)
|
|
|
|
);
|
2018-11-26 08:20:32 -05:00
|
|
|
}
|
|
|
|
}
|
2018-11-28 10:19:25 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
return dateTime;
|
|
|
|
}
|
|
|
|
|
2019-11-25 17:32:24 -05:00
|
|
|
function _applyRecurrence(dateTime, { recurring, timezone }) {
|
2018-11-28 10:19:25 -05:00
|
|
|
const parts = recurring.split(".");
|
|
|
|
const count = parseInt(parts[0], 10);
|
|
|
|
const type = parts[1];
|
|
|
|
const diff = moment().diff(dateTime, type);
|
|
|
|
const add = Math.ceil(diff + count);
|
|
|
|
|
2019-11-25 17:32:24 -05:00
|
|
|
// we create new moment object from format
|
|
|
|
// to ensure it's created in user context
|
|
|
|
const wasDST = moment(dateTime.format()).isDST();
|
|
|
|
let dateTimeWithRecurrence = moment(dateTime).add(add, type);
|
|
|
|
const isDST = moment(dateTimeWithRecurrence.format()).isDST();
|
|
|
|
|
|
|
|
// these dates are more or less DST "certain"
|
|
|
|
const noDSTOffset = moment
|
|
|
|
.tz({ month: 0, day: 1 }, timezone || "Etc/UTC")
|
|
|
|
.utcOffset();
|
|
|
|
const withDSTOffset = moment
|
|
|
|
.tz({ month: 5, day: 1 }, timezone || "Etc/UTC")
|
|
|
|
.utcOffset();
|
|
|
|
|
|
|
|
// we remove the DST offset present when the date was created,
|
|
|
|
// and add current DST offset
|
2019-04-01 06:19:09 -04:00
|
|
|
if (!wasDST && isDST) {
|
2019-11-25 17:32:24 -05:00
|
|
|
dateTimeWithRecurrence.add(-withDSTOffset + noDSTOffset, "minutes");
|
2019-04-01 06:19:09 -04:00
|
|
|
}
|
|
|
|
|
2019-11-25 17:32:24 -05:00
|
|
|
// we add the DST offset present when the date was created,
|
|
|
|
// and remove current DST offset
|
2019-04-01 06:19:09 -04:00
|
|
|
if (wasDST && !isDST) {
|
2019-11-25 17:32:24 -05:00
|
|
|
dateTimeWithRecurrence.add(withDSTOffset - noDSTOffset, "minutes");
|
2019-04-01 06:19:09 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
return dateTimeWithRecurrence;
|
2018-11-28 10:19:25 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
function _createDateTimeRange(dateTime, timezone) {
|
2019-06-06 05:16:58 -04:00
|
|
|
const dt = moment(dateTime).tz(timezone);
|
|
|
|
|
2019-06-06 06:28:41 -04:00
|
|
|
return [dt.format("LLL"), "→", dt.add(24, "hours").format("LLL")].join(" ");
|
2018-11-28 10:19:25 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
function _generatePreviews(dateTime, displayedTimezone, options) {
|
|
|
|
const previewedTimezones = [];
|
|
|
|
const watchingUserTimezone = moment.tz.guess();
|
|
|
|
const timezones = options.timezones.filter(
|
2019-11-22 11:14:27 -05:00
|
|
|
timezone =>
|
|
|
|
!_isEqualZones(timezone, watchingUserTimezone) &&
|
|
|
|
!_isEqualZones(timezone, options.timezone)
|
2018-11-28 10:19:25 -05:00
|
|
|
);
|
|
|
|
|
2018-11-29 06:02:27 -05:00
|
|
|
previewedTimezones.push({
|
|
|
|
timezone: watchingUserTimezone,
|
|
|
|
current: true,
|
|
|
|
dateTime: options.time
|
2019-06-06 06:28:41 -04:00
|
|
|
? moment(dateTime)
|
|
|
|
.tz(watchingUserTimezone)
|
|
|
|
.format("LLL")
|
2018-11-29 06:02:27 -05:00
|
|
|
: _createDateTimeRange(dateTime, watchingUserTimezone)
|
|
|
|
});
|
2018-11-28 10:19:25 -05:00
|
|
|
|
|
|
|
if (
|
|
|
|
options.timezone &&
|
|
|
|
displayedTimezone === watchingUserTimezone &&
|
|
|
|
options.timezone !== displayedTimezone &&
|
|
|
|
!_isEqualZones(displayedTimezone, options.timezone)
|
|
|
|
) {
|
|
|
|
timezones.unshift(options.timezone);
|
|
|
|
}
|
|
|
|
|
2019-11-22 13:43:37 -05:00
|
|
|
Array.from(new Set(timezones.filter(Boolean))).forEach(timezone => {
|
2019-11-22 11:14:27 -05:00
|
|
|
if (_isEqualZones(timezone, displayedTimezone)) {
|
|
|
|
return;
|
|
|
|
}
|
2018-11-26 08:20:32 -05:00
|
|
|
|
2019-11-22 11:14:27 -05:00
|
|
|
if (_isEqualZones(timezone, watchingUserTimezone)) {
|
|
|
|
timezone = watchingUserTimezone;
|
|
|
|
}
|
2018-11-27 09:17:23 -05:00
|
|
|
|
2019-11-22 11:14:27 -05:00
|
|
|
previewedTimezones.push({
|
|
|
|
timezone,
|
|
|
|
dateTime: options.time
|
|
|
|
? moment(dateTime)
|
|
|
|
.tz(timezone)
|
|
|
|
.format("LLL")
|
|
|
|
: _createDateTimeRange(dateTime, timezone)
|
2018-11-27 09:17:23 -05:00
|
|
|
});
|
2019-11-22 11:14:27 -05:00
|
|
|
});
|
2018-11-27 09:17:23 -05:00
|
|
|
|
2018-11-28 10:19:25 -05:00
|
|
|
if (!previewedTimezones.length) {
|
|
|
|
previewedTimezones.push({
|
|
|
|
timezone: "Etc/UTC",
|
|
|
|
dateTime: options.time
|
2019-06-06 06:28:41 -04:00
|
|
|
? moment(dateTime)
|
|
|
|
.tz("Etc/UTC")
|
|
|
|
.format("LLL")
|
2018-11-28 10:19:25 -05:00
|
|
|
: _createDateTimeRange(dateTime, "Etc/UTC")
|
|
|
|
});
|
|
|
|
}
|
2018-11-26 08:20:32 -05:00
|
|
|
|
2018-11-28 10:19:25 -05:00
|
|
|
return _.uniq(previewedTimezones, "timezone");
|
|
|
|
}
|
2018-11-26 08:20:32 -05:00
|
|
|
|
2018-11-28 10:19:25 -05:00
|
|
|
function _generateTextPreview(previews) {
|
|
|
|
return previews
|
|
|
|
.map(preview => {
|
|
|
|
const formatedZone = _zoneWithoutPrefix(preview.timezone);
|
2018-11-26 08:20:32 -05:00
|
|
|
|
2018-11-28 10:19:25 -05:00
|
|
|
if (preview.dateTime.match(/TZ/)) {
|
|
|
|
return preview.dateTime.replace(/TZ/, formatedZone);
|
|
|
|
} else {
|
|
|
|
return `${formatedZone} ${preview.dateTime}`;
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.join(", ");
|
|
|
|
}
|
2018-11-26 08:20:32 -05:00
|
|
|
|
2018-11-28 10:19:25 -05:00
|
|
|
function _generateHtmlPreview(previews) {
|
|
|
|
return previews
|
|
|
|
.map(preview => {
|
|
|
|
const $template = $(PREVIEW_TEMPLATE);
|
2018-11-26 08:20:32 -05:00
|
|
|
|
|
|
|
if (preview.current) $template.addClass("current");
|
|
|
|
|
2018-11-27 05:52:02 -05:00
|
|
|
$template.find(".timezone").text(_zoneWithoutPrefix(preview.timezone));
|
2018-11-26 08:20:32 -05:00
|
|
|
$template.find(".date-time").text(preview.dateTime);
|
2018-11-28 10:19:25 -05:00
|
|
|
return $template[0].outerHTML;
|
|
|
|
})
|
|
|
|
.join("");
|
|
|
|
}
|
2018-11-26 08:20:32 -05:00
|
|
|
|
2018-11-28 10:19:25 -05:00
|
|
|
$.fn.applyLocalDates = function() {
|
2018-11-26 08:20:32 -05:00
|
|
|
return this.each(function() {
|
|
|
|
const $element = $(this);
|
|
|
|
|
|
|
|
const options = {};
|
|
|
|
options.time = $element.attr("data-time");
|
|
|
|
options.date = $element.attr("data-date");
|
|
|
|
options.recurring = $element.attr("data-recurring");
|
|
|
|
options.timezones = (
|
|
|
|
$element.attr("data-timezones") ||
|
|
|
|
Discourse.SiteSettings.discourse_local_dates_default_timezones ||
|
|
|
|
"Etc/UTC"
|
|
|
|
).split("|");
|
|
|
|
options.timezone = $element.attr("data-timezone");
|
|
|
|
options.calendar = ($element.attr("data-calendar") || "on") === "on";
|
|
|
|
options.displayedTimezone = $element.attr("data-displayed-timezone");
|
|
|
|
options.format =
|
|
|
|
$element.attr("data-format") || (options.time ? "LLL" : "LL");
|
2019-08-24 12:39:20 -04:00
|
|
|
options.countdown = $element.attr("data-countdown");
|
2018-11-26 08:20:32 -05:00
|
|
|
|
|
|
|
processElement($element, options);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
})(jQuery);
|