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 9c316932c09..b53ae55b1cf 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 99811350d01..3c64c2fc839 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 7c807456b29..a9fe69f7f02 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 5ffbaff63b8..71d49053140 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"; @@ -1084,6 +1085,15 @@ class PluginApi { addCategorySortCriteria(criteria); } + /** + * 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. * 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 2b67cd1bde7..1eafd024807 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 }); return this._show(username, $target); @@ -302,12 +329,14 @@ export default Mixin.create({ willDestroyElement() { this._super(...arguments); const clickOutsideEventName = this.clickOutsideEventName; - const clickDataExpand = this.clickDataExpand; - const clickMention = this.clickMention; const previewClickEvent = this.previewClickEvent; $("html").off(clickOutsideEventName); - $("#main").off(clickDataExpand).off(clickMention); + _cardClickListenerSelectors.forEach((selector) => { + document + .querySelector(selector) + .removeEventListener("click", this.boundCardClickHandler); + }); this.appEvents.off(previewClickEvent, this, "_previewClick");