DEV: Move all scroll position reset/remember logic to a shared service (#22552)
Previously we were implementing scroll reset/memorization on a per-page basis. Many of these approaches relied on the `didInsertElement` hook, which is no longer appropriate since Discourse changed to use the 'loading slider' strategy for page transitions. This commit rips out all of our custom scroll resetting/memorizing, and implements those things in a generic service. There are two features: 1. After every route transition, scroll to the top of the page 2. When using browser back/forward buttons, restore the last known scroll position for those routes To opt-out of the behaviour, individual routes can add a scrollOnTransition boolean to their RouteInfo metadata using Ember's `buildRouteInfoMetadata` hook.
This commit is contained in:
parent
80578e75f0
commit
dfe94ba118
|
@ -10,8 +10,6 @@
|
||||||
@canBulkSelect={{this.canBulkSelect}}
|
@canBulkSelect={{this.canBulkSelect}}
|
||||||
@selected={{this.selected}}
|
@selected={{this.selected}}
|
||||||
@tagsForUser={{this.tagsForUser}}
|
@tagsForUser={{this.tagsForUser}}
|
||||||
@onScroll={{this.onScroll}}
|
|
||||||
@scrollOnLoad={{this.scrollOnLoad}}
|
|
||||||
@toggleBulkSelect={{this.toggleBulkSelect}}
|
@toggleBulkSelect={{this.toggleBulkSelect}}
|
||||||
@updateAutoAddTopicsToBulkSelect={{this.updateAutoAddTopicsToBulkSelect}}
|
@updateAutoAddTopicsToBulkSelect={{this.updateAutoAddTopicsToBulkSelect}}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,48 +1,19 @@
|
||||||
import Component from "@ember/component";
|
import Component from "@ember/component";
|
||||||
import { action } from "@ember/object";
|
import { action } from "@ember/object";
|
||||||
import { next, schedule } from "@ember/runloop";
|
|
||||||
import { openBookmarkModal } from "discourse/controllers/bookmark";
|
import { openBookmarkModal } from "discourse/controllers/bookmark";
|
||||||
import { ajax } from "discourse/lib/ajax";
|
import { ajax } from "discourse/lib/ajax";
|
||||||
import {
|
import {
|
||||||
openLinkInNewTab,
|
openLinkInNewTab,
|
||||||
shouldOpenInNewTab,
|
shouldOpenInNewTab,
|
||||||
} from "discourse/lib/click-track";
|
} from "discourse/lib/click-track";
|
||||||
import Scrolling from "discourse/mixins/scrolling";
|
|
||||||
import I18n from "I18n";
|
import I18n from "I18n";
|
||||||
import { Promise } from "rsvp";
|
import { Promise } from "rsvp";
|
||||||
import { inject as service } from "@ember/service";
|
import { inject as service } from "@ember/service";
|
||||||
|
|
||||||
export default Component.extend(Scrolling, {
|
export default Component.extend({
|
||||||
dialog: service(),
|
dialog: service(),
|
||||||
classNames: ["bookmark-list-wrapper"],
|
classNames: ["bookmark-list-wrapper"],
|
||||||
|
|
||||||
didInsertElement() {
|
|
||||||
this._super(...arguments);
|
|
||||||
this.bindScrolling();
|
|
||||||
this.scrollToLastPosition();
|
|
||||||
},
|
|
||||||
|
|
||||||
willDestroyElement() {
|
|
||||||
this._super(...arguments);
|
|
||||||
this.unbindScrolling();
|
|
||||||
},
|
|
||||||
|
|
||||||
scrollToLastPosition() {
|
|
||||||
const scrollTo = this.session.bookmarkListScrollPosition;
|
|
||||||
if (scrollTo >= 0) {
|
|
||||||
schedule("afterRender", () => {
|
|
||||||
if (this.element && !this.isDestroying && !this.isDestroyed) {
|
|
||||||
next(() => window.scrollTo(0, scrollTo));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
scrolled() {
|
|
||||||
this._super(...arguments);
|
|
||||||
this.session.set("bookmarkListScrollPosition", window.scrollY);
|
|
||||||
},
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
removeBookmark(bookmark) {
|
removeBookmark(bookmark) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|
|
@ -1,36 +1,13 @@
|
||||||
import deprecated from "discourse-common/lib/deprecated";
|
|
||||||
import Component from "@ember/component";
|
import Component from "@ember/component";
|
||||||
import { scrollTop } from "discourse/mixins/scroll-top";
|
|
||||||
import { scheduleOnce } from "@ember/runloop";
|
import { scheduleOnce } from "@ember/runloop";
|
||||||
|
|
||||||
// Can add a body class from within a component, also will scroll to the top automatically.
|
// Can add a body class from within a component
|
||||||
export default class extends Component {
|
export default class extends Component {
|
||||||
tagName = null;
|
tagName = null;
|
||||||
pageClass = null;
|
pageClass = null;
|
||||||
bodyClass = null;
|
bodyClass = null;
|
||||||
scrollTop = true;
|
|
||||||
currentClasses = new Set();
|
currentClasses = new Set();
|
||||||
|
|
||||||
didInsertElement() {
|
|
||||||
this._super(...arguments);
|
|
||||||
|
|
||||||
if (this.scrollTop === "false") {
|
|
||||||
deprecated("Uses boolean instead of string for scrollTop.", {
|
|
||||||
since: "2.8.0.beta9",
|
|
||||||
dropFrom: "2.9.0.beta1",
|
|
||||||
id: "discourse.d-section.scroll-top-boolean",
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.scrollTop) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
scrollTop();
|
|
||||||
}
|
|
||||||
|
|
||||||
didReceiveAttrs() {
|
didReceiveAttrs() {
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
scheduleOnce("afterRender", this, this._updateClasses);
|
scheduleOnce("afterRender", this, this._updateClasses);
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
{{yield (hash saveScrollPosition=this.saveScrollPosition)}}
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { observes, on } from "discourse-common/utils/decorators";
|
import { observes, on } from "discourse-common/utils/decorators";
|
||||||
import { next, schedule, scheduleOnce } from "@ember/runloop";
|
|
||||||
import Component from "@ember/component";
|
import Component from "@ember/component";
|
||||||
import LoadMore from "discourse/mixins/load-more";
|
import LoadMore from "discourse/mixins/load-more";
|
||||||
import UrlRefresh from "discourse/mixins/url-refresh";
|
import UrlRefresh from "discourse/mixins/url-refresh";
|
||||||
|
@ -10,21 +9,6 @@ export default Component.extend(UrlRefresh, LoadMore, {
|
||||||
eyelineSelector: ".topic-list-item",
|
eyelineSelector: ".topic-list-item",
|
||||||
documentTitle: service(),
|
documentTitle: service(),
|
||||||
|
|
||||||
@on("didInsertElement")
|
|
||||||
@observes("model")
|
|
||||||
_readjustScrollPosition() {
|
|
||||||
const scrollTo = this.session.topicListScrollPosition;
|
|
||||||
if (scrollTo >= 0) {
|
|
||||||
schedule("afterRender", () => {
|
|
||||||
if (this.element && !this.isDestroying && !this.isDestroyed) {
|
|
||||||
next(() => window.scrollTo(0, scrollTo));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
scheduleOnce("afterRender", this, this.loadMoreUnlessFull);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
@on("didInsertElement")
|
@on("didInsertElement")
|
||||||
_monitorTrackingState() {
|
_monitorTrackingState() {
|
||||||
this.stateChangeCallbackId = this.topicTrackingState.onStateChange(() =>
|
this.stateChangeCallbackId = this.topicTrackingState.onStateChange(() =>
|
||||||
|
@ -48,10 +32,6 @@ export default Component.extend(UrlRefresh, LoadMore, {
|
||||||
this.documentTitle.updateContextCount(this.incomingCount);
|
this.documentTitle.updateContextCount(this.incomingCount);
|
||||||
},
|
},
|
||||||
|
|
||||||
saveScrollPosition() {
|
|
||||||
this.session.set("topicListScrollPosition", $(window).scrollTop());
|
|
||||||
},
|
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
loadMore() {
|
loadMore() {
|
||||||
this.documentTitle.updateContextCount(0);
|
this.documentTitle.updateContextCount(0);
|
||||||
|
@ -64,7 +44,6 @@ export default Component.extend(UrlRefresh, LoadMore, {
|
||||||
) {
|
) {
|
||||||
this.addTopicsToBulkSelect(newTopics);
|
this.addTopicsToBulkSelect(newTopics);
|
||||||
}
|
}
|
||||||
schedule("afterRender", () => this.saveScrollPosition());
|
|
||||||
if (moreTopicsUrl && $(window).height() >= $(document).height()) {
|
if (moreTopicsUrl && $(window).height() >= $(document).height()) {
|
||||||
this.send("loadMore");
|
this.send("loadMore");
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,4 @@
|
||||||
<DSection
|
<DSection @pageClass="has-sidebar" @id="d-sidebar" @class="sidebar-container">
|
||||||
@pageClass="has-sidebar"
|
|
||||||
@id="d-sidebar"
|
|
||||||
@class="sidebar-container"
|
|
||||||
@scrollTop={{false}}
|
|
||||||
>
|
|
||||||
<Sidebar::Sections
|
<Sidebar::Sections
|
||||||
@currentUser={{this.currentUser}}
|
@currentUser={{this.currentUser}}
|
||||||
@collapsableSections={{true}}
|
@collapsableSections={{true}}
|
||||||
|
|
|
@ -3,7 +3,6 @@ import discourseComputed, { observes } from "discourse-common/utils/decorators";
|
||||||
import Component from "@ember/component";
|
import Component from "@ember/component";
|
||||||
import LoadMore from "discourse/mixins/load-more";
|
import LoadMore from "discourse/mixins/load-more";
|
||||||
import { on } from "@ember/object/evented";
|
import { on } from "@ember/object/evented";
|
||||||
import { next, schedule } from "@ember/runloop";
|
|
||||||
import showModal from "discourse/lib/show-modal";
|
import showModal from "discourse/lib/show-modal";
|
||||||
|
|
||||||
export default Component.extend(LoadMore, {
|
export default Component.extend(LoadMore, {
|
||||||
|
@ -67,26 +66,6 @@ export default Component.extend(LoadMore, {
|
||||||
onScroll.call(this);
|
onScroll.call(this);
|
||||||
},
|
},
|
||||||
|
|
||||||
scrollToLastPosition() {
|
|
||||||
if (!this.scrollOnLoad) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const scrollTo = this.session.topicListScrollPosition;
|
|
||||||
if (scrollTo >= 0) {
|
|
||||||
schedule("afterRender", () => {
|
|
||||||
if (this.element && !this.isDestroying && !this.isDestroyed) {
|
|
||||||
next(() => window.scrollTo(0, scrollTo));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
didInsertElement() {
|
|
||||||
this._super(...arguments);
|
|
||||||
this.scrollToLastPosition();
|
|
||||||
},
|
|
||||||
|
|
||||||
_updateLastVisitedTopic(topics, order, ascending, top) {
|
_updateLastVisitedTopic(topics, order, ascending, top) {
|
||||||
this.set("lastVisitedTopic", null);
|
this.set("lastVisitedTopic", null);
|
||||||
|
|
||||||
|
|
|
@ -6,10 +6,8 @@ import I18n from "I18n";
|
||||||
import LoadMore from "discourse/mixins/load-more";
|
import LoadMore from "discourse/mixins/load-more";
|
||||||
import Post from "discourse/models/post";
|
import Post from "discourse/models/post";
|
||||||
import { NEW_TOPIC_KEY } from "discourse/models/composer";
|
import { NEW_TOPIC_KEY } from "discourse/models/composer";
|
||||||
import { observes } from "discourse-common/utils/decorators";
|
|
||||||
import { on } from "@ember/object/evented";
|
import { on } from "@ember/object/evented";
|
||||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
import { next, schedule } from "@ember/runloop";
|
|
||||||
import { inject as service } from "@ember/service";
|
import { inject as service } from "@ember/service";
|
||||||
|
|
||||||
export default Component.extend(LoadMore, {
|
export default Component.extend(LoadMore, {
|
||||||
|
@ -32,14 +30,7 @@ export default Component.extend(LoadMore, {
|
||||||
eyelineSelector: ".user-stream .item",
|
eyelineSelector: ".user-stream .item",
|
||||||
classNames: ["user-stream"],
|
classNames: ["user-stream"],
|
||||||
|
|
||||||
@observes("stream.user.id")
|
|
||||||
_scrollTopOnModelChange() {
|
|
||||||
schedule("afterRender", () => $(document).scrollTop(0));
|
|
||||||
},
|
|
||||||
|
|
||||||
_inserted: on("didInsertElement", function () {
|
_inserted: on("didInsertElement", function () {
|
||||||
$(window).on("resize.discourse-on-scroll", () => this.scrolled());
|
|
||||||
|
|
||||||
$(this.element).on(
|
$(this.element).on(
|
||||||
"click.details-disabled",
|
"click.details-disabled",
|
||||||
"details.disabled",
|
"details.disabled",
|
||||||
|
@ -49,12 +40,10 @@ export default Component.extend(LoadMore, {
|
||||||
return ClickTrack.trackClick(e, this.siteSettings);
|
return ClickTrack.trackClick(e, this.siteSettings);
|
||||||
});
|
});
|
||||||
this._updateLastDecoratedElement();
|
this._updateLastDecoratedElement();
|
||||||
this._scrollToLastPosition();
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// This view is being removed. Shut down operations
|
// This view is being removed. Shut down operations
|
||||||
_destroyed: on("willDestroyElement", function () {
|
_destroyed: on("willDestroyElement", function () {
|
||||||
$(window).unbind("resize.discourse-on-scroll");
|
|
||||||
$(this.element).off("click.details-disabled", "details.disabled");
|
$(this.element).off("click.details-disabled", "details.disabled");
|
||||||
|
|
||||||
// Unbind link tracking
|
// Unbind link tracking
|
||||||
|
@ -73,22 +62,6 @@ export default Component.extend(LoadMore, {
|
||||||
this._lastDecoratedElement = lastElement;
|
this._lastDecoratedElement = lastElement;
|
||||||
},
|
},
|
||||||
|
|
||||||
_scrollToLastPosition() {
|
|
||||||
const scrollTo = this.session.userStreamScrollPosition;
|
|
||||||
if (scrollTo >= 0) {
|
|
||||||
schedule("afterRender", () => {
|
|
||||||
if (this.element && !this.isDestroying && !this.isDestroyed) {
|
|
||||||
next(() => window.scrollTo(0, scrollTo));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
scrolled() {
|
|
||||||
this._super(...arguments);
|
|
||||||
this.session.set("userStreamScrollPosition", window.scrollY);
|
|
||||||
},
|
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
removeBookmark(userAction) {
|
removeBookmark(userAction) {
|
||||||
const stream = this.stream;
|
const stream = this.stream;
|
||||||
|
|
|
@ -25,10 +25,6 @@ export default Controller.extend(BulkTopicSelection, {
|
||||||
return topicsLength === 0 && incomingCount === 0;
|
return topicsLength === 0 && incomingCount === 0;
|
||||||
},
|
},
|
||||||
|
|
||||||
saveScrollPosition() {
|
|
||||||
this.session.set("topicListScrollPosition", $(window).scrollTop());
|
|
||||||
},
|
|
||||||
|
|
||||||
@observes("model.canLoadMore")
|
@observes("model.canLoadMore")
|
||||||
_showFooter() {
|
_showFooter() {
|
||||||
this.set("application.showFooter", !this.get("model.canLoadMore"));
|
this.set("application.showFooter", !this.get("model.canLoadMore"));
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
export default {
|
||||||
|
initialize(owner) {
|
||||||
|
owner.lookup("service:route-scroll-manager");
|
||||||
|
},
|
||||||
|
};
|
|
@ -9,7 +9,6 @@ export function getCachedTopicList(session) {
|
||||||
export function resetCachedTopicList(session) {
|
export function resetCachedTopicList(session) {
|
||||||
session.setProperties({
|
session.setProperties({
|
||||||
topicList: null,
|
topicList: null,
|
||||||
topicListScrollPosition: null,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,17 @@ let popstateFired = false;
|
||||||
const supportsHistoryState = window.history && "state" in window.history;
|
const supportsHistoryState = window.history && "state" in window.history;
|
||||||
const popstateCallbacks = [];
|
const popstateCallbacks = [];
|
||||||
|
|
||||||
|
function _uuid() {
|
||||||
|
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
|
||||||
|
let r, v;
|
||||||
|
/* eslint-disable no-bitwise */
|
||||||
|
r = (Math.random() * 16) | 0;
|
||||||
|
v = c === "x" ? r : (r & 3) | 8;
|
||||||
|
/* eslint-enable no-bitwise */
|
||||||
|
return v.toString(16);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
`Ember.DiscourseLocation` implements the location API using the browser's
|
`Ember.DiscourseLocation` implements the location API using the browser's
|
||||||
`history.pushState` API.
|
`history.pushState` API.
|
||||||
|
@ -130,7 +141,7 @@ const DiscourseLocation = EmberObject.extend({
|
||||||
@param path {String}
|
@param path {String}
|
||||||
*/
|
*/
|
||||||
pushState(path) {
|
pushState(path) {
|
||||||
const state = { path };
|
const state = { path, uuid: _uuid() };
|
||||||
|
|
||||||
// store state if browser doesn't support `history.state`
|
// store state if browser doesn't support `history.state`
|
||||||
if (!supportsHistoryState) {
|
if (!supportsHistoryState) {
|
||||||
|
@ -152,7 +163,7 @@ const DiscourseLocation = EmberObject.extend({
|
||||||
@param path {String}
|
@param path {String}
|
||||||
*/
|
*/
|
||||||
replaceState(path) {
|
replaceState(path) {
|
||||||
const state = { path };
|
const state = { path, uuid: _uuid() };
|
||||||
|
|
||||||
// store state if browser doesn't support `history.state`
|
// store state if browser doesn't support `history.state`
|
||||||
if (!supportsHistoryState) {
|
if (!supportsHistoryState) {
|
||||||
|
|
|
@ -56,7 +56,7 @@ async function findTopicList(
|
||||||
session.set("topicList", null);
|
session.set("topicList", null);
|
||||||
} else {
|
} else {
|
||||||
// Clear the cache
|
// Clear the cache
|
||||||
session.setProperties({ topicList: null, topicListScrollPosition: null });
|
session.setProperties({ topicList: null });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!list) {
|
if (!list) {
|
||||||
|
|
|
@ -5,9 +5,9 @@
|
||||||
import DiscourseRoute from "discourse/routes/discourse";
|
import DiscourseRoute from "discourse/routes/discourse";
|
||||||
import OpenComposer from "discourse/mixins/open-composer";
|
import OpenComposer from "discourse/mixins/open-composer";
|
||||||
import User from "discourse/models/user";
|
import User from "discourse/models/user";
|
||||||
import { scrollTop } from "discourse/mixins/scroll-top";
|
|
||||||
import { setTopicList } from "discourse/lib/topic-list-tracker";
|
import { setTopicList } from "discourse/lib/topic-list-tracker";
|
||||||
import { action } from "@ember/object";
|
import { action } from "@ember/object";
|
||||||
|
import { resetCachedTopicList } from "discourse/lib/cached-topic-list";
|
||||||
|
|
||||||
export default DiscourseRoute.extend(OpenComposer, {
|
export default DiscourseRoute.extend(OpenComposer, {
|
||||||
queryParams: {
|
queryParams: {
|
||||||
|
@ -56,9 +56,6 @@ export default DiscourseRoute.extend(OpenComposer, {
|
||||||
@action
|
@action
|
||||||
loadingComplete() {
|
loadingComplete() {
|
||||||
this.controllerFor("discovery").loadingComplete();
|
this.controllerFor("discovery").loadingComplete();
|
||||||
if (!this.session.get("topicListScrollPosition")) {
|
|
||||||
scrollTop();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
@action
|
@action
|
||||||
|
@ -99,6 +96,11 @@ export default DiscourseRoute.extend(OpenComposer, {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
refresh() {
|
||||||
|
resetCachedTopicList(this.session);
|
||||||
|
this._super();
|
||||||
|
},
|
||||||
|
|
||||||
@action
|
@action
|
||||||
triggerRefresh() {
|
triggerRefresh() {
|
||||||
this.refresh();
|
this.refresh();
|
||||||
|
|
|
@ -21,6 +21,12 @@ const TopicRoute = DiscourseRoute.extend({
|
||||||
lastScrollPos: null,
|
lastScrollPos: null,
|
||||||
isTransitioning: false,
|
isTransitioning: false,
|
||||||
|
|
||||||
|
buildRouteInfoMetadata() {
|
||||||
|
return {
|
||||||
|
scrollOnTransition: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
redirect() {
|
redirect() {
|
||||||
return this.redirectIfLoginRequired();
|
return this.redirectIfLoginRequired();
|
||||||
},
|
},
|
||||||
|
|
|
@ -25,7 +25,6 @@ export default DiscourseRoute.extend({
|
||||||
|
|
||||||
this.session.setProperties({
|
this.session.setProperties({
|
||||||
bookmarksModel: null,
|
bookmarksModel: null,
|
||||||
bookmarkListScrollPosition: null,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
controller.set("loading", true);
|
controller.set("loading", true);
|
||||||
|
|
|
@ -24,12 +24,6 @@ export default UserTopicListRoute.extend({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
afterModel(model, transition) {
|
|
||||||
if (!this.isPoppedState(transition)) {
|
|
||||||
this.session.set("topicListScrollPosition", null);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
emptyState() {
|
emptyState() {
|
||||||
const title = I18n.t("user_activity.no_read_topics_title");
|
const title = I18n.t("user_activity.no_read_topics_title");
|
||||||
const body = htmlSafe(
|
const body = htmlSafe(
|
||||||
|
|
|
@ -21,10 +21,6 @@ export default DiscourseRoute.extend(ViewingActionType, {
|
||||||
},
|
},
|
||||||
|
|
||||||
afterModel(model, transition) {
|
afterModel(model, transition) {
|
||||||
if (!this.isPoppedState(transition)) {
|
|
||||||
this.session.set("userStreamScrollPosition", null);
|
|
||||||
}
|
|
||||||
|
|
||||||
return model.stream.filterBy({
|
return model.stream.filterBy({
|
||||||
filter: this.userActionType,
|
filter: this.userActionType,
|
||||||
actingUsername: transition.to.queryParams.acting_username,
|
actingUsername: transition.to.queryParams.acting_username,
|
||||||
|
|
|
@ -24,12 +24,6 @@ export default UserTopicListRoute.extend({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
afterModel(model, transition) {
|
|
||||||
if (!this.isPoppedState(transition)) {
|
|
||||||
this.session.set("topicListScrollPosition", null);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
emptyState() {
|
emptyState() {
|
||||||
const user = this.modelFor("user");
|
const user = this.modelFor("user");
|
||||||
let title, body;
|
let title, body;
|
||||||
|
|
|
@ -11,12 +11,6 @@ export default DiscourseRoute.extend({
|
||||||
return user;
|
return user;
|
||||||
},
|
},
|
||||||
|
|
||||||
afterModel(_model, transition) {
|
|
||||||
if (!this.isPoppedState(transition)) {
|
|
||||||
this.session.set("userStreamScrollPosition", null);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
setupController(controller, user) {
|
setupController(controller, user) {
|
||||||
this.controllerFor("user-activity").set("model", user);
|
this.controllerFor("user-activity").set("model", user);
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,92 @@
|
||||||
|
import Service, { inject as service } from "@ember/service";
|
||||||
|
import { bind } from "discourse-common/utils/decorators";
|
||||||
|
import { schedule } from "@ember/runloop";
|
||||||
|
import { isTesting } from "discourse-common/config/environment";
|
||||||
|
|
||||||
|
const MAX_SCROLL_LOCATIONS = 100;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This service is responsible for managing scroll position when transitioning.
|
||||||
|
* When visiting a new route, this service will scroll to the top of the page.
|
||||||
|
* When returning to a previously-visited route via the browser back button,
|
||||||
|
* this service will scroll to the previous scroll position.
|
||||||
|
*
|
||||||
|
* To opt-out of the behaviour, individual routes can add a scrollOnTransition
|
||||||
|
* boolean to their RouteInfo metadata using Ember's `buildRouteInfoMetadata` hook.
|
||||||
|
*/
|
||||||
|
export default class RouteScrollManager extends Service {
|
||||||
|
@service router;
|
||||||
|
|
||||||
|
scrollLocationHistory = new Map();
|
||||||
|
uuid;
|
||||||
|
|
||||||
|
scrollElement = isTesting()
|
||||||
|
? document.getElementById("ember-testing-container")
|
||||||
|
: document.scrollingElement;
|
||||||
|
|
||||||
|
@bind
|
||||||
|
routeWillChange() {
|
||||||
|
if (!this.uuid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.scrollLocationHistory.set(this.uuid, [
|
||||||
|
this.scrollElement.scrollLeft,
|
||||||
|
this.scrollElement.scrollTop,
|
||||||
|
]);
|
||||||
|
this.#pruneOldScrollLocations();
|
||||||
|
}
|
||||||
|
|
||||||
|
@bind
|
||||||
|
routeDidChange(transition) {
|
||||||
|
const newUuid = this.router.location.getState?.().uuid;
|
||||||
|
|
||||||
|
if (newUuid === this.uuid) {
|
||||||
|
// routeDidChange fired without the history state actually changing. Most likely a refresh.
|
||||||
|
// Forget the previously-stored scroll location so that we scroll to the top
|
||||||
|
this.scrollLocationHistory.delete(this.uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.uuid = newUuid;
|
||||||
|
|
||||||
|
if (!this.#shouldScroll(transition.to)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const scrollLocation = this.scrollLocationHistory.get(this.uuid) || [0, 0];
|
||||||
|
schedule("afterRender", () => {
|
||||||
|
this.scrollElement.scrollTo(...scrollLocation);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#pruneOldScrollLocations() {
|
||||||
|
while (this.scrollLocationHistory.size > MAX_SCROLL_LOCATIONS) {
|
||||||
|
// JS Map guarantees keys will be returned in insertion order
|
||||||
|
const oldestUUID = this.scrollLocationHistory.keys().next().value;
|
||||||
|
this.scrollLocationHistory.delete(oldestUUID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#shouldScroll(routeInfo) {
|
||||||
|
// Leafmost route has priority
|
||||||
|
for (let route = routeInfo; route; route = route.parent) {
|
||||||
|
const scrollOnTransition = route.metadata?.scrollOnTransition;
|
||||||
|
if (typeof scrollOnTransition === "boolean") {
|
||||||
|
return scrollOnTransition;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No overrides - default to true
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
super.init(...arguments);
|
||||||
|
this.router.on("routeDidChange", this.routeDidChange);
|
||||||
|
this.router.on("routeWillChange", this.routeWillChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
willDestroy() {
|
||||||
|
this.router.off("routeDidChange", this.routeDidChange);
|
||||||
|
this.router.off("routeWillChange", this.routeWillChange);
|
||||||
|
}
|
||||||
|
}
|
|
@ -31,7 +31,6 @@
|
||||||
@autoAddTopicsToBulkSelect={{this.autoAddTopicsToBulkSelect}}
|
@autoAddTopicsToBulkSelect={{this.autoAddTopicsToBulkSelect}}
|
||||||
@bulkSelectEnabled={{this.bulkSelectEnabled}}
|
@bulkSelectEnabled={{this.bulkSelectEnabled}}
|
||||||
@addTopicsToBulkSelect={{action "addTopicsToBulkSelect"}}
|
@addTopicsToBulkSelect={{action "addTopicsToBulkSelect"}}
|
||||||
as |discoveryTopicList|
|
|
||||||
>
|
>
|
||||||
{{#if this.top}}
|
{{#if this.top}}
|
||||||
<div class="top-lists">
|
<div class="top-lists">
|
||||||
|
@ -90,8 +89,6 @@
|
||||||
@category={{this.category}}
|
@category={{this.category}}
|
||||||
@topics={{this.model.topics}}
|
@topics={{this.model.topics}}
|
||||||
@discoveryList={{true}}
|
@discoveryList={{true}}
|
||||||
@scrollOnLoad={{true}}
|
|
||||||
@onScroll={{discoveryTopicList.saveScrollPosition}}
|
|
||||||
@focusLastVisitedTopic={{true}}
|
@focusLastVisitedTopic={{true}}
|
||||||
/>
|
/>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
|
@ -11,7 +11,6 @@
|
||||||
@model={{this.model}}
|
@model={{this.model}}
|
||||||
@refresh={{action "refresh"}}
|
@refresh={{action "refresh"}}
|
||||||
@incomingCount={{this.topicTrackingState.incomingCount}}
|
@incomingCount={{this.topicTrackingState.incomingCount}}
|
||||||
as |discoveryTopicList|
|
|
||||||
>
|
>
|
||||||
{{#if this.top}}
|
{{#if this.top}}
|
||||||
<div class="top-lists">
|
<div class="top-lists">
|
||||||
|
@ -56,8 +55,6 @@
|
||||||
@expandAllPinned={{this.expandAllPinned}}
|
@expandAllPinned={{this.expandAllPinned}}
|
||||||
@category={{this.category}}
|
@category={{this.category}}
|
||||||
@topics={{this.model.topics}}
|
@topics={{this.model.topics}}
|
||||||
@scrollOnLoad={{true}}
|
|
||||||
@onScroll={{discoveryTopicList.saveScrollPosition}}
|
|
||||||
/>
|
/>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</DiscoveryTopicsList>
|
</DiscoveryTopicsList>
|
||||||
|
|
|
@ -1,8 +1,4 @@
|
||||||
<DSection
|
<DSection @bodyClass="navigation-topics" @class="navigation-container">
|
||||||
@bodyClass="navigation-topics"
|
|
||||||
@class="navigation-container"
|
|
||||||
@scrollTop={{false}}
|
|
||||||
>
|
|
||||||
<DNavigation
|
<DNavigation
|
||||||
@filterMode={{this.filterMode}}
|
@filterMode={{this.filterMode}}
|
||||||
@canCreateTopic={{this.canCreateTopic}}
|
@canCreateTopic={{this.canCreateTopic}}
|
||||||
|
|
|
@ -1,8 +1,4 @@
|
||||||
<DSection
|
<DSection @bodyClass="navigation-filter" @class="navigation-container">
|
||||||
@bodyClass="navigation-filter"
|
|
||||||
@class="navigation-container"
|
|
||||||
@scrollTop={{false}}
|
|
||||||
>
|
|
||||||
<div class="topic-query-filter">
|
<div class="topic-query-filter">
|
||||||
<div class="topic-query-filter__input">
|
<div class="topic-query-filter__input">
|
||||||
{{d-icon "filter" class="topic-query-filter__icon"}}
|
{{d-icon "filter" class="topic-query-filter__icon"}}
|
||||||
|
|
|
@ -95,7 +95,6 @@
|
||||||
@autoAddTopicsToBulkSelect={{this.autoAddTopicsToBulkSelect}}
|
@autoAddTopicsToBulkSelect={{this.autoAddTopicsToBulkSelect}}
|
||||||
@bulkSelectEnabled={{this.bulkSelectEnabled}}
|
@bulkSelectEnabled={{this.bulkSelectEnabled}}
|
||||||
@addTopicsToBulkSelect={{action "addTopicsToBulkSelect"}}
|
@addTopicsToBulkSelect={{action "addTopicsToBulkSelect"}}
|
||||||
as |discoveryTopicList|
|
|
||||||
>
|
>
|
||||||
{{#if this.top}}
|
{{#if this.top}}
|
||||||
<div class="top-lists">
|
<div class="top-lists">
|
||||||
|
@ -140,8 +139,6 @@
|
||||||
@order={{this.order}}
|
@order={{this.order}}
|
||||||
@ascending={{this.ascending}}
|
@ascending={{this.ascending}}
|
||||||
@changeSort={{action "changeSort"}}
|
@changeSort={{action "changeSort"}}
|
||||||
@onScroll={{discoveryTopicList.saveScrollPosition}}
|
|
||||||
@scrollOnLoad={{true}}
|
|
||||||
@focusLastVisitedTopic={{true}}
|
@focusLastVisitedTopic={{true}}
|
||||||
/>
|
/>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
|
@ -43,9 +43,7 @@
|
||||||
@bulkSelectAction={{action "refresh"}}
|
@bulkSelectAction={{action "refresh"}}
|
||||||
@selected={{this.selected}}
|
@selected={{this.selected}}
|
||||||
@tagsForUser={{this.tagsForUser}}
|
@tagsForUser={{this.tagsForUser}}
|
||||||
@onScroll={{this.saveScrollPosition}}
|
|
||||||
@canBulkSelect={{this.canBulkSelect}}
|
@canBulkSelect={{this.canBulkSelect}}
|
||||||
@scrollOnLoad={{true}}
|
|
||||||
@toggleBulkSelect={{action "toggleBulkSelect"}}
|
@toggleBulkSelect={{action "toggleBulkSelect"}}
|
||||||
@updateAutoAddTopicsToBulkSelect={{action
|
@updateAutoAddTopicsToBulkSelect={{action
|
||||||
"updateAutoAddTopicsToBulkSelect"
|
"updateAutoAddTopicsToBulkSelect"
|
||||||
|
|
|
@ -10,8 +10,12 @@ module PageObjects
|
||||||
TOPIC_LIST_BODY_SELECTOR
|
TOPIC_LIST_BODY_SELECTOR
|
||||||
end
|
end
|
||||||
|
|
||||||
def has_topics?(count:)
|
def has_topics?(count: nil)
|
||||||
page.has_css?(TOPIC_LIST_ITEM_SELECTOR, count: count)
|
if count.nil?
|
||||||
|
page.has_css?(TOPIC_LIST_ITEM_SELECTOR)
|
||||||
|
else
|
||||||
|
page.has_css?(TOPIC_LIST_ITEM_SELECTOR, count: count)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def has_no_topics?
|
def has_no_topics?
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
describe "Ember route-scroll-manager service", type: :system do
|
||||||
|
before do
|
||||||
|
Fabricate(:admin)
|
||||||
|
Fabricate.times(50, :post)
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:discovery) { PageObjects::Pages::Discovery.new }
|
||||||
|
let(:topic) { PageObjects::Pages::Topic.new }
|
||||||
|
|
||||||
|
def current_scroll_y
|
||||||
|
page.evaluate_script("window.scrollY")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "scrolls to top when navigating to new routes, and remembers scroll position when going back" do
|
||||||
|
visit("/")
|
||||||
|
expect(page).to have_css("body.navigation-topics")
|
||||||
|
expect(discovery.topic_list).to have_topics
|
||||||
|
|
||||||
|
page.execute_script <<~JS
|
||||||
|
document.querySelectorAll('.topic-list-item')[10].scrollIntoView(true);
|
||||||
|
JS
|
||||||
|
|
||||||
|
topic_list_scroll_y = current_scroll_y
|
||||||
|
try_until_success { expect(topic_list_scroll_y).to be > 0 }
|
||||||
|
|
||||||
|
find(".sidebar-section-link[data-link-name='all-categories']").click
|
||||||
|
|
||||||
|
expect(page).to have_css("body.navigation-categories")
|
||||||
|
|
||||||
|
try_until_success { expect(current_scroll_y).to eq(0) }
|
||||||
|
|
||||||
|
page.go_back
|
||||||
|
|
||||||
|
expect(page).to have_css("body.navigation-topics")
|
||||||
|
expect(discovery.topic_list).to have_topics
|
||||||
|
|
||||||
|
try_until_success { expect(current_scroll_y).to eq(topic_list_scroll_y) }
|
||||||
|
|
||||||
|
# Clicking site logo triggers refresh and scrolls to top
|
||||||
|
find("#site-logo").click
|
||||||
|
try_until_success { expect(current_scroll_y).to eq(0) }
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in New Issue