FEATURE: local dates range on click (#14355)

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
diff --git a/plugins/discourse-local-dates/assets/javascripts/initializers/discourse-local-dates.js.es6 b/plugins/discourse-local-dates/assets/javascripts/initializers/discourse-local-dates.js.es6
index 30c1b5c..50d543c 100644
--- a/plugins/discourse-local-dates/assets/javascripts/initializers/discourse-local-dates.js.es6
+++ b/plugins/discourse-local-dates/assets/javascripts/initializers/discourse-local-dates.js.es6
@@ -39,6 +39,11 @@ export function applyLocalDates(dates, siteSettings) {
 function buildOptionsFromElement(element, siteSettings) {
   const opts = {};
   const dataset = element.dataset;
+
+  if (_rangeElements(element).length === 2) {
+    opts.duration = _calculateDuration(element);
+  }
+
   opts.time = dataset.time;
   opts.date = dataset.date;
   opts.recurring = dataset.recurring;
@@ -57,6 +62,12 @@ function buildOptionsFromElement(element, siteSettings) {
   return opts;
 }
 
+function _rangeElements(element) {
+  return Array.from(element.parentElement.children).filter(
+    (span) => span.dataset.date
+  );
+}
+
 function initializeDiscourseLocalDates(api) {
   const siteSettings = api.container.lookup("site-settings:main");
 
@@ -114,7 +125,7 @@ function buildHtmlPreview(element, siteSettings) {
 
     const dateTimeNode = document.createElement("span");
     dateTimeNode.classList.add("date-time");
-    dateTimeNode.innerText = preview.formated;
+    dateTimeNode.innerHTML = preview.formated;
     previewNode.appendChild(dateTimeNode);
 
     return previewNode;
@@ -127,6 +138,20 @@ function buildHtmlPreview(element, siteSettings) {
   return previewsNode.outerHTML;
 }
 
+function _calculateDuration(element) {
+  const [startDataset, endDataset] = _rangeElements(element).map(
+    (dateElement) => dateElement.dataset
+  );
+  const startDateTime = moment(
+    `${startDataset.date} ${startDataset.time || ""}`
+  );
+  const endDateTime = moment(`${endDataset.date} ${endDataset.time || ""}`);
+  const duration = endDateTime.diff(startDateTime, "minutes");
+
+  // negative duration is used when we calculate difference for end date from range
+  return element.dataset === startDataset ? duration : -duration;
+}
+
 export default {
   name: "discourse-local-dates",
 
@@ -138,9 +163,13 @@ export default {
 
     const siteSettings = owner.lookup("site-settings:main");
     if (event?.target?.classList?.contains("discourse-local-date")) {
-      showPopover(event, {
-        htmlContent: buildHtmlPreview(event.target, siteSettings),
-      });
+      if ($(document.getElementById("d-popover"))[0]) {
+        hidePopover(event);
+      } else {
+        showPopover(event, {
+          htmlContent: buildHtmlPreview(event.target, siteSettings),
+        });
+      }
     }
   },
 
@@ -155,7 +184,6 @@ export default {
     router.on("routeWillChange", hidePopover);
 
     window.addEventListener("click", this.showDatePopover);
-    window.addEventListener("mouseover", this.showDatePopover);
     window.addEventListener("mouseout", this.hideDatePopover);
 
     const siteSettings = container.lookup("site-settings:main");
@@ -174,7 +202,6 @@ export default {
 
   teardown() {
     window.removeEventListener("click", this.showDatePopover);
-    window.removeEventListener("mouseover", this.showDatePopover);
     window.removeEventListener("mouseout", this.hideDatePopover);
   },
 };
diff --git a/plugins/discourse-local-dates/assets/javascripts/lib/local-date-builder.js.es6 b/plugins/discourse-local-dates/assets/javascripts/lib/local-date-builder.js.es6
index 915d87c..d266134 100644
--- a/plugins/discourse-local-dates/assets/javascripts/lib/local-date-builder.js.es6
+++ b/plugins/discourse-local-dates/assets/javascripts/lib/local-date-builder.js.es6
@@ -1,9 +1,15 @@
 import DateWithZoneHelper from "./date-with-zone-helper";
 import I18n from "I18n";
+import { renderIcon } from "discourse-common/lib/icon-library";
 
-const TIME_FORMAT = "LLL";
+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) {
@@ -17,8 +23,9 @@ export default class LocalDateBuilder {
     this.calendar =
       typeof params.calendar === "undefined" ? true : params.calendar;
     this.displayedTimezone = params.displayedTimezone;
-    this.format = params.format || (this.time ? TIME_FORMAT : DATE_FORMAT);
+    this.format = params.format || (this.time ? DATETIME_FORMAT : DATE_FORMAT);
     this.countdown = params.countdown;
+    this.duration = params.duration;
     this.localTimezone = localTimezone;
   }
 
@@ -95,7 +102,8 @@ export default class LocalDateBuilder {
           localDate.timezone,
           this.localTimezone
         ),
-        this.time
+        this.time,
+        this.duration
       ),
     });
 
@@ -126,7 +134,8 @@ export default class LocalDateBuilder {
             localDate.timezone,
             timezone
           ),
-          this.time
+          this.time,
+          this.duration
         ),
       });
     });
@@ -148,19 +157,56 @@ export default class LocalDateBuilder {
     );
   }
 
-  _createDateTimeRange(startRange, time) {
+  _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) {
-      return startRange.format(TIME_FORMAT);
-    } else {
-      const endRange = startRange.add(24, "hours");
-      return [
-        startRange.format("LLLL"),
-        RANGE_SEPARATOR,
-        endRange.format("LLLL"),
-      ].join(" ");
+    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) {
diff --git a/plugins/discourse-local-dates/test/javascripts/lib/local-date-builder-test.js.es6 b/plugins/discourse-local-dates/test/javascripts/lib/local-date-builder-test.js.es6
index 9764dac..f01c289 100644
--- a/plugins/discourse-local-dates/test/javascripts/lib/local-date-builder-test.js.es6
+++ b/plugins/discourse-local-dates/test/javascripts/lib/local-date-builder-test.js.es6
@@ -448,12 +448,45 @@ test("previews", function (assert) {
 
   freezeTime({ date: "2020-03-22", timezone: PARIS }, () => {
     assert.buildsCorrectDate(
+      { duration: 90, timezone: PARIS, timezones: [PARIS] },
+      {
+        previews: [
+          {
+            current: true,
+            formated:

[... diff too long, it was truncated ...]

GitHub sha: 82b7e34f308f672e262a698233747ebcca345dfd

This commit appears in #14355 which was approved by eviltrout. It was merged by lis2.