Skip to content

Commit

Permalink
Merge pull request #2051 from inertiajs/takeover-scroll-restoration
Browse files Browse the repository at this point in the history
[2.x] Take over scroll restoration from browser
  • Loading branch information
joetannenbaum authored Jan 9, 2025
2 parents 50f0ffb + b9695b4 commit b0bd53b
Show file tree
Hide file tree
Showing 15 changed files with 242 additions and 131 deletions.
6 changes: 4 additions & 2 deletions packages/core/src/eventHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class EventHandler {
public init() {
if (typeof window !== 'undefined') {
window.addEventListener('popstate', this.handlePopstateEvent.bind(this))
window.addEventListener('scroll', debounce(Scroll.onWindowScroll.bind(Scroll), 100), true)
}

if (typeof document !== 'undefined') {
Expand Down Expand Up @@ -71,7 +72,7 @@ class EventHandler {
url.hash = window.location.hash

history.replaceState({ ...currentPage.get(), url: url.href })
Scroll.reset(currentPage.get())
Scroll.reset()

return
}
Expand All @@ -81,7 +82,8 @@ class EventHandler {
.decrypt(state.page)
.then((data) => {
currentPage.setQuietly(data, { preserveState: false }).then(() => {
Scroll.restore(currentPage.get())
Scroll.restore(history.getScrollRegions())
Scroll.restoreDocument()
fireNavigateEvent(currentPage.get())
})
})
Expand Down
91 changes: 69 additions & 22 deletions packages/core/src/history.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import { decryptHistory, encryptHistory, historySessionStorageKeys } from './encryption'
import { page as currentPage } from './page'
import Queue from './queue'
import { SessionStorage } from './sessionStorage'
import { Page } from './types'
import { Page, ScrollRegion } from './types'

const isServer = typeof window === 'undefined'

const queue = new Queue<Promise<void>>()

class History {
public rememberedState = 'rememberedState' as const
public scrollRegions = 'scrollRegions' as const
public preserveUrl = false
protected current: Partial<Page> = {}
protected queue: (() => Promise<void>)[] = []
// We need initialState for `restore`
protected initialState: Partial<Page> | null = null

Expand All @@ -36,19 +38,18 @@ class History {
}

if (this.preserveUrl) {
cb && cb();
cb && cb()

return;
return
}

this.current = page

this.addToQueue(() => {
queue.add(() => {
return this.getPageData(page).then((data) => {
window.history.pushState(
{
page: data,
timestamp: Date.now(),
},
'',
page.url,
Expand All @@ -66,13 +67,7 @@ class History {
}

public processQueue(): Promise<void> {
const next = this.queue.shift()

if (next) {
return next().then(() => this.processQueue())
}

return Promise.resolve()
return queue.process()
}

public decrypt(page: Page | null = null): Promise<Page> {
Expand Down Expand Up @@ -101,6 +96,42 @@ class History {
return pageData instanceof ArrayBuffer ? decryptHistory(pageData) : Promise.resolve(pageData)
}

public saveScrollPositions(scrollRegions: ScrollRegion[]): void {
queue.add(() => {
return Promise.resolve().then(() => {
this.doReplaceState(
{
page: window.history.state.page,
scrollRegions,
},
this.current.url!,
)
})
})
}

public saveDocumentScrollPosition(scrollRegion: ScrollRegion): void {
queue.add(() => {
return Promise.resolve().then(() => {
this.doReplaceState(
{
page: window.history.state.page,
documentScrollPosition: scrollRegion,
},
this.current.url!,
)
})
})
}

public getScrollRegions(): ScrollRegion[] {
return window.history.state.scrollRegions || []
}

public getDocumentScrollPosition(): ScrollRegion {
return window.history.state.documentScrollPosition || { top: 0, left: 0 }
}

public replaceState(page: Page, cb: (() => void) | null = null): void {
currentPage.merge(page)

Expand All @@ -109,21 +140,19 @@ class History {
}

if (this.preserveUrl) {
cb && cb();
cb && cb()

return;
return
}

this.current = page

this.addToQueue(() => {
queue.add(() => {
return this.getPageData(page).then((data) => {
window.history.replaceState(
this.doReplaceState(
{
page: data,
timestamp: Date.now(),
},
'',
page.url,
)

Expand All @@ -132,9 +161,23 @@ class History {
})
}

protected addToQueue(fn: () => Promise<void>): void {
this.queue.push(fn)
this.processQueue()
protected doReplaceState(
data: {
page: Page | ArrayBuffer
scrollRegions?: ScrollRegion[]
documentScrollPosition?: ScrollRegion
},
url: string,
): void {
window.history.replaceState(
{
...data,
scrollRegions: data.scrollRegions ?? window.history.state?.scrollRegions,
documentScrollPosition: data.documentScrollPosition ?? window.history.state?.documentScrollPosition,
},
'',
url,
)
}

public getState<T>(key: keyof Page, defaultValue?: T): any {
Expand Down Expand Up @@ -166,4 +209,8 @@ class History {
}
}

if (window.history.scrollRestoration) {
window.history.scrollRestoration = 'manual'
}

export const history = new History()
9 changes: 5 additions & 4 deletions packages/core/src/initialVisit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,13 @@ export class InitialVisit {
return false
}

const scrollRegions = history.getScrollRegions()

history
.decrypt()
.then((data) => {
currentPage.set(data, { preserveScroll: true, preserveState: true }).then(() => {
Scroll.restore(currentPage.get())
Scroll.restore(scrollRegions)
fireNavigateEvent(currentPage.get())
})
})
Expand Down Expand Up @@ -62,9 +64,8 @@ export class InitialVisit {
.decrypt()
.then(() => {
const rememberedState = history.getState<Page['rememberedState']>(history.rememberedState, {})
const scrollRegions = history.getState<Page['scrollRegions']>(history.scrollRegions, [])
const scrollRegions = history.getScrollRegions()
currentPage.remember(rememberedState)
currentPage.scrollRegions(scrollRegions)

currentPage
.set(currentPage.get(), {
Expand All @@ -73,7 +74,7 @@ export class InitialVisit {
})
.then(() => {
if (locationVisit.preserveScroll) {
Scroll.restore(currentPage.get())
Scroll.restore(scrollRegions)
}

fireNavigateEvent(currentPage.get())
Expand Down
31 changes: 21 additions & 10 deletions packages/core/src/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ class CurrentPage {
return
}

page.scrollRegions ??= []
page.rememberedState ??= {}

const location = typeof window !== 'undefined' ? window.location : new URL(page.url)
replace = replace || isSameUrlWithoutHash(hrefToUrl(page.url), location)

Expand All @@ -69,6 +69,9 @@ class CurrentPage {
this.page = page
this.cleared = false

this.page = page
this.cleared = false

if (isNewComponent) {
this.fireEventsFor('newComponent')
}
Expand All @@ -81,14 +84,26 @@ class CurrentPage {

return this.swap({ component, page, preserveState }).then(() => {
if (!preserveScroll) {
Scroll.reset(page)
Scroll.reset()
}

eventHandler.fireInternalEvent('loadDeferredProps')

if (!replace) {
fireNavigateEvent(page)
if (this.isFirstPageLoad) {
this.fireEventsFor('firstLoad')
}

this.isFirstPageLoad = false

return this.swap({ component, page, preserveState }).then(() => {
if (!preserveScroll) {
Scroll.reset()
}

eventHandler.fireInternalEvent('loadDeferredProps')

if (!replace) {
fireNavigateEvent(page)
}
})
})
})
})
Expand Down Expand Up @@ -133,10 +148,6 @@ class CurrentPage {
this.page.rememberedState = data
}

public scrollRegions(regions: Page['scrollRegions']): void {
this.page.scrollRegions = regions
}

public swap({
component,
page,
Expand Down
27 changes: 27 additions & 0 deletions packages/core/src/queue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export default class Queue<T> {
protected items: (() => T)[] = []
protected processingPromise: Promise<void> | null = null

public add(item: () => T) {
this.items.push(item)
return this.process()
}

public process() {
this.processingPromise ??= this.processNext().then(() => {
this.processingPromise = null
})

return this.processingPromise
}

protected processNext(): Promise<void> {
const next = this.items.shift()

if (next) {
return Promise.resolve(next()).then(() => this.processNext())
}

return Promise.resolve()
}
}
38 changes: 3 additions & 35 deletions packages/core/src/response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,44 +3,13 @@ import { fireErrorEvent, fireInvalidEvent, firePrefetchedEvent, fireSuccessEvent
import { history } from './history'
import modal from './modal'
import { page as currentPage } from './page'
import Queue from './queue'
import { RequestParams } from './requestParams'
import { SessionStorage } from './sessionStorage'
import { ActiveVisit, ErrorBag, Errors, Page } from './types'
import { hrefToUrl, isSameUrlWithoutHash, setHashIfSameUrl } from './url'

class ResponseQueue {
protected queue: Response[] = []
protected processing = false

public add(response: Response) {
this.queue.push(response)
}

public async process(): Promise<void> {
if (this.processing) {
return Promise.resolve()
}

this.processing = true
await this.processQueue()
this.processing = false

return Promise.resolve()
}

protected async processQueue(): Promise<void> {
const nextResponse = this.queue.shift()

if (nextResponse) {
await nextResponse.process()
return this.processQueue()
}

return Promise.resolve()
}
}

const queue = new ResponseQueue()
const queue = new Queue<Promise<boolean | void>>()

export class Response {
constructor(
Expand All @@ -60,8 +29,7 @@ export class Response {
}

public async handle() {
queue.add(this)
return queue.process()
return queue.add(() => this.process())
}

public async process() {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ export class Router {

if (!currentPage.isCleared() && !visit.preserveUrl) {
// Save scroll regions for the current page
Scroll.save(currentPage.get())
Scroll.save()
}

const requestParams: PendingVisit & VisitCallbacks = {
Expand Down
Loading

0 comments on commit b0bd53b

Please sign in to comment.