DEV: Plugin API to add card listener elements (#13887)

DEV: Plugin API to add card listener elements (#13887)

diff --git a/app/assets/javascripts/discourse/app/components/group-card-contents.js b/app/assets/javascripts/discourse/app/components/group-card-contents.js
index 9c31693..b53ae55 100644
--- a/app/assets/javascripts/discourse/app/components/group-card-contents.js
+++ b/app/assets/javascripts/discourse/app/components/group-card-contents.js
@@ -11,7 +11,7 @@ const maxMembersToDisplay = 10;
 
 export default Component.extend(CardContentsBase, CleansUp, {
   elementId: "group-card",
-  triggeringLinkClass: "mention-group",
+  mentionSelector: "a.mention-group",
   classNames: ["no-bg", "group-card"],
   classNameBindings: [
     "visible:show",
diff --git a/app/assets/javascripts/discourse/app/components/user-card-contents.js b/app/assets/javascripts/discourse/app/components/user-card-contents.js
index 9981135..3c64c2f 100644
--- a/app/assets/javascripts/discourse/app/components/user-card-contents.js
+++ b/app/assets/javascripts/discourse/app/components/user-card-contents.js
@@ -16,7 +16,9 @@ import { prioritizeNameInUx } from "discourse/lib/settings";
 export default Component.extend(CardContentsBase, CanCheckEmails, CleansUp, {
   elementId: "user-card",
   classNames: "user-card",
-  triggeringLinkClass: "mention",
+  avatarSelector: "[data-user-card]",
+  avatarDataAttrKey: "userCard",
+  mentionSelector: "a.mention",
   classNameBindings: [
     "visible:show",
     "showBadges",
diff --git a/app/assets/javascripts/discourse/app/lib/intercept-click.js b/app/assets/javascripts/discourse/app/lib/intercept-click.js
index 7c80745..a9fe69f 100644
--- a/app/assets/javascripts/discourse/app/lib/intercept-click.js
+++ b/app/assets/javascripts/discourse/app/lib/intercept-click.js
@@ -2,7 +2,8 @@ import DiscourseURL from "discourse/lib/url";
 
 export function wantsNewWindow(e) {
   return (
-    e.isDefaultPrevented() ||
+    e.defaultPrevented ||
+    (e.isDefaultPrevernted && e.isDefaultPrevented()) ||
     e.shiftKey ||
     e.metaKey ||
     e.ctrlKey ||
diff --git a/app/assets/javascripts/discourse/app/lib/plugin-api.js b/app/assets/javascripts/discourse/app/lib/plugin-api.js
index 5ffbaff..71d4905 100644
--- a/app/assets/javascripts/discourse/app/lib/plugin-api.js
+++ b/app/assets/javascripts/discourse/app/lib/plugin-api.js
@@ -37,6 +37,7 @@ import DiscourseBanner from "discourse/components/discourse-banner";
 import KeyboardShortcuts from "discourse/lib/keyboard-shortcuts";
 import Sharing from "discourse/lib/sharing";
 import { addAdvancedSearchOptions } from "discourse/components/search-advanced-options";
+import { addCardClickListenerSelector } from "discourse/mixins/card-contents-base";
 import { addCategorySortCriteria } from "discourse/components/edit-category-settings";
 import { addDecorator } from "discourse/widgets/post-cooked";
 import { addDiscoveryQueryParam } from "discourse/controllers/discovery-sortable";
@@ -1085,6 +1086,15 @@ class PluginApi {
   }
 
   /**
+   * Card contents mixin will add a listener to elements matching this selector
+   * that will open card contents when a mention of div with the correct data attribute
+   * is clicked
+   */
+  addCardClickListenerSelector(selector) {
+    addCardClickListenerSelector(selector);
+  }
+
+  /**
    * Registers a renderer that overrides the display of category links.
    *
    * Example:
diff --git a/app/assets/javascripts/discourse/app/mixins/card-contents-base.js b/app/assets/javascripts/discourse/app/mixins/card-contents-base.js
index 2b67cd1..1eafd02 100644
--- a/app/assets/javascripts/discourse/app/mixins/card-contents-base.js
+++ b/app/assets/javascripts/discourse/app/mixins/card-contents-base.js
@@ -8,6 +8,11 @@ import headerOutletHeights from "discourse/lib/header-outlet-height";
 import { inject as service } from "@ember/service";
 import { wantsNewWindow } from "discourse/lib/intercept-click";
 
+let _cardClickListenerSelectors = ["#main-outlet"];
+export function addCardClickListenerSelector(selector) {
+  _cardClickListenerSelectors.push(selector);
+}
+
 export default Mixin.create({
   router: service(),
 
@@ -26,7 +31,7 @@ export default Mixin.create({
   isFixed: false,
   isDocked: false,
 
-  _show(username, $target) {
+  _show(username, target) {
     // No user card for anon
     if (this.siteSettings.hide_user_profiles_from_public && !this.currentUser) {
       return false;
@@ -35,9 +40,9 @@ export default Mixin.create({
     username = escapeExpression(username.toString());
 
     // Don't show if nested
-    if ($target.parents(".card-content").length) {
+    if (target.closest(".card-content")) {
       this._close();
-      DiscourseURL.routeTo($target.attr("href"));
+      DiscourseURL.routeTo(target.href);
       return false;
     }
 
@@ -46,10 +51,10 @@ export default Mixin.create({
       return;
     }
 
-    const postId = $target.parents("article").data("post-id");
+    const closestArticle = target.closest("article");
+    const postId = closestArticle ? closestArticle.dataset["post-id"] : null;
     const wasVisible = this.visible;
     const previousTarget = this.cardTarget;
-    const target = $target[0];
 
     if (wasVisible) {
       this._close();
@@ -69,7 +74,7 @@ export default Mixin.create({
       post,
     });
 
-    this._showCallback(username, $target);
+    this._showCallback(username, $(target));
 
     // We bind scrolling on mobile after cards are shown to hide them if user scrolls
     if (this.site.mobileView) {
@@ -85,15 +90,12 @@ export default Mixin.create({
     const id = this.elementId;
     const triggeringLinkClass = this.triggeringLinkClass;
     const clickOutsideEventName = `mousedown.outside-${id}`;
-    const clickDataExpand = `click.discourse-${id}`;
-    const clickMention = `click.discourse-${id}-${triggeringLinkClass}`;
     const previewClickEvent = `click.discourse-preview-${id}-${triggeringLinkClass}`;
     const mobileScrollEvent = "scroll.mobile-card-cloak";
 
     this.setProperties({
+      boundCardClickHandler: this._cardClickHandler.bind(this),
       clickOutsideEventName,
-      clickDataExpand,
-      clickMention,
       previewClickEvent,
       mobileScrollEvent,
     });
@@ -117,20 +119,10 @@ export default Mixin.create({
         return true;
       });
 
-    $("#main-outlet").on(clickDataExpand, `[data-${id}]`, (e) => {
-      if (wantsNewWindow(e)) {
-        return;
-      }
-      const $target = $(e.currentTarget);
-      return this._show($target.data(id), $target);
-    });
-
-    $("#main-outlet").on(clickMention, `a.${triggeringLinkClass}`, (e) => {
-      if (wantsNewWindow(e)) {
-        return;
-      }
-      const $target = $(e.currentTarget);
-      return this._show($target.text().replace(/^@/, ""), $target);
+    _cardClickListenerSelectors.forEach((selector) => {
+      document
+        .querySelector(selector)
+        .addEventListener("click", this.boundCardClickHandler);
     });
 
     this.appEvents.on(previewClickEvent, this, "_previewClick");
@@ -142,6 +134,41 @@ export default Mixin.create({
     );
   },
 
+  _cardClickHandler(event) {
+    if (this.avatarSelector) {
+      let matched = this._showCardOnClick(
+        event,
+        this.avatarSelector,
+        (el) => el.dataset[this.avatarDataAttrKey]
+      );
+
+      if (matched) {
+        return; // Don't need to check for mention click; it's an avatar click
+      }
+    }
+
+    // Mention click
+    this._showCardOnClick(event, this.mentionSelector, (el) =>
+      el.innerText.replace(/^@/, "")
+    );
+  },
+
+  _showCardOnClick(event, selector, transformText) {
+    let matchingEl = event.target.closest(selector);
+    if (matchingEl) {
+      if (wantsNewWindow(event)) {
+        return true;
+      }
+
+      event.preventDefault();
+      event.stopPropagation();
+      return this._show(transformText(matchingEl), matchingEl);
+    }
+    {
+      return false;
+    }
+  },
+
   _topicHeaderTrigger(username, $target) {
     this.setProperties({ isFixed: true, isDocked: true });

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

GitHub sha: 91456ad2cbbf767f71aedffb6b556617df64342f

This commit appears in #13887 which was approved by eviltrout. It was merged by markvanlan.

Typo? I think this should be:

- (e.isDefaultPrevernted && e.isDefaultPrevented()) ||
+ (e.isDefaultPrevented && e.isDefaultPrevented()) ||