feat: add scroll state persistence to RantFeed with per-page state keys
Implement saveScrollState, restoreScrollState, and clearScrollState methods in RantFeed component, storing anchor rant ID and scroll offset per page. Wire up initFeed calls in CollabsPage, HomePage, StoriesPage, WeeklyPage, and SearchPage to set unique state keys (feed_state_collabs, feed_state_home, etc.) enabling seamless scroll restoration on navigation back.
This commit is contained in:
parent
fefd47f703
commit
f76c44a7e6
@ -21,11 +21,130 @@ class RantFeed extends BaseComponent {
|
||||
this.sort = this.getAttr('sort') || 'recent';
|
||||
this.feedType = this.getAttr('feed-type') || 'rants';
|
||||
this.blockChangeHandler = () => this.render();
|
||||
this.stateKey = null;
|
||||
this.pendingRestore = null;
|
||||
|
||||
this.render();
|
||||
this.bindEvents();
|
||||
}
|
||||
|
||||
setStateKey(key) {
|
||||
this.stateKey = key;
|
||||
}
|
||||
|
||||
getTopVisibleRant() {
|
||||
const cards = this.$$('rant-card');
|
||||
if (!cards || cards.length === 0) return null;
|
||||
|
||||
for (const card of cards) {
|
||||
const rect = card.getBoundingClientRect();
|
||||
if (rect.top >= -rect.height && rect.top < window.innerHeight) {
|
||||
const rantId = card.getRantId();
|
||||
const offsetFromTop = rect.top;
|
||||
return { rantId, offsetFromTop };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
saveScrollState() {
|
||||
if (!this.stateKey) return;
|
||||
|
||||
const topRant = this.getTopVisibleRant();
|
||||
if (!topRant) return;
|
||||
|
||||
const state = {
|
||||
anchorRantId: topRant.rantId,
|
||||
offsetFromTop: topRant.offsetFromTop,
|
||||
skip: this.skip,
|
||||
sort: this.sort
|
||||
};
|
||||
|
||||
this.getStorage()?.set(this.stateKey, state);
|
||||
}
|
||||
|
||||
clearScrollState() {
|
||||
if (!this.stateKey) return;
|
||||
this.getStorage()?.remove(this.stateKey);
|
||||
}
|
||||
|
||||
async restoreScrollState() {
|
||||
if (!this.stateKey) return false;
|
||||
|
||||
const state = this.getStorage()?.get(this.stateKey);
|
||||
if (!state || state.sort !== this.sort) {
|
||||
this.clearScrollState();
|
||||
return false;
|
||||
}
|
||||
|
||||
this.pendingRestore = state;
|
||||
await this.loadWithLimit(state.skip || this.limit);
|
||||
return true;
|
||||
}
|
||||
|
||||
scrollToAnchor() {
|
||||
if (!this.pendingRestore) return;
|
||||
|
||||
const { anchorRantId, offsetFromTop } = this.pendingRestore;
|
||||
this.pendingRestore = null;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
const card = this.$(`rant-card[rant-id="${anchorRantId}"]`);
|
||||
if (card) {
|
||||
const rect = card.getBoundingClientRect();
|
||||
const scrollY = window.scrollY + rect.top - offsetFromTop;
|
||||
window.scrollTo(0, Math.max(0, scrollY));
|
||||
}
|
||||
this.clearScrollState();
|
||||
});
|
||||
}
|
||||
|
||||
async loadWithLimit(limit) {
|
||||
if (this.isLoading) return;
|
||||
|
||||
this.rants = [];
|
||||
this.skip = 0;
|
||||
this.hasMore = true;
|
||||
this.isLoading = true;
|
||||
this.updateLoadingState();
|
||||
|
||||
try {
|
||||
let result;
|
||||
const api = this.getApi();
|
||||
|
||||
switch (this.feedType) {
|
||||
case 'weekly':
|
||||
result = await api?.getWeeklyRants(this.sort, limit, 0);
|
||||
break;
|
||||
case 'collabs':
|
||||
result = await api?.getCollabs(this.sort, limit, 0);
|
||||
break;
|
||||
case 'stories':
|
||||
result = await api?.getStories(this.sort, limit, 0);
|
||||
break;
|
||||
case 'search':
|
||||
break;
|
||||
default:
|
||||
result = await api?.getRants(this.sort, limit, 0);
|
||||
}
|
||||
|
||||
if (result?.success) {
|
||||
const newRants = result.rants || [];
|
||||
this.rants = newRants;
|
||||
this.skip = newRants.length;
|
||||
this.hasMore = newRants.length >= limit;
|
||||
} else {
|
||||
this.hasMore = false;
|
||||
}
|
||||
} catch (error) {
|
||||
this.hasMore = false;
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
this.render();
|
||||
this.scrollToAnchor();
|
||||
}
|
||||
}
|
||||
|
||||
getBlock() {
|
||||
return window.app?.block;
|
||||
}
|
||||
@ -207,11 +326,15 @@ class RantFeed extends BaseComponent {
|
||||
this.intersectionObserver = observer;
|
||||
}
|
||||
|
||||
onConnected() {
|
||||
this.load(true);
|
||||
async onConnected() {
|
||||
const restored = await this.restoreScrollState();
|
||||
if (!restored) {
|
||||
this.load(true);
|
||||
}
|
||||
}
|
||||
|
||||
onDisconnected() {
|
||||
this.saveScrollState();
|
||||
if (this.intersectionObserver) {
|
||||
this.intersectionObserver.disconnect();
|
||||
}
|
||||
@ -223,10 +346,12 @@ class RantFeed extends BaseComponent {
|
||||
onAttributeChanged(name, oldValue, newValue) {
|
||||
if (name === 'sort' && oldValue !== newValue) {
|
||||
this.sort = newValue;
|
||||
this.clearScrollState();
|
||||
this.load(true);
|
||||
}
|
||||
if (name === 'feed-type' && oldValue !== newValue) {
|
||||
this.feedType = newValue;
|
||||
this.clearScrollState();
|
||||
this.load(true);
|
||||
}
|
||||
}
|
||||
@ -234,12 +359,14 @@ class RantFeed extends BaseComponent {
|
||||
setSort(sort) {
|
||||
this.sort = sort;
|
||||
this.setAttr('sort', sort);
|
||||
this.clearScrollState();
|
||||
this.load(true);
|
||||
}
|
||||
|
||||
setFeedType(type) {
|
||||
this.feedType = type;
|
||||
this.setAttr('feed-type', type);
|
||||
this.clearScrollState();
|
||||
this.load(true);
|
||||
}
|
||||
|
||||
|
||||
@ -10,6 +10,7 @@ import { BaseComponent } from '../components/base-component.js';
|
||||
class CollabsPage extends BaseComponent {
|
||||
init() {
|
||||
this.render();
|
||||
this.initFeed();
|
||||
}
|
||||
|
||||
render() {
|
||||
@ -23,6 +24,13 @@ class CollabsPage extends BaseComponent {
|
||||
`);
|
||||
}
|
||||
|
||||
initFeed() {
|
||||
const feed = this.$('rant-feed');
|
||||
if (feed) {
|
||||
feed.setStateKey('feed_state_collabs');
|
||||
}
|
||||
}
|
||||
|
||||
refresh() {
|
||||
const feed = this.$('rant-feed');
|
||||
if (feed) {
|
||||
|
||||
@ -14,6 +14,7 @@ class HomePage extends BaseComponent {
|
||||
|
||||
init() {
|
||||
this.render();
|
||||
this.initFeed();
|
||||
}
|
||||
|
||||
render() {
|
||||
@ -26,6 +27,13 @@ class HomePage extends BaseComponent {
|
||||
`);
|
||||
}
|
||||
|
||||
initFeed() {
|
||||
const feed = this.$('rant-feed');
|
||||
if (feed) {
|
||||
feed.setStateKey('feed_state_home');
|
||||
}
|
||||
}
|
||||
|
||||
onAttributeChanged(name, oldValue, newValue) {
|
||||
if (name === 'sort') {
|
||||
const feed = this.$('rant-feed');
|
||||
|
||||
@ -13,6 +13,7 @@ class SearchPage extends BaseComponent {
|
||||
}
|
||||
|
||||
init() {
|
||||
this.currentQuery = null;
|
||||
this.render();
|
||||
this.bindEvents();
|
||||
}
|
||||
@ -50,10 +51,19 @@ class SearchPage extends BaseComponent {
|
||||
`);
|
||||
|
||||
if (query) {
|
||||
this.currentQuery = query;
|
||||
this.initFeed();
|
||||
this.search(query);
|
||||
}
|
||||
}
|
||||
|
||||
initFeed() {
|
||||
const feed = this.$('rant-feed');
|
||||
if (feed && this.currentQuery) {
|
||||
feed.setStateKey('feed_state_search_' + this.currentQuery);
|
||||
}
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
this.on(this, 'submit', this.handleSubmit);
|
||||
}
|
||||
|
||||
@ -10,6 +10,7 @@ import { BaseComponent } from '../components/base-component.js';
|
||||
class StoriesPage extends BaseComponent {
|
||||
init() {
|
||||
this.render();
|
||||
this.initFeed();
|
||||
}
|
||||
|
||||
render() {
|
||||
@ -23,6 +24,13 @@ class StoriesPage extends BaseComponent {
|
||||
`);
|
||||
}
|
||||
|
||||
initFeed() {
|
||||
const feed = this.$('rant-feed');
|
||||
if (feed) {
|
||||
feed.setStateKey('feed_state_stories');
|
||||
}
|
||||
}
|
||||
|
||||
refresh() {
|
||||
const feed = this.$('rant-feed');
|
||||
if (feed) {
|
||||
|
||||
@ -10,6 +10,7 @@ import { BaseComponent } from '../components/base-component.js';
|
||||
class WeeklyPage extends BaseComponent {
|
||||
init() {
|
||||
this.render();
|
||||
this.initFeed();
|
||||
}
|
||||
|
||||
render() {
|
||||
@ -23,6 +24,13 @@ class WeeklyPage extends BaseComponent {
|
||||
`);
|
||||
}
|
||||
|
||||
initFeed() {
|
||||
const feed = this.$('rant-feed');
|
||||
if (feed) {
|
||||
feed.setStateKey('feed_state_weekly');
|
||||
}
|
||||
}
|
||||
|
||||
refresh() {
|
||||
const feed = this.$('rant-feed');
|
||||
if (feed) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user