diff --git a/app/assets/javascripts/discourse/components/topic-navigation.js.es6 b/app/assets/javascripts/discourse/components/topic-navigation.js.es6 index 688d49b03ca..6e41bacfff8 100644 --- a/app/assets/javascripts/discourse/components/topic-navigation.js.es6 +++ b/app/assets/javascripts/discourse/components/topic-navigation.js.es6 @@ -1,9 +1,11 @@ import { observes } from "ember-addons/ember-computed-decorators"; import showModal from "discourse/lib/show-modal"; +import PanEvents from "discourse/mixins/pan-events"; -export default Ember.Component.extend({ +export default Ember.Component.extend(PanEvents, { composerOpen: null, info: null, + isPanning: false, init() { this._super(); @@ -91,7 +93,7 @@ export default Ember.Component.extend({ _collapseFullscreen() { if (this.get("info.topicProgressExpanded")) { $(".timeline-fullscreen").removeClass("show"); - setTimeout(() => { + Ember.run.later(() => { this.set("info.topicProgressExpanded", false); this._checkSize(); }, 500); @@ -109,6 +111,59 @@ export default Ember.Component.extend({ } }, + _panOpenClose(offset, velocity, direction) { + + const $timelineContainer = $(".timeline-container"); + const maxOffset = parseInt($timelineContainer.css("height")); + direction === "close" ? offset += velocity : offset -= velocity; + + $timelineContainer.css("bottom", -offset); + if(offset > maxOffset) { + this._collapseFullscreen(); + } + else if(offset <= 0) { + $timelineContainer.css("bottom", ""); + } + else { + Ember.run.later(() => this._panOpenClose(offset, velocity, direction), 20); + } + }, + + _shouldPanClose(e) { + return (e.deltaY > 200 && e.velocityY > -0.15) || e.velocityY > 0.15; + }, + + panStart(e) { + const center = e.center; + const $centeredElement = $(document.elementFromPoint(center.x, center.y)); + if ($centeredElement.parents(".timeline-scrollarea-wrapper").length) { + this.set("isPanning", false); + } + else { + this.set("isPanning", true); + } + }, + + panEnd(e) { + if(!this.get("isPanning")) { + return; + } + this.set("isPanning", false); + if(this._shouldPanClose(e)) { + this._panOpenClose(e.deltaY, 40, "close"); + } + else { + this._panOpenClose(e.deltaY, 40, "open"); + } + }, + + panMove(e) { + if(!this.get("isPanning")) { + return; + } + $(".timeline-container").css("bottom", Math.min(0, -e.deltaY)); + }, + didInsertElement() { this._super(); diff --git a/app/assets/javascripts/discourse/mixins/pan-events.js.es6 b/app/assets/javascripts/discourse/mixins/pan-events.js.es6 new file mode 100644 index 00000000000..0acf6d03a40 --- /dev/null +++ b/app/assets/javascripts/discourse/mixins/pan-events.js.es6 @@ -0,0 +1,89 @@ +import { + on, + default as computed +} from "ember-addons/ember-computed-decorators"; + +export default Ember.Mixin.create({ + + //velocity is pixels per ms + + _panState: null, + + didInsertElement() { + this.$().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() { + this.$().off("pointerdown") + .off("pointerup") + .off("pointermove") + .off("pointercancel"); + }, + + _calculateNewPanState(oldState, e) { + if(e.type == "pointerup" || e.type == "pointercancel") { + return oldState; + } + const newTimestamp = new Date().getTime(); + const timeDiffSeconds = (newTimestamp - oldState.timestamp); + //calculate delta x, y, distance from START location + const deltaX = Math.round(e.clientX) - oldState.startLocation.x; + const deltaY = Math.round(e.clientY) - oldState.startLocation.y; + const distance = Math.round(Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2))); + + //calculate velocity from previous event center location + const eventDeltaX = e.clientX - oldState.center.x; + const eventDeltaY = e.clientY - oldState.center.y; + const velocityX = eventDeltaX / timeDiffSeconds; + const velocityY = eventDeltaY / timeDiffSeconds; + const deltaDistance = Math.sqrt(Math.pow(eventDeltaX, 2) + Math.pow(eventDeltaY, 2)); + const velocity = deltaDistance / timeDiffSeconds; + + return { + startLocation: oldState.startLocation, + center: {x: Math.round(e.clientX), y: Math.round(e.clientY)}, + velocity, + velocityX, + velocityY, + deltaX, + deltaY, + distance, + start: false, + timestamp: newTimestamp + }; + }, + + _panStart(e) { + const newState = { + center: {x: Math.round(e.clientX), y: Math.round(e.clientY)}, + startLocation: {x: Math.round(e.clientX), y: Math.round(e.clientY)}, + velocity: 0, + velocityX: 0, + velocityY: 0, + deltaX: 0, + deltaY: 0, + distance: 0, + start: true, + timestamp: new Date().getTime() + }; + this.set("_panState", newState); + }, + + _panMove(e) { + const previousState = this.get("_panState"); + const newState = this._calculateNewPanState(previousState, e); + this.set("_panState", newState); + if(previousState.start && "panStart" in this) { + this.panStart(newState); + } + else if((e.type == "pointerup" || e.type == "pointercancel") && "panEnd" in this) { + this.panEnd(newState); + } + else if(e.type == "pointermove" && "panMove" in this) { + this.panMove(newState); + } + }, +});