Full height swipe-able menus (#6566)
* Feature: Full height swipe enabled menus support pan events on iphone
This commit is contained in:
parent
688755baf2
commit
71d8807fec
|
@ -1,6 +1,11 @@
|
||||||
import MountWidget from "discourse/components/mount-widget";
|
import MountWidget from "discourse/components/mount-widget";
|
||||||
import { observes } from "ember-addons/ember-computed-decorators";
|
import { observes } from "ember-addons/ember-computed-decorators";
|
||||||
import Docking from "discourse/mixins/docking";
|
import Docking from "discourse/mixins/docking";
|
||||||
|
import PanEvents, {
|
||||||
|
SWIPE_VELOCITY,
|
||||||
|
SWIPE_DISTANCE_THRESHOLD,
|
||||||
|
SWIPE_VELOCITY_THRESHOLD
|
||||||
|
} from "discourse/mixins/pan-events";
|
||||||
|
|
||||||
const _flagProperties = [];
|
const _flagProperties = [];
|
||||||
function addFlagProperty(prop) {
|
function addFlagProperty(prop) {
|
||||||
|
@ -9,10 +14,20 @@ function addFlagProperty(prop) {
|
||||||
|
|
||||||
const PANEL_BODY_MARGIN = 30;
|
const PANEL_BODY_MARGIN = 30;
|
||||||
|
|
||||||
const SiteHeaderComponent = MountWidget.extend(Docking, {
|
//android supports pulling in from the screen edges
|
||||||
|
const SCREEN_EDGE_MARGIN = 30;
|
||||||
|
const SCREEN_OFFSET = 300;
|
||||||
|
|
||||||
|
const SiteHeaderComponent = MountWidget.extend(Docking, PanEvents, {
|
||||||
widget: "header",
|
widget: "header",
|
||||||
docAt: null,
|
docAt: null,
|
||||||
dockedHeader: null,
|
dockedHeader: null,
|
||||||
|
_animate: false,
|
||||||
|
_isPanning: false,
|
||||||
|
_panMenuOrigin: "right",
|
||||||
|
_panMenuOffset: 0,
|
||||||
|
_scheduledMovingAnimation: null,
|
||||||
|
_scheduledRemoveAnimate: null,
|
||||||
_topic: null,
|
_topic: null,
|
||||||
|
|
||||||
@observes(
|
@observes(
|
||||||
|
@ -23,6 +38,141 @@ const SiteHeaderComponent = MountWidget.extend(Docking, {
|
||||||
this.queueRerender();
|
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;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_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));
|
||||||
|
const $window = $(window);
|
||||||
|
const windowWidth = parseInt($window.width());
|
||||||
|
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 if (
|
||||||
|
center.x < SCREEN_EDGE_MARGIN &&
|
||||||
|
!this.$(".menu-panel").length &&
|
||||||
|
e.direction === "right"
|
||||||
|
) {
|
||||||
|
this._animate = false;
|
||||||
|
this._panMenuOrigin = "left";
|
||||||
|
this._panMenuOffset = -SCREEN_OFFSET;
|
||||||
|
this._isPanning = true;
|
||||||
|
$("header.d-header").removeClass("scroll-down scroll-up");
|
||||||
|
this.eventDispatched("toggleHamburger", "header");
|
||||||
|
} else if (
|
||||||
|
windowWidth - center.x < SCREEN_EDGE_MARGIN &&
|
||||||
|
!this.$(".menu-panel").length &&
|
||||||
|
e.direction === "left"
|
||||||
|
) {
|
||||||
|
this._animate = false;
|
||||||
|
this._panMenuOrigin = "right";
|
||||||
|
this._panMenuOffset = -SCREEN_OFFSET;
|
||||||
|
this._isPanning = true;
|
||||||
|
$("header.d-header").removeClass("scroll-down scroll-up");
|
||||||
|
this.eventDispatched("toggleUserMenu", "header");
|
||||||
|
} 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) {
|
dockCheck(info) {
|
||||||
const $header = $("header.d-header");
|
const $header = $("header.d-header");
|
||||||
|
|
||||||
|
@ -47,6 +197,7 @@ const SiteHeaderComponent = MountWidget.extend(Docking, {
|
||||||
},
|
},
|
||||||
|
|
||||||
setTopic(topic) {
|
setTopic(topic) {
|
||||||
|
this.eventDispatched("dom:clean", "header");
|
||||||
this._topic = topic;
|
this._topic = topic;
|
||||||
this.queueRerender();
|
this.queueRerender();
|
||||||
},
|
},
|
||||||
|
@ -74,6 +225,14 @@ const SiteHeaderComponent = MountWidget.extend(Docking, {
|
||||||
this.eventDispatched("dom:clean", "header");
|
this.eventDispatched("dom:clean", "header");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (this.site.mobileView) {
|
||||||
|
$("body")
|
||||||
|
.on("pointerdown", e => this._panStart(e))
|
||||||
|
.on("pointermove", e => this._panMove(e))
|
||||||
|
.on("pointerup", e => this._panMove(e))
|
||||||
|
.on("pointercancel", e => this._panMove(e));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
willDestroyElement() {
|
willDestroyElement() {
|
||||||
|
@ -84,6 +243,16 @@ const SiteHeaderComponent = MountWidget.extend(Docking, {
|
||||||
this.appEvents.off("header:show-topic");
|
this.appEvents.off("header:show-topic");
|
||||||
this.appEvents.off("header:hide-topic");
|
this.appEvents.off("header:hide-topic");
|
||||||
this.appEvents.off("dom:clean");
|
this.appEvents.off("dom:clean");
|
||||||
|
|
||||||
|
if (this.site.mobileView) {
|
||||||
|
$("body")
|
||||||
|
.off("pointerdown")
|
||||||
|
.off("pointerup")
|
||||||
|
.off("pointermove")
|
||||||
|
.off("pointercancel");
|
||||||
|
}
|
||||||
|
Ember.run.cancel(this._scheduledRemoveAnimate);
|
||||||
|
window.cancelAnimationFrame(this._scheduledMovingAnimation);
|
||||||
},
|
},
|
||||||
|
|
||||||
buildArgs() {
|
buildArgs() {
|
||||||
|
@ -100,6 +269,9 @@ const SiteHeaderComponent = MountWidget.extend(Docking, {
|
||||||
afterRender() {
|
afterRender() {
|
||||||
const $menuPanels = $(".menu-panel");
|
const $menuPanels = $(".menu-panel");
|
||||||
if ($menuPanels.length === 0) {
|
if ($menuPanels.length === 0) {
|
||||||
|
if (this.site.mobileView) {
|
||||||
|
this._animate = true;
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,15 +284,29 @@ const SiteHeaderComponent = MountWidget.extend(Docking, {
|
||||||
|
|
||||||
$menuPanels.each((idx, panel) => {
|
$menuPanels.each((idx, panel) => {
|
||||||
const $panel = $(panel);
|
const $panel = $(panel);
|
||||||
|
const $headerCloak = $(".header-cloak");
|
||||||
let width = parseInt($panel.attr("data-max-width") || 300);
|
let width = parseInt($panel.attr("data-max-width") || 300);
|
||||||
if (windowWidth - width < 50) {
|
if (windowWidth - width < 50) {
|
||||||
width = windowWidth - 50;
|
width = windowWidth - 50;
|
||||||
}
|
}
|
||||||
|
if (this._panMenuOffset) {
|
||||||
|
this._panMenuOffset = -width;
|
||||||
|
}
|
||||||
|
|
||||||
$panel
|
$panel.removeClass("drop-down slide-in").addClass(viewMode);
|
||||||
.removeClass("drop-down")
|
if (this._animate || this._panMenuOffset !== 0) {
|
||||||
.removeClass("slide-in")
|
$headerCloak.css("opacity", 0);
|
||||||
.addClass(viewMode);
|
if (
|
||||||
|
this.site.mobileView &&
|
||||||
|
$panel.parent(".hamburger-panel").length > 0
|
||||||
|
) {
|
||||||
|
this._panMenuOrigin = "left";
|
||||||
|
$panel.css("left", -windowWidth);
|
||||||
|
} else {
|
||||||
|
this._panMenuOrigin = "right";
|
||||||
|
$panel.css("right", -windowWidth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const $panelBody = $(".panel-body", $panel);
|
const $panelBody = $(".panel-body", $panel);
|
||||||
// 2 pixel fudge allows for firefox subpixel sizing stuff causing scrollbar
|
// 2 pixel fudge allows for firefox subpixel sizing stuff causing scrollbar
|
||||||
|
@ -150,7 +336,8 @@ const SiteHeaderComponent = MountWidget.extend(Docking, {
|
||||||
|
|
||||||
if (
|
if (
|
||||||
contentHeight + (offsetTop - scrollTop) + PANEL_BODY_MARGIN >
|
contentHeight + (offsetTop - scrollTop) + PANEL_BODY_MARGIN >
|
||||||
fullHeight
|
fullHeight ||
|
||||||
|
this.site.mobileView
|
||||||
) {
|
) {
|
||||||
contentHeight =
|
contentHeight =
|
||||||
fullHeight - (offsetTop - scrollTop) - PANEL_BODY_MARGIN;
|
fullHeight - (offsetTop - scrollTop) - PANEL_BODY_MARGIN;
|
||||||
|
@ -160,11 +347,19 @@ const SiteHeaderComponent = MountWidget.extend(Docking, {
|
||||||
}
|
}
|
||||||
$("body").addClass("drop-down-mode");
|
$("body").addClass("drop-down-mode");
|
||||||
} else {
|
} else {
|
||||||
const menuTop = headerHeight();
|
if (this.site.mobileView) {
|
||||||
|
$headerCloak.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
const menuTop = this.site.mobileView ? 0 : headerHeight();
|
||||||
|
|
||||||
let height;
|
let height;
|
||||||
const winHeight = $(window).height() - 16;
|
const winHeightOffset = 16;
|
||||||
if (menuTop + contentHeight < winHeight) {
|
let initialWinHeight = window.innerHeight
|
||||||
|
? window.innerHeight
|
||||||
|
: $(window).height();
|
||||||
|
const winHeight = initialWinHeight - winHeightOffset;
|
||||||
|
if (menuTop + contentHeight < winHeight && !this.site.mobileView) {
|
||||||
height = contentHeight + "px";
|
height = contentHeight + "px";
|
||||||
} else {
|
} else {
|
||||||
height = winHeight - menuTop;
|
height = winHeight - menuTop;
|
||||||
|
@ -180,6 +375,17 @@ const SiteHeaderComponent = MountWidget.extend(Docking, {
|
||||||
}
|
}
|
||||||
|
|
||||||
$panel.width(width);
|
$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;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
import { observes } from "ember-addons/ember-computed-decorators";
|
import { observes } from "ember-addons/ember-computed-decorators";
|
||||||
import showModal from "discourse/lib/show-modal";
|
import showModal from "discourse/lib/show-modal";
|
||||||
import PanEvents from "discourse/mixins/pan-events";
|
import PanEvents, {
|
||||||
|
SWIPE_VELOCITY,
|
||||||
|
SWIPE_DISTANCE_THRESHOLD,
|
||||||
|
SWIPE_VELOCITY_THRESHOLD
|
||||||
|
} from "discourse/mixins/pan-events";
|
||||||
|
|
||||||
export default Ember.Component.extend(PanEvents, {
|
export default Ember.Component.extend(PanEvents, {
|
||||||
composerOpen: null,
|
composerOpen: null,
|
||||||
|
@ -117,10 +121,11 @@ export default Ember.Component.extend(PanEvents, {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_panOpenClose(offset, velocity, direction) {
|
_handlePanDone(offset, event) {
|
||||||
const $timelineContainer = $(".timeline-container");
|
const $timelineContainer = $(".timeline-container");
|
||||||
const maxOffset = parseInt($timelineContainer.css("height"));
|
const maxOffset = parseInt($timelineContainer.css("height"), 10);
|
||||||
direction === "close" ? (offset += velocity) : (offset -= velocity);
|
|
||||||
|
this._shouldPanClose(event) ? (offset += SWIPE_VELOCITY) : (offset -= SWIPE_VELOCITY);
|
||||||
|
|
||||||
$timelineContainer.css("bottom", -offset);
|
$timelineContainer.css("bottom", -offset);
|
||||||
if (offset > maxOffset) {
|
if (offset > maxOffset) {
|
||||||
|
@ -129,42 +134,41 @@ export default Ember.Component.extend(PanEvents, {
|
||||||
$timelineContainer.css("bottom", "");
|
$timelineContainer.css("bottom", "");
|
||||||
} else {
|
} else {
|
||||||
Ember.run.later(
|
Ember.run.later(
|
||||||
() => this._panOpenClose(offset, velocity, direction),
|
() => this._handlePanDone(offset, event),
|
||||||
20
|
20
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_shouldPanClose(e) {
|
_shouldPanClose(e) {
|
||||||
return (e.deltaY > 200 && e.velocityY > -0.15) || e.velocityY > 0.15;
|
return (e.deltaY > SWIPE_DISTANCE_THRESHOLD && e.velocityY > -SWIPE_VELOCITY_THRESHOLD) || e.velocityY > SWIPE_VELOCITY_THRESHOLD;
|
||||||
},
|
},
|
||||||
|
|
||||||
panStart(e) {
|
panStart(e) {
|
||||||
|
e.originalEvent.preventDefault();
|
||||||
const center = e.center;
|
const center = e.center;
|
||||||
const $centeredElement = $(document.elementFromPoint(center.x, center.y));
|
const $centeredElement = $(document.elementFromPoint(center.x, center.y));
|
||||||
if ($centeredElement.parents(".timeline-scrollarea-wrapper").length) {
|
if ($centeredElement.parents(".timeline-scrollarea-wrapper").length) {
|
||||||
this.set("isPanning", false);
|
this.isPanning = false;
|
||||||
} else if (e.direction === "up" || e.direction === "down") {
|
} else if (e.direction === "up" || e.direction === "down") {
|
||||||
this.set("isPanning", true);
|
this.isPanning = true;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
panEnd(e) {
|
panEnd(e) {
|
||||||
if (!this.get("isPanning")) {
|
if (!this.isPanning) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.set("isPanning", false);
|
e.originalEvent.preventDefault();
|
||||||
if (this._shouldPanClose(e)) {
|
this.isPanning = false;
|
||||||
this._panOpenClose(e.deltaY, 40, "close");
|
this._handlePanDone(e.deltaY, e);
|
||||||
} else {
|
|
||||||
this._panOpenClose(e.deltaY, 40, "open");
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
panMove(e) {
|
panMove(e) {
|
||||||
if (!this.get("isPanning")) {
|
if (!this.isPanning) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
e.originalEvent.preventDefault();
|
||||||
$(".timeline-container").css("bottom", Math.min(0, -e.deltaY));
|
$(".timeline-container").css("bottom", Math.min(0, -e.deltaY));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,10 @@
|
||||||
|
/**
|
||||||
|
Pan events is a mixin that allows components to detect and respond to swipe gestures
|
||||||
|
It fires callbacks for panStart, panEnd, panMove with the pan state, and the original event.
|
||||||
|
**/
|
||||||
|
export const SWIPE_VELOCITY = 40;
|
||||||
|
export const SWIPE_DISTANCE_THRESHOLD = 50;
|
||||||
|
export const SWIPE_VELOCITY_THRESHOLD = 0.1;
|
||||||
export default Ember.Mixin.create({
|
export default Ember.Mixin.create({
|
||||||
//velocity is pixels per ms
|
//velocity is pixels per ms
|
||||||
|
|
||||||
|
@ -7,11 +14,23 @@ export default Ember.Mixin.create({
|
||||||
this._super();
|
this._super();
|
||||||
|
|
||||||
if (this.site.mobileView) {
|
if (this.site.mobileView) {
|
||||||
this.$()
|
if ("onpointerdown" in document.documentElement) {
|
||||||
.on("pointerdown", e => this._panStart(e))
|
this.$()
|
||||||
.on("pointermove", e => this._panMove(e))
|
.on("pointerdown", e => this._panStart(e))
|
||||||
.on("pointerup", e => this._panMove(e))
|
.on("pointermove", e => this._panMove(e, e))
|
||||||
.on("pointercancel", e => this._panMove(e));
|
.on("pointerup", e => this._panMove(e, e))
|
||||||
|
.on("pointercancel", e => this._panMove(e, e));
|
||||||
|
} else if ("ontouchstart" in document.documentElement) {
|
||||||
|
this.$()
|
||||||
|
.on("touchstart", e => this._panStart(e.touches[0]))
|
||||||
|
.on("touchmove", e => {
|
||||||
|
const touchEvent = e.touches[0];
|
||||||
|
touchEvent.type = "pointermove";
|
||||||
|
this._panMove(touchEvent, e);
|
||||||
|
})
|
||||||
|
.on("touchend", e => this._panMove({ type: "pointerup" }, e))
|
||||||
|
.on("touchcancel", e => this._panMove({ type: "pointercancel" }, e));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -23,7 +42,11 @@ export default Ember.Mixin.create({
|
||||||
.off("pointerdown")
|
.off("pointerdown")
|
||||||
.off("pointerup")
|
.off("pointerup")
|
||||||
.off("pointermove")
|
.off("pointermove")
|
||||||
.off("pointercancel");
|
.off("pointercancel")
|
||||||
|
.off("touchstart")
|
||||||
|
.off("touchmove")
|
||||||
|
.off("touchend")
|
||||||
|
.off("touchcancel");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -44,6 +67,9 @@ export default Ember.Mixin.create({
|
||||||
}
|
}
|
||||||
const newTimestamp = new Date().getTime();
|
const newTimestamp = new Date().getTime();
|
||||||
const timeDiffSeconds = newTimestamp - oldState.timestamp;
|
const timeDiffSeconds = newTimestamp - oldState.timestamp;
|
||||||
|
if (timeDiffSeconds === 0) {
|
||||||
|
return oldState;
|
||||||
|
}
|
||||||
//calculate delta x, y, distance from START location
|
//calculate delta x, y, distance from START location
|
||||||
const deltaX = e.clientX - oldState.startLocation.x;
|
const deltaX = e.clientX - oldState.startLocation.x;
|
||||||
const deltaY = e.clientY - oldState.startLocation.y;
|
const deltaY = e.clientY - oldState.startLocation.y;
|
||||||
|
@ -93,7 +119,7 @@ export default Ember.Mixin.create({
|
||||||
this.set("_panState", newState);
|
this.set("_panState", newState);
|
||||||
},
|
},
|
||||||
|
|
||||||
_panMove(e) {
|
_panMove(e, originalEvent) {
|
||||||
if (!this.get("_panState")) {
|
if (!this.get("_panState")) {
|
||||||
this._panStart(e);
|
this._panStart(e);
|
||||||
return;
|
return;
|
||||||
|
@ -104,6 +130,7 @@ export default Ember.Mixin.create({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.set("_panState", newState);
|
this.set("_panState", newState);
|
||||||
|
newState.originalEvent = originalEvent;
|
||||||
if (previousState.start && "panStart" in this) {
|
if (previousState.start && "panStart" in this) {
|
||||||
this.panStart(newState);
|
this.panStart(newState);
|
||||||
} else if (
|
} else if (
|
||||||
|
|
|
@ -323,7 +323,32 @@ export default createWidget("hamburger-menu", {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
clickOutside() {
|
clickOutsideMobile(e) {
|
||||||
this.sendWidgetAction("toggleHamburger");
|
const $centeredElement = $(document.elementFromPoint(e.clientX, e.clientY));
|
||||||
|
if (
|
||||||
|
$centeredElement.parents(".panel").length &&
|
||||||
|
!$centeredElement.hasClass("header-cloak")
|
||||||
|
) {
|
||||||
|
this.sendWidgetAction("toggleHamburger");
|
||||||
|
} else {
|
||||||
|
const $window = $(window);
|
||||||
|
const windowWidth = parseInt($window.width(), 10);
|
||||||
|
const $panel = $(".menu-panel");
|
||||||
|
$panel.addClass("animate");
|
||||||
|
const panelOffsetDirection = this.site.mobileView ? "left" : "right";
|
||||||
|
$panel.css(panelOffsetDirection, -windowWidth);
|
||||||
|
const $headerCloak = $(".header-cloak");
|
||||||
|
$headerCloak.addClass("animate");
|
||||||
|
$headerCloak.css("opacity", 0);
|
||||||
|
Ember.run.later(() => this.sendWidgetAction("toggleHamburger"), 200);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
clickOutside(e) {
|
||||||
|
if (this.site.mobileView) {
|
||||||
|
this.clickOutsideMobile(e);
|
||||||
|
} else {
|
||||||
|
this.sendWidgetAction("toggleHamburger");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -253,6 +253,15 @@ createWidget("header-buttons", {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
createWidget("header-cloak", {
|
||||||
|
tagName: "div.header-cloak",
|
||||||
|
html() {
|
||||||
|
return "";
|
||||||
|
},
|
||||||
|
click() {},
|
||||||
|
scheduleRerender() {}
|
||||||
|
});
|
||||||
|
|
||||||
const forceContextEnabled = ["category", "user", "private_messages"];
|
const forceContextEnabled = ["category", "user", "private_messages"];
|
||||||
|
|
||||||
let additionalPanels = [];
|
let additionalPanels = [];
|
||||||
|
@ -315,6 +324,9 @@ export default createWidget("header", {
|
||||||
} else if (state.userVisible) {
|
} else if (state.userVisible) {
|
||||||
panels.push(this.attach("user-menu"));
|
panels.push(this.attach("user-menu"));
|
||||||
}
|
}
|
||||||
|
if (this.site.mobileView) {
|
||||||
|
panels.push(this.attach("header-cloak"));
|
||||||
|
}
|
||||||
|
|
||||||
additionalPanels.map(panel => {
|
additionalPanels.map(panel => {
|
||||||
if (this.state[panel.toggle]) {
|
if (this.state[panel.toggle]) {
|
||||||
|
@ -348,6 +360,7 @@ export default createWidget("header", {
|
||||||
this.state.userVisible = false;
|
this.state.userVisible = false;
|
||||||
this.state.hamburgerVisible = false;
|
this.state.hamburgerVisible = false;
|
||||||
this.state.searchVisible = false;
|
this.state.searchVisible = false;
|
||||||
|
this.toggleBodyScrolling(false);
|
||||||
},
|
},
|
||||||
|
|
||||||
linkClickedEvent(attrs) {
|
linkClickedEvent(attrs) {
|
||||||
|
@ -416,10 +429,32 @@ export default createWidget("header", {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.state.userVisible = !this.state.userVisible;
|
this.state.userVisible = !this.state.userVisible;
|
||||||
|
this.toggleBodyScrolling(this.state.userVisible);
|
||||||
},
|
},
|
||||||
|
|
||||||
toggleHamburger() {
|
toggleHamburger() {
|
||||||
this.state.hamburgerVisible = !this.state.hamburgerVisible;
|
this.state.hamburgerVisible = !this.state.hamburgerVisible;
|
||||||
|
this.toggleBodyScrolling(this.state.hamburgerVisible);
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleBodyScrolling(bool) {
|
||||||
|
if (!this.site.mobileView) return;
|
||||||
|
if (bool) {
|
||||||
|
document.body.addEventListener("touchmove", this.preventDefault, { passive: false });
|
||||||
|
} else {
|
||||||
|
document.body.removeEventListener("touchmove", this.preventDefault, { passive: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
preventDefault(e) {
|
||||||
|
// prevent all scrollin on menu panels, except on overflow
|
||||||
|
const height = window.innerHeight
|
||||||
|
? window.innerHeight
|
||||||
|
: $(window).height();
|
||||||
|
if (!$(e.target).parents(".menu-panel").length ||
|
||||||
|
$(".menu-panel .panel-body-contents").height() <= height) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
togglePageSearch() {
|
togglePageSearch() {
|
||||||
|
|
|
@ -194,7 +194,31 @@ export default createWidget("user-menu", {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
clickOutside() {
|
clickOutsideMobile(e) {
|
||||||
this.sendWidgetAction("toggleUserMenu");
|
const $centeredElement = $(document.elementFromPoint(e.clientX, e.clientY));
|
||||||
|
if (
|
||||||
|
$centeredElement.parents(".panel").length &&
|
||||||
|
!$centeredElement.hasClass("header-cloak")
|
||||||
|
) {
|
||||||
|
this.sendWidgetAction("toggleUserMenu");
|
||||||
|
} else {
|
||||||
|
const $window = $(window);
|
||||||
|
const windowWidth = parseInt($window.width(), 10);
|
||||||
|
const $panel = $(".menu-panel");
|
||||||
|
$panel.addClass("animate");
|
||||||
|
$panel.css("right", -windowWidth);
|
||||||
|
const $headerCloak = $(".header-cloak");
|
||||||
|
$headerCloak.addClass("animate");
|
||||||
|
$headerCloak.css("opacity", 0);
|
||||||
|
Ember.run.later(() => this.sendWidgetAction("toggleUserMenu"), 200);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
clickOutside(e) {
|
||||||
|
if (this.site.mobileView) {
|
||||||
|
this.clickOutsideMobile(e);
|
||||||
|
} else {
|
||||||
|
this.sendWidgetAction("toggleUserMenu");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,6 +2,9 @@
|
||||||
position: fixed;
|
position: fixed;
|
||||||
right: 0;
|
right: 0;
|
||||||
box-shadow: shadow("header");
|
box-shadow: shadow("header");
|
||||||
|
&.animate {
|
||||||
|
transition: right 0.2s ease-out, left 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
.panel-body {
|
.panel-body {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -10,6 +13,9 @@
|
||||||
width: 97%;
|
width: 97%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.header-cloak {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.menu-panel.drop-down {
|
.menu-panel.drop-down {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -42,6 +48,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-body {
|
.panel-body {
|
||||||
|
touch-action: pan-y pinch-zoom;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,7 @@
|
||||||
@import "mobile/admin_report";
|
@import "mobile/admin_report";
|
||||||
@import "mobile/admin_report_table";
|
@import "mobile/admin_report_table";
|
||||||
@import "mobile/admin_report_counters";
|
@import "mobile/admin_report_counters";
|
||||||
|
@import "mobile/menu-panel";
|
||||||
|
|
||||||
// Import all component-specific files
|
// Import all component-specific files
|
||||||
@import "mobile/components/*";
|
@import "mobile/components/*";
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
.hamburger-panel .menu-panel.slide-in {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
.header-cloak {
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
position: fixed;
|
||||||
|
background-color: black;
|
||||||
|
opacity: 0.5;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
display: none;
|
||||||
|
touch-action: pan-y pinch-zoom;
|
||||||
|
&.animate {
|
||||||
|
transition: opacity 0.2s ease-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue