diff --git a/app/assets/javascripts/discourse/app/components/site-header.js b/app/assets/javascripts/discourse/app/components/site-header.js index 0a6b8aa4f0e..d66f49fca0c 100644 --- a/app/assets/javascripts/discourse/app/components/site-header.js +++ b/app/assets/javascripts/discourse/app/components/site-header.js @@ -6,404 +6,387 @@ import PanEvents, { import { cancel, later, schedule } from "@ember/runloop"; import Docking from "discourse/mixins/docking"; import MountWidget from "discourse/components/mount-widget"; -import { isTesting } from "discourse-common/config/environment"; +import RerenderOnDoNotDisturbChange from "discourse/mixins/rerender-on-do-not-disturb-change"; import { observes } from "discourse-common/utils/decorators"; import { topicTitleDecorators } from "discourse/components/topic-title"; -const SiteHeaderComponent = MountWidget.extend(Docking, PanEvents, { - widget: "header", - docAt: null, - dockedHeader: null, - _listenToDoNotDisturbLoop: null, - _animate: false, - _isPanning: false, - _panMenuOrigin: "right", - _panMenuOffset: 0, - _scheduledMovingAnimation: null, - _scheduledRemoveAnimate: null, - _topic: null, +const SiteHeaderComponent = MountWidget.extend( + Docking, + PanEvents, + RerenderOnDoNotDisturbChange, + { + 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_high_priority_notifications", - "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; - 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 = $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"); - } - }, - - listenForDoNotDisturbChanges() { - if (this.currentUser && !this.currentUser.isInDoNotDisturb()) { + @observes( + "currentUser.unread_notifications", + "currentUser.unread_high_priority_notifications", + "currentUser.reviewable_count" + ) + notificationsChanged() { this.queueRerender(); - } else { - cancel(this._listenToDoNotDisturbLoop); - this._listenToDoNotDisturbLoop = later( - this, - () => { - this.listenForDoNotDisturbChanges(); - }, - 10000 - ); - } - }, + }, - didInsertElement() { - this._super(...arguments); - $(window).on("resize.discourse-menu-panel", () => this.afterRender()); + _animateOpening($panel) { + $panel.css({ right: "", left: "" }); + this._panMenuOffset = 0; + }, - this.appEvents.on("header:show-topic", this, "setTopic"); - this.appEvents.on("header:hide-topic", this, "setTopic"); - this.appEvents.on("do-not-disturb:changed", () => this.queueRerender()); + _animateClosing($panel, menuOrigin, windowWidth) { + $panel.css(menuOrigin, -windowWidth); + this._animate = true; + schedule("afterRender", () => { + this.eventDispatched("dom:clean", "header"); + this._panMenuOffset = 0; + }); + }, - if (!isTesting()) { - this.listenForDoNotDisturbChanges(); - } + _isRTL() { + return $("html").css("direction") === "rtl"; + }, - this.dispatch("notifications:changed", "user-notifications"); - this.dispatch("header:keyboard-trigger", "header"); - this.dispatch("search-autocomplete:after-complete", "search-term"); + _leftMenuClass() { + return this._isRTL() ? ".user-menu" : ".hamburger-panel"; + }, - this.appEvents.on("dom:clean", this, "_cleanDom"); + _leftMenuAction() { + return this._isRTL() ? "toggleUserMenu" : "toggleHamburger"; + }, - // Allow first notification to be dismissed on a click anywhere - if ( - this.currentUser && - !this.get("currentUser.read_first_notification") && - !this.get("currentUser.enforcedSecondFactor") - ) { - this._dismissFirstNotification = (e) => { - if ( - !e.target.closest("#current-user") && - !e.target.closest(".ring-backdrop") && - this.currentUser && - !this.get("currentUser.read_first_notification") && - !this.get("currentUser.enforcedSecondFactor") - ) { - this.eventDispatched( - "header:dismiss-first-notification-mask", - "header" + _rightMenuAction() { + return this._isRTL() ? "toggleHamburger" : "toggleUserMenu"; + }, + + _handlePanDone(offset, event) { + const $window = $(window); + const windowWidth = $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) ); } - }; - document.addEventListener("click", this._dismissFirstNotification, { - once: true, }); - } - }, + }, - _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"); - - cancel(this._scheduledRemoveAnimate); - cancel(this._listenToDoNotDisturbLoop); - window.cancelAnimationFrame(this._scheduledMovingAnimation); - - document.removeEventListener("click", this._dismissFirstNotification); - }, - - buildArgs() { - return { - topic: this._topic, - canSignUp: this.canSignUp, - }; - }, - - afterRender() { - const headerTitle = document.querySelector(".header-title .topic-link"); - if (headerTitle && this._topic) { - topicTitleDecorators.forEach((cb) => - cb(this._topic, headerTitle, "header-title") - ); - } - - const $menuPanels = $(".menu-panel"); - if ($menuPanels.length === 0) { - if (this.site.mobileView) { - this._animate = true; + _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 + ); } - return; - } + }, - const $window = $(window); - const windowWidth = $window.width(); - - const headerWidth = $("#main-outlet .container").width() || 1100; - const remaining = (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"), 10) || 300; - if (windowWidth - width < 50) { - width = windowWidth - 50; - } - if (this._panMenuOffset) { - this._panMenuOffset = -width; + 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; } + }, - $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); + 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)); + } + }); + }, - const $panelBody = $(".panel-body", $panel); + dockCheck(info) { + const $header = $("header.d-header"); - // 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) { + if (this.docAt === null) { + if (!($header && $header.length === 1)) { return; } + this.docAt = $header.offset().top; + } - // 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" }); + const $body = $("body"); + const offset = info.offset(); + if (offset >= this.docAt) { + if (!this.dockedHeader) { + $body.addClass("docked"); + this.dockedHeader = true; } - - $("body").addClass("drop-down-mode"); } else { - if (this.site.mobileView) { - $headerCloak.show(); + if (this.dockedHeader) { + $body.removeClass("docked"); + this.dockedHeader = false; } + } + }, - const menuTop = this.site.mobileView ? headerTop() : headerHeight(); + setTopic(topic) { + this.eventDispatched("dom:clean", "header"); + this._topic = topic; + this.queueRerender(); + }, - const winHeightOffset = 16; - let initialWinHeight = window.innerHeight - ? window.innerHeight - : $(window).height(); - const winHeight = initialWinHeight - winHeightOffset; + willRender() { + if (this.get("currentUser.staff")) { + $("body").addClass("staff"); + } + }, - let height; - if (this.site.mobileView) { - height = winHeight - menuTop; - } + didInsertElement() { + this._super(...arguments); + $(window).on("resize.discourse-menu-panel", () => this.afterRender()); - const isIPadApp = document.body.classList.contains("footer-nav-ipad"), - heightProp = isIPadApp ? "max-height" : "height", - iPadOffset = 10; + this.appEvents.on("header:show-topic", this, "setTopic"); + this.appEvents.on("header:hide-topic", this, "setTopic"); - if (isIPadApp) { - height = winHeight - menuTop - iPadOffset; - } + this.dispatch("notifications:changed", "user-notifications"); + this.dispatch("header:keyboard-trigger", "header"); + this.dispatch("search-autocomplete:after-complete", "search-term"); - if ($panelBody.prop("style").height !== "100%") { - $panelBody.height("100%"); - } - if (style.top !== menuTop + "px" || style[heightProp] !== height) { - $panel.css({ top: menuTop + "px", [heightProp]: height }); - $(".header-cloak").css({ top: menuTop + "px" }); - } - $("body").removeClass("drop-down-mode"); + this.appEvents.on("dom:clean", this, "_cleanDom"); + + // Allow first notification to be dismissed on a click anywhere + if ( + this.currentUser && + !this.get("currentUser.read_first_notification") && + !this.get("currentUser.enforcedSecondFactor") + ) { + this._dismissFirstNotification = (e) => { + if ( + !e.target.closest("#current-user") && + !e.target.closest(".ring-backdrop") && + this.currentUser && + !this.get("currentUser.read_first_notification") && + !this.get("currentUser.enforcedSecondFactor") + ) { + this.eventDispatched( + "header:dismiss-first-notification-mask", + "header" + ); + } + }; + document.addEventListener("click", this._dismissFirstNotification, { + once: true, + }); + } + }, + + _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"); + + cancel(this._scheduledRemoveAnimate); + window.cancelAnimationFrame(this._scheduledMovingAnimation); + + document.removeEventListener("click", this._dismissFirstNotification); + }, + + buildArgs() { + return { + topic: this._topic, + canSignUp: this.canSignUp, + }; + }, + + afterRender() { + const headerTitle = document.querySelector(".header-title .topic-link"); + if (headerTitle && this._topic) { + topicTitleDecorators.forEach((cb) => + cb(this._topic, headerTitle, "header-title") + ); } - $panel.width(width); - if (this._animate) { - $panel.addClass("animate"); - $headerCloak.addClass("animate"); - this._scheduledRemoveAnimate = later(() => { - $panel.removeClass("animate"); - $headerCloak.removeClass("animate"); - }, 200); + const $menuPanels = $(".menu-panel"); + if ($menuPanels.length === 0) { + if (this.site.mobileView) { + this._animate = true; + } + return; } - $panel.css({ right: "", left: "" }); - $headerCloak.css("opacity", 0.5); - this._animate = false; - }); - }, -}); + + const $window = $(window); + const windowWidth = $window.width(); + + const headerWidth = $("#main-outlet .container").width() || 1100; + const remaining = (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"), 10) || 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); + + // 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" }); + } + + $("body").addClass("drop-down-mode"); + } else { + if (this.site.mobileView) { + $headerCloak.show(); + } + + const menuTop = this.site.mobileView ? headerTop() : headerHeight(); + + const winHeightOffset = 16; + let initialWinHeight = window.innerHeight + ? window.innerHeight + : $(window).height(); + const winHeight = initialWinHeight - winHeightOffset; + + let height; + if (this.site.mobileView) { + height = winHeight - menuTop; + } + + const isIPadApp = document.body.classList.contains("footer-nav-ipad"), + heightProp = isIPadApp ? "max-height" : "height", + iPadOffset = 10; + + if (isIPadApp) { + height = winHeight - menuTop - iPadOffset; + } + + if ($panelBody.prop("style").height !== "100%") { + $panelBody.height("100%"); + } + if (style.top !== menuTop + "px" || style[heightProp] !== height) { + $panel.css({ top: menuTop + "px", [heightProp]: 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 = later(() => { + $panel.removeClass("animate"); + $headerCloak.removeClass("animate"); + }, 200); + } + $panel.css({ right: "", left: "" }); + $headerCloak.css("opacity", 0.5); + this._animate = false; + }); + }, + } +); export default SiteHeaderComponent.extend({ classNames: ["d-header-wrap"], diff --git a/app/assets/javascripts/discourse/app/mixins/rerender-on-do-not-disturb-change.js b/app/assets/javascripts/discourse/app/mixins/rerender-on-do-not-disturb-change.js new file mode 100644 index 00000000000..dd73d650f1c --- /dev/null +++ b/app/assets/javascripts/discourse/app/mixins/rerender-on-do-not-disturb-change.js @@ -0,0 +1,36 @@ +import { cancel, later } from "@ember/runloop"; +import Mixin from "@ember/object/mixin"; +import { isTesting } from "discourse-common/config/environment"; + +export default Mixin.create({ + _listenToDoNotDisturbLoop: null, + + listenForDoNotDisturbChanges() { + if (this.currentUser && !this.currentUser.isInDoNotDisturb()) { + this.queueRerender(); + } else { + cancel(this._listenToDoNotDisturbLoop); + this._listenToDoNotDisturbLoop = later( + this, + () => { + this.listenForDoNotDisturbChanges(); + }, + 10000 + ); + } + }, + + didInsertElement() { + this._super(...arguments); + + this.appEvents.on("do-not-disturb:changed", () => this.queueRerender()); + if (!isTesting()) { + this.listenForDoNotDisturbChanges(); + } + }, + + willDestroyElement() { + this._super(...arguments); + cancel(this._listenToDoNotDisturbLoop); + }, +});