DEV: uses popperjs for positioning user and group card (#20063)
Behavior should be very similar but the code is simplified and it should fix various bugs where the card was showing out of screen even if we had available space.
This commit is contained in:
parent
18f7b47ecb
commit
335c3f4621
|
@ -2,12 +2,13 @@ import { alias, match } from "@ember/object/computed";
|
||||||
import { schedule, throttle } from "@ember/runloop";
|
import { schedule, throttle } from "@ember/runloop";
|
||||||
import DiscourseURL from "discourse/lib/url";
|
import DiscourseURL from "discourse/lib/url";
|
||||||
import Mixin from "@ember/object/mixin";
|
import Mixin from "@ember/object/mixin";
|
||||||
import afterTransition from "discourse/lib/after-transition";
|
|
||||||
import { escapeExpression } from "discourse/lib/utilities";
|
import { escapeExpression } from "discourse/lib/utilities";
|
||||||
import { inject as service } from "@ember/service";
|
import { inject as service } from "@ember/service";
|
||||||
import { wantsNewWindow } from "discourse/lib/intercept-click";
|
import { wantsNewWindow } from "discourse/lib/intercept-click";
|
||||||
import { bind } from "discourse-common/utils/decorators";
|
import { bind } from "discourse-common/utils/decorators";
|
||||||
import discourseLater from "discourse-common/lib/later";
|
import discourseLater from "discourse-common/lib/later";
|
||||||
|
import { createPopper } from "@popperjs/core";
|
||||||
|
import { headerOffset } from "discourse/lib/offset-calculator";
|
||||||
|
|
||||||
const DEFAULT_SELECTOR = "#main-outlet";
|
const DEFAULT_SELECTOR = "#main-outlet";
|
||||||
|
|
||||||
|
@ -27,6 +28,7 @@ export default Mixin.create({
|
||||||
elementId: null, //click detection added for data-{elementId}
|
elementId: null, //click detection added for data-{elementId}
|
||||||
triggeringLinkClass: null, //the <a> classname where this card should appear
|
triggeringLinkClass: null, //the <a> classname where this card should appear
|
||||||
_showCallback: null, //username, $target - load up data for when show is called, should call this._positionCard($target) when it's done.
|
_showCallback: null, //username, $target - load up data for when show is called, should call this._positionCard($target) when it's done.
|
||||||
|
_popperReference: null,
|
||||||
|
|
||||||
postStream: alias("topic.postStream"),
|
postStream: alias("topic.postStream"),
|
||||||
viewingTopic: match("router.currentRouteName", /^topic\./),
|
viewingTopic: match("router.currentRouteName", /^topic\./),
|
||||||
|
@ -36,7 +38,6 @@ export default Mixin.create({
|
||||||
loading: null,
|
loading: null,
|
||||||
cardTarget: null,
|
cardTarget: null,
|
||||||
post: null,
|
post: null,
|
||||||
isFixed: false,
|
|
||||||
isDocked: false,
|
isDocked: false,
|
||||||
|
|
||||||
_show(username, target, event) {
|
_show(username, target, event) {
|
||||||
|
@ -58,6 +59,10 @@ export default Mixin.create({
|
||||||
|
|
||||||
const currentUsername = this.username;
|
const currentUsername = this.username;
|
||||||
if (username === currentUsername || this.loading === username) {
|
if (username === currentUsername || this.loading === username) {
|
||||||
|
// prevents opacity flasing when clicking on same trigger
|
||||||
|
if (username !== currentUsername) {
|
||||||
|
this.element.dataset.popperPlacement = "";
|
||||||
|
}
|
||||||
this._positionCard($(target));
|
this._positionCard($(target));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -100,7 +105,7 @@ export default Mixin.create({
|
||||||
|
|
||||||
didInsertElement() {
|
didInsertElement() {
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
afterTransition($(this.element), this._hide);
|
|
||||||
const id = this.elementId;
|
const id = this.elementId;
|
||||||
const triggeringLinkClass = this.triggeringLinkClass;
|
const triggeringLinkClass = this.triggeringLinkClass;
|
||||||
const previewClickEvent = `click.discourse-preview-${id}-${triggeringLinkClass}`;
|
const previewClickEvent = `click.discourse-preview-${id}-${triggeringLinkClass}`;
|
||||||
|
@ -168,7 +173,7 @@ export default Mixin.create({
|
||||||
},
|
},
|
||||||
|
|
||||||
_topicHeaderTrigger(username, target) {
|
_topicHeaderTrigger(username, target) {
|
||||||
this.setProperties({ isFixed: true, isDocked: true });
|
this.setProperties({ isDocked: true });
|
||||||
return this._show(username, target);
|
return this._show(username, target);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -188,110 +193,81 @@ export default Mixin.create({
|
||||||
},
|
},
|
||||||
|
|
||||||
_previewClick($target) {
|
_previewClick($target) {
|
||||||
this.set("isFixed", true);
|
|
||||||
return this._show($target.text().replace(/^@/, ""), $target);
|
return this._show($target.text().replace(/^@/, ""), $target);
|
||||||
},
|
},
|
||||||
|
|
||||||
_positionCard(target) {
|
_positionCard(target) {
|
||||||
const rtl = $("html").css("direction") === "rtl";
|
|
||||||
if (!target) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const width = $(this.element).width();
|
|
||||||
const height = 175;
|
|
||||||
const isFixed = this.isFixed;
|
|
||||||
const isDocked = this.isDocked;
|
|
||||||
|
|
||||||
let verticalAdjustments = 0;
|
|
||||||
|
|
||||||
schedule("afterRender", () => {
|
schedule("afterRender", () => {
|
||||||
if (target) {
|
if (!target) {
|
||||||
if (!this.site.mobileView) {
|
return;
|
||||||
let position = target.offset();
|
}
|
||||||
if (target.parents(".d-header").length > 0) {
|
|
||||||
position.top = target.position().top;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (position) {
|
if (this.site.desktopView) {
|
||||||
position.bottom = "unset";
|
const avatarOverflowSize = 44;
|
||||||
|
this._popperReference = createPopper(target[0], this.element, {
|
||||||
|
placement: "right",
|
||||||
|
modifiers: [
|
||||||
|
{
|
||||||
|
name: "preventOverflow",
|
||||||
|
options: {
|
||||||
|
padding: {
|
||||||
|
top: headerOffset() + avatarOverflowSize,
|
||||||
|
right: 10,
|
||||||
|
bottom: 10,
|
||||||
|
left: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ name: "eventListeners", enabled: false },
|
||||||
|
{ name: "offset", options: { offset: [10, 10] } },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
document.querySelector(".card-cloak")?.classList.remove("hidden");
|
||||||
|
this._popperReference = createPopper(target[0], this.element, {
|
||||||
|
modifiers: [
|
||||||
|
{ name: "eventListeners", enabled: false },
|
||||||
|
{
|
||||||
|
name: "computeStyles",
|
||||||
|
enabled: true,
|
||||||
|
fn({ state }) {
|
||||||
|
// mimics our modal top of the screen positioning
|
||||||
|
state.styles.popper = {
|
||||||
|
...state.styles.popper,
|
||||||
|
position: "fixed",
|
||||||
|
left: `${
|
||||||
|
(window.innerWidth - state.rects.popper.width) / 2
|
||||||
|
}px`,
|
||||||
|
top: "10%",
|
||||||
|
transform: "translateY(-10%)",
|
||||||
|
};
|
||||||
|
|
||||||
if (rtl) {
|
return state;
|
||||||
// The site direction is rtl
|
},
|
||||||
position.right = $(window).width() - position.left + 10;
|
},
|
||||||
position.left = "auto";
|
],
|
||||||
let overage = $(window).width() - 50 - (position.right + width);
|
});
|
||||||
if (overage < 0) {
|
}
|
||||||
position.right += overage;
|
|
||||||
position.top += target.height() + 48;
|
|
||||||
verticalAdjustments += target.height() + 48;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// The site direction is ltr
|
|
||||||
position.left += target.width() + 10;
|
|
||||||
|
|
||||||
let overage = $(window).width() - 50 - (position.left + width);
|
this.element.classList.toggle("docked-card", this.isDocked);
|
||||||
if (overage < 0) {
|
|
||||||
position.left += overage;
|
|
||||||
position.top += target.height() + 48;
|
|
||||||
verticalAdjustments += target.height() + 48;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// It looks better to have the card aligned slightly higher
|
// After the card is shown, focus on the first link
|
||||||
position.top -= 24;
|
//
|
||||||
|
// note: we DO NOT use afterRender here cause _positionCard may
|
||||||
if (isFixed) {
|
// run afterwards, if we allowed this to happen the usercard
|
||||||
position.top -= $("html").scrollTop();
|
// may be offscreen and we may scroll all the way to it on focus
|
||||||
//if content is fixed and will be cut off on the bottom, display it above...
|
if (event.pointerId === -1) {
|
||||||
if (
|
discourseLater(() => {
|
||||||
position.top + height + verticalAdjustments >
|
this.element.querySelector("a")?.focus();
|
||||||
$(window).height() - 50
|
}, 350);
|
||||||
) {
|
|
||||||
position.bottom =
|
|
||||||
$(window).height() -
|
|
||||||
(target.offset().top - $("html").scrollTop());
|
|
||||||
if (verticalAdjustments > 0) {
|
|
||||||
position.bottom += 48;
|
|
||||||
}
|
|
||||||
position.top = "unset";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const avatarOverflowSize = 44;
|
|
||||||
if (isDocked && position.top < avatarOverflowSize) {
|
|
||||||
position.top = avatarOverflowSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
$(this.element).css(position);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.site.mobileView) {
|
|
||||||
$(".card-cloak").removeClass("hidden");
|
|
||||||
let position = target.offset();
|
|
||||||
position.top = "10%"; // match modal behaviour
|
|
||||||
position.left = 0;
|
|
||||||
$(this.element).css(position);
|
|
||||||
}
|
|
||||||
$(this.element).toggleClass("docked-card", isDocked);
|
|
||||||
|
|
||||||
// After the card is shown, focus on the first link
|
|
||||||
//
|
|
||||||
// note: we DO NOT use afterRender here cause _positionCard may
|
|
||||||
// run afterwards, if we allowed this to happen the usercard
|
|
||||||
// may be offscreen and we may scroll all the way to it on focus
|
|
||||||
if (event.pointerId === -1) {
|
|
||||||
discourseLater(() => {
|
|
||||||
const firstLink = this.element.querySelector("a");
|
|
||||||
firstLink && firstLink.focus();
|
|
||||||
}, 350);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@bind
|
@bind
|
||||||
_hide() {
|
_hide() {
|
||||||
|
this.element.dataset.popperPlacement = "";
|
||||||
|
|
||||||
if (!this.visible) {
|
if (!this.visible) {
|
||||||
$(this.element).css({ left: -9999, top: -9999 });
|
$(this.element).css({ left: -9999, top: -9999 });
|
||||||
if (this.site.mobileView) {
|
if (this.site.mobileView) {
|
||||||
|
@ -307,7 +283,6 @@ export default Mixin.create({
|
||||||
loading: null,
|
loading: null,
|
||||||
cardTarget: null,
|
cardTarget: null,
|
||||||
post: null,
|
post: null,
|
||||||
isFixed: false,
|
|
||||||
isDocked: false,
|
isDocked: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -37,14 +37,17 @@ $avatar_margin: -50px; // negative margin makes avatars extend above cards
|
||||||
color: var(--primary);
|
color: var(--primary);
|
||||||
background: var(--secondary) center center;
|
background: var(--secondary) center center;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
transition: opacity 0.2s, transform 0.2s;
|
|
||||||
opacity: 0;
|
|
||||||
outline: 2px solid transparent;
|
outline: 2px solid transparent;
|
||||||
@include transform(scale(0.9));
|
|
||||||
&.show {
|
&[data-popper-placement] {
|
||||||
opacity: 1;
|
opacity: 0;
|
||||||
@include transform(scale(1));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&[data-popper-placement]:not([data-popper-placement=""]) {
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
.card-content {
|
.card-content {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
background: rgba(var(--secondary-rgb), 0.85);
|
background: rgba(var(--secondary-rgb), 0.85);
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
// shared styles for user and group cards
|
// shared styles for user and group cards
|
||||||
.user-card,
|
.user-card,
|
||||||
.group-card {
|
.group-card {
|
||||||
position: absolute;
|
|
||||||
z-index: z("usercard");
|
z-index: z("usercard");
|
||||||
&.fixed {
|
&.fixed {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|
|
@ -3,7 +3,6 @@ $avatar_width: 120px;
|
||||||
// shared styles for user and group cards
|
// shared styles for user and group cards
|
||||||
.user-card,
|
.user-card,
|
||||||
.group-card {
|
.group-card {
|
||||||
position: fixed;
|
|
||||||
// mobile cards should always be on top of everything - 1102
|
// mobile cards should always be on top of everything - 1102
|
||||||
z-index: z("mobile-composer") + 2;
|
z-index: z("mobile-composer") + 2;
|
||||||
max-width: 95vw;
|
max-width: 95vw;
|
||||||
|
|
Loading…
Reference in New Issue