Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Backport 1.x fixes #2193

Merged
merged 11 commits into from
Jan 15, 2025
56 changes: 36 additions & 20 deletions packages/core/src/history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import { SessionStorage } from './sessionStorage'
import { Page, ScrollRegion } from './types'

const isServer = typeof window === 'undefined'

const queue = new Queue<Promise<void>>()
const isChromeIOS = !isServer && /CriOS/.test(window.navigator.userAgent)

class History {
public rememberedState = 'rememberedState' as const
Expand Down Expand Up @@ -39,23 +39,25 @@ class History {

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

return
}

this.current = page

queue.add(() => {
return this.getPageData(page).then((data) => {
window.history.pushState(
{
page: data,
},
'',
page.url,
)

cb && cb()
// Defer history.pushState to the next event loop tick to prevent timing conflicts.
// Ensure any previous history.replaceState completes before pushState is executed.
const doPush = () => {
this.doPushState({ page: data }, page.url)
cb && cb()
}

if (isChromeIOS) {
setTimeout(doPush)
} else {
doPush()
}
})
})
}
Expand Down Expand Up @@ -141,22 +143,25 @@ class History {

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

return
}

this.current = page

queue.add(() => {
return this.getPageData(page).then((data) => {
this.doReplaceState(
{
page: data,
},
page.url,
)

cb && cb()
// Defer history.replaceState to the next event loop tick to prevent timing conflicts.
// Ensure any previous history.pushState completes before replaceState is executed.
const doReplace = () => {
this.doReplaceState({ page: data }, page.url)
cb && cb()
}

if (isChromeIOS) {
setTimeout(doReplace)
} else {
doReplace()
}
})
})
}
Expand All @@ -180,6 +185,17 @@ class History {
)
}

protected doPushState(
data: {
page: Page | ArrayBuffer
scrollRegions?: ScrollRegion[]
documentScrollPosition?: ScrollRegion
},
url: string,
): void {
window.history.pushState(data, '', url)
}

public getState<T>(key: keyof Page, defaultValue?: T): any {
return this.current?.[key] ?? defaultValue
}
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/initialVisit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@ export class InitialVisit {
currentPage.setUrlHash(window.location.hash)
}

currentPage.set(currentPage.get(), { preserveState: true }).then(() => {
currentPage.set(currentPage.get(), { preserveScroll: true, preserveState: true }).then(() => {
Scroll.restore(history.getScrollRegions())
fireNavigateEvent(currentPage.get())
})
}
Expand Down
20 changes: 16 additions & 4 deletions packages/core/src/navigationType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,23 @@ class NavigationType {
protected type: NavigationTimingType

public constructor() {
if (typeof window !== 'undefined' && window?.performance.getEntriesByType('navigation').length > 0) {
this.type = (window.performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming).type
} else {
this.type = 'navigate'
this.type = this.resolveType()
}

protected resolveType(): NavigationTimingType {
if (typeof window === 'undefined') {
return 'navigate'
}

if (
window.performance &&
window.performance.getEntriesByType &&
window.performance.getEntriesByType('navigation').length > 0
) {
return (window.performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming).type
}

return 'navigate'
}

public get(): NavigationTimingType {
Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,9 @@ class CurrentPage {
}

public setUrlHash(hash: string): void {
this.page.url += hash
if (!this.page.url.includes(hash)) {
this.page.url += hash
}
}

public remember(data: Page['rememberedState']): void {
Expand Down
6 changes: 5 additions & 1 deletion packages/core/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,10 @@ export class Router {
type: TEventName,
callback: (event: GlobalEvent<TEventName>) => GlobalEventResult<TEventName>,
): VoidFunction {
if (typeof window === 'undefined') {
return () => {}
}

return eventHandler.onGlobalEvent(type, callback)
}

Expand Down Expand Up @@ -267,7 +271,7 @@ export class Router {
protected clientVisit(params: ClientSideVisitOptions, { replace = false }: { replace?: boolean } = {}): void {
const current = currentPage.get()

const props = typeof params.props === 'function' ? params.props(current.props) : params.props ?? current.props
const props = typeof params.props === 'function' ? params.props(current.props) : (params.props ?? current.props)

currentPage.set(
{
Expand Down
13 changes: 11 additions & 2 deletions packages/core/src/shouldIntercept.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
export default function shouldIntercept(event: MouseEvent | KeyboardEvent): boolean {
// The actual event passed to this function could be a native JavaScript event
// or a React synthetic event, so we are picking just the keys needed here (that
// are present in both types).

export default function shouldIntercept(
event: Pick<
MouseEvent,
'altKey' | 'ctrlKey' | 'defaultPrevented' | 'target' | 'currentTarget' | 'metaKey' | 'shiftKey' | 'button'
>,
): boolean {
const isLink = (event.currentTarget as HTMLElement).tagName.toLowerCase() === 'a'

return !(
(event.target && (event?.target as HTMLElement).isContentEditable) ||
event.defaultPrevented ||
(isLink && event.which > 1) ||
(isLink && event.altKey) ||
(isLink && event.ctrlKey) ||
(isLink && event.metaKey) ||
Expand Down
2 changes: 1 addition & 1 deletion packages/vue3/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export { default as Deferred } from './deferred'
export { default as Head } from './head'
export { InertiaLinkProps, default as Link } from './link'
export * from './types'
export { InertiaForm, default as useForm } from './useForm'
export { InertiaForm, InertiaFormProps, default as useForm } from './useForm'
export { default as usePoll } from './usePoll'
export { default as usePrefetch } from './usePrefetch'
export { default as useRemember } from './useRemember'
Expand Down
3 changes: 2 additions & 1 deletion packages/vue3/src/useForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { reactive, watch } from 'vue'
type FormDataType = Record<string, FormDataConvertible>
type FormOptions = Omit<VisitOptions, 'data'>

interface InertiaFormProps<TForm extends FormDataType> {
export interface InertiaFormProps<TForm extends FormDataType> {
isDirty: boolean
errors: Partial<Record<keyof TForm, string>>
hasErrors: boolean
Expand Down Expand Up @@ -80,6 +80,7 @@ export default function useForm<TForm extends FormDataType>(

if (typeof fieldOrFields === 'undefined') {
defaults = this.data()
this.isDirty = false
} else {
defaults = Object.assign(
{},
Expand Down
Loading