This repository has been archived on 2023-03-18. You can view files and clone it, but cannot push or open issues or pull requests.
osr-discourse-src/app/assets/javascripts/discourse/components/site-header.js.es6
Kyle Zhao 9b10a78d82 FEATURE: Quick access panels in user menu (#8073)
* Extract QuickAccessPanel from UserNotifications.

* FEATURE: Quick access panels in user menu.

This feature adds quick access panels for bookmarks and personal
messages. It allows uses to browse recent items directly in the user
menu, without being redirected to the full pages.

* REFACTOR: Use QuickAccessItem for messages.

Reusing `DefaultNotificationItem` feels nice but it actually requires a
lot of extra work that is not needed for a quick access item.

Also, `DefaultNotificationItem` shows an incorrect tooptip ("unread
private message"), and it is not trivial to remove / override that.

* Use a plain JS object instead.

An Ember object was required when `DefaultNotificationItem` was used.

* Prefix instead suffix `_` for private helpers.

* Set to null instead of deleting object keys.

JavaScript engines can optimize object property access based on the
object’s shape. https://mathiasbynens.be/notes/shapes-ics

* Change trivial try/catch to one-liners.

* Return the promise in case needs to be waited on.

* Refactor showAll to a link with href

* Store `emptyStatePlaceholderItemText` in state.

* Store items in Session singleton instead.

We can drop `staleItems` (and `findStaleItems`) altogether. Because
`(old) items === staleItems` when switching back to a quick access
panel.

* Add `limit` parameter to the `user_actions` API.

* Explicitly import Session instead.
2019-09-09 11:03:57 -04:00

384 lines
11 KiB
JavaScript

import MountWidget from "discourse/components/mount-widget";
import { observes } from "ember-addons/ember-computed-decorators";
import Docking from "discourse/mixins/docking";
import PanEvents, {
SWIPE_VELOCITY,
SWIPE_DISTANCE_THRESHOLD,
SWIPE_VELOCITY_THRESHOLD
} from "discourse/mixins/pan-events";
const PANEL_BODY_MARGIN = 30;
const SiteHeaderComponent = MountWidget.extend(Docking, PanEvents, {
widget: "header",
docAt: null,
dockedHeader: null,
_animate: false,
_isPanning: false,
_panMenuOrigin: "right",
_panMenuOffset: 0,
_scheduledMovingAnimation: null,
_scheduledRemoveAnimate: null,
_topic: null,
@observes(
"currentUser.unread_notifications",
"currentUser.unread_private_messages",
"currentUser.reviewable_count"
)
notificationsChanged() {
this.queueRerender();
},
_animateOpening($panel) {
$panel.css({ right: "", left: "" });
this._panMenuOffset = 0;
},
_animateClosing($panel, menuOrigin, windowWidth) {
$panel.css(menuOrigin, -windowWidth);
this._animate = true;
Ember.run.schedule("afterRender", () => {
this.eventDispatched("dom:clean", "header");
this._panMenuOffset = 0;
});
},
_isRTL() {
return $("html").css("direction") === "rtl";
},
_leftMenuClass() {
return this._isRTL() ? ".user-menu" : ".hamburger-panel";
},
_leftMenuAction() {
return this._isRTL() ? "toggleUserMenu" : "toggleHamburger";
},
_rightMenuAction() {
return this._isRTL() ? "toggleHamburger" : "toggleUserMenu";
},
_handlePanDone(offset, event) {
const $window = $(window);
const windowWidth = parseInt($window.width());
const $menuPanels = $(".menu-panel");
const menuOrigin = this._panMenuOrigin;
this._shouldMenuClose(event, menuOrigin)
? (offset += SWIPE_VELOCITY)
: (offset -= SWIPE_VELOCITY);
$menuPanels.each((idx, panel) => {
const $panel = $(panel);
const $headerCloak = $(".header-cloak");
$panel.css(menuOrigin, -offset);
$headerCloak.css("opacity", Math.min(0.5, (300 - offset) / 600));
if (offset > windowWidth) {
this._animateClosing($panel, menuOrigin, windowWidth);
} else if (offset <= 0) {
this._animateOpening($panel);
} else {
//continue to open or close menu
this._scheduledMovingAnimation = window.requestAnimationFrame(() =>
this._handlePanDone(offset, event)
);
}
});
},
_shouldMenuClose(e, menuOrigin) {
// menu should close after a pan either:
// if a user moved the panel closed past a threshold and away and is NOT swiping back open
// if a user swiped to close fast enough regardless of distance
if (menuOrigin === "right") {
return (
(e.deltaX > SWIPE_DISTANCE_THRESHOLD &&
e.velocityX > -SWIPE_VELOCITY_THRESHOLD) ||
e.velocityX > 0
);
} else {
return (
(e.deltaX < -SWIPE_DISTANCE_THRESHOLD &&
e.velocityX < SWIPE_VELOCITY_THRESHOLD) ||
e.velocityX < 0
);
}
},
panStart(e) {
const center = e.center;
const $centeredElement = $(document.elementFromPoint(center.x, center.y));
if (
($centeredElement.hasClass("panel-body") ||
$centeredElement.hasClass("header-cloak") ||
$centeredElement.parents(".panel-body").length) &&
(e.direction === "left" || e.direction === "right")
) {
e.originalEvent.preventDefault();
this._isPanning = true;
} else {
this._isPanning = false;
}
},
panEnd(e) {
if (!this._isPanning) {
return;
}
this._isPanning = false;
$(".menu-panel").each((idx, panel) => {
const $panel = $(panel);
let offset = $panel.css("right");
if (this._panMenuOrigin === "left") {
offset = $panel.css("left");
}
offset = Math.abs(parseInt(offset, 10));
this._handlePanDone(offset, e);
});
},
panMove(e) {
if (!this._isPanning) {
return;
}
const $menuPanels = $(".menu-panel");
$menuPanels.each((idx, panel) => {
const $panel = $(panel);
const $headerCloak = $(".header-cloak");
if (this._panMenuOrigin === "right") {
const pxClosed = Math.min(0, -e.deltaX + this._panMenuOffset);
$panel.css("right", pxClosed);
$headerCloak.css("opacity", Math.min(0.5, (300 + pxClosed) / 600));
} else {
const pxClosed = Math.min(0, e.deltaX + this._panMenuOffset);
$panel.css("left", pxClosed);
$headerCloak.css("opacity", Math.min(0.5, (300 + pxClosed) / 600));
}
});
},
dockCheck(info) {
const $header = $("header.d-header");
if (this.docAt === null) {
if (!($header && $header.length === 1)) return;
this.docAt = $header.offset().top;
}
const $body = $("body");
const offset = info.offset();
if (offset >= this.docAt) {
if (!this.dockedHeader) {
$body.addClass("docked");
this.dockedHeader = true;
}
} else {
if (this.dockedHeader) {
$body.removeClass("docked");
this.dockedHeader = false;
}
}
},
setTopic(topic) {
this.eventDispatched("dom:clean", "header");
this._topic = topic;
this.queueRerender();
},
willRender() {
if (this.get("currentUser.staff")) {
$("body").addClass("staff");
}
},
didInsertElement() {
this._super(...arguments);
$(window).on("resize.discourse-menu-panel", () => this.afterRender());
this.appEvents.on("header:show-topic", this, "setTopic");
this.appEvents.on("header:hide-topic", this, "setTopic");
this.dispatch("notifications:changed", "user-notifications");
this.dispatch("header:keyboard-trigger", "header");
this.dispatch("search-autocomplete:after-complete", "search-term");
this.appEvents.on("dom:clean", this, "_cleanDom");
},
_cleanDom() {
// For performance, only trigger a re-render if any menu panels are visible
if (this.element.querySelector(".menu-panel")) {
this.eventDispatched("dom:clean", "header");
}
},
willDestroyElement() {
this._super(...arguments);
$("body").off("keydown.header");
$(window).off("resize.discourse-menu-panel");
this.appEvents.off("header:show-topic", this, "setTopic");
this.appEvents.off("header:hide-topic", this, "setTopic");
this.appEvents.off("dom:clean", this, "_cleanDom");
Ember.run.cancel(this._scheduledRemoveAnimate);
window.cancelAnimationFrame(this._scheduledMovingAnimation);
},
buildArgs() {
return {
topic: this._topic,
canSignUp: this.canSignUp
};
},
afterRender() {
const $menuPanels = $(".menu-panel");
if ($menuPanels.length === 0) {
if (this.site.mobileView) {
this._animate = true;
}
return;
}
const $window = $(window);
const windowWidth = parseInt($window.width());
const headerWidth = $("#main-outlet .container").width() || 1100;
const remaining = parseInt((windowWidth - headerWidth) / 2);
const viewMode = remaining < 50 ? "slide-in" : "drop-down";
$menuPanels.each((idx, panel) => {
const $panel = $(panel);
const $headerCloak = $(".header-cloak");
let width = parseInt($panel.attr("data-max-width") || 300);
if (windowWidth - width < 50) {
width = windowWidth - 50;
}
if (this._panMenuOffset) {
this._panMenuOffset = -width;
}
$panel.removeClass("drop-down slide-in").addClass(viewMode);
if (this._animate || this._panMenuOffset !== 0) {
$headerCloak.css("opacity", 0);
if (
this.site.mobileView &&
$panel.parent(this._leftMenuClass()).length > 0
) {
this._panMenuOrigin = "left";
$panel.css("left", -windowWidth);
} else {
this._panMenuOrigin = "right";
$panel.css("right", -windowWidth);
}
}
const $panelBody = $(".panel-body", $panel);
// 2 pixel fudge allows for firefox subpixel sizing stuff causing scrollbar
let contentHeight =
parseInt($(".panel-body-contents", $panel).height()) + 2;
// We use a mutationObserver to check for style changes, so it's important
// we don't set it if it doesn't change. Same goes for the $panelBody!
const style = $panel.prop("style");
if (viewMode === "drop-down") {
const $buttonPanel = $("header ul.icons");
if ($buttonPanel.length === 0) {
return;
}
// These values need to be set here, not in the css file - this is to deal with the
// possibility of the window being resized and the menu changing from .slide-in to .drop-down.
if (style.top !== "100%" || style.height !== "auto") {
$panel.css({ top: "100%", height: "auto" });
}
// adjust panel height
const fullHeight = parseInt($window.height());
const offsetTop = $panel.offset().top;
const scrollTop = $window.scrollTop();
if (
contentHeight + (offsetTop - scrollTop) + PANEL_BODY_MARGIN >
fullHeight ||
this.site.mobileView
) {
contentHeight =
fullHeight - (offsetTop - scrollTop) - PANEL_BODY_MARGIN;
}
if ($panelBody.height() !== contentHeight) {
$panelBody.height(contentHeight);
}
$("body").addClass("drop-down-mode");
} else {
if (this.site.mobileView) {
$headerCloak.show();
}
const menuTop = this.site.mobileView ? headerTop() : headerHeight();
let height;
const winHeightOffset = 16;
let initialWinHeight = window.innerHeight
? window.innerHeight
: $(window).height();
const winHeight = initialWinHeight - winHeightOffset;
if (menuTop + contentHeight < winHeight && !this.site.mobileView) {
height = contentHeight + "px";
} else {
height = winHeight - menuTop;
}
if ($panelBody.prop("style").height !== "100%") {
$panelBody.height("100%");
}
if (style.top !== menuTop + "px" || style.height !== height) {
$panel.css({ top: menuTop + "px", height });
$(".header-cloak").css({ top: menuTop + "px" });
}
$("body").removeClass("drop-down-mode");
}
$panel.width(width);
if (this._animate) {
$panel.addClass("animate");
$headerCloak.addClass("animate");
this._scheduledRemoveAnimate = Ember.run.later(() => {
$panel.removeClass("animate");
$headerCloak.removeClass("animate");
}, 200);
}
$panel.css({ right: "", left: "" });
$headerCloak.css("opacity", 0.5);
this._animate = false;
});
}
});
export default SiteHeaderComponent;
export function headerHeight() {
const $header = $("header.d-header");
// Header may not exist in tests (e.g. in the user menu component test).
if ($header.length === 0) {
return 0;
}
const headerOffset = $header.offset();
const headerOffsetTop = headerOffset ? headerOffset.top : 0;
return parseInt(
$header.outerHeight() + headerOffsetTop - $(window).scrollTop()
);
}
export function headerTop() {
const $header = $("header.d-header");
const headerOffset = $header.offset();
const headerOffsetTop = headerOffset ? headerOffset.top : 0;
return parseInt(headerOffsetTop - $(window).scrollTop());
}