diff --git a/packages/core/src/history.ts b/packages/core/src/history.ts index f74683eeb..6534e3bb9 100644 --- a/packages/core/src/history.ts +++ b/packages/core/src/history.ts @@ -5,8 +5,8 @@ import { SessionStorage } from './sessionStorage' import { Page, ScrollRegion } from './types' const isServer = typeof window === 'undefined' - const queue = new Queue>() +const isChromeIOS = !isServer && /CriOS/.test(window.navigator.userAgent) class History { public rememberedState = 'rememberedState' as const @@ -39,7 +39,6 @@ class History { if (this.preserveUrl) { cb && cb() - return } @@ -47,15 +46,18 @@ class History { 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() + } }) }) } @@ -141,7 +143,6 @@ class History { if (this.preserveUrl) { cb && cb() - return } @@ -149,14 +150,18 @@ class History { 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() + } }) }) } @@ -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(key: keyof Page, defaultValue?: T): any { return this.current?.[key] ?? defaultValue } diff --git a/packages/core/src/initialVisit.ts b/packages/core/src/initialVisit.ts index d4689801f..afc43082d 100644 --- a/packages/core/src/initialVisit.ts +++ b/packages/core/src/initialVisit.ts @@ -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()) }) } diff --git a/packages/core/src/navigationType.ts b/packages/core/src/navigationType.ts index 449451796..0202b47e3 100644 --- a/packages/core/src/navigationType.ts +++ b/packages/core/src/navigationType.ts @@ -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 { diff --git a/packages/core/src/page.ts b/packages/core/src/page.ts index a8f95f3c6..f53f23dd4 100644 --- a/packages/core/src/page.ts +++ b/packages/core/src/page.ts @@ -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 { diff --git a/packages/core/src/router.ts b/packages/core/src/router.ts index 586b6d918..f1f3c1cc8 100644 --- a/packages/core/src/router.ts +++ b/packages/core/src/router.ts @@ -114,6 +114,10 @@ export class Router { type: TEventName, callback: (event: GlobalEvent) => GlobalEventResult, ): VoidFunction { + if (typeof window === 'undefined') { + return () => {} + } + return eventHandler.onGlobalEvent(type, callback) } @@ -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( { diff --git a/packages/core/src/shouldIntercept.ts b/packages/core/src/shouldIntercept.ts index c46b9b5c2..5502e07bc 100644 --- a/packages/core/src/shouldIntercept.ts +++ b/packages/core/src/shouldIntercept.ts @@ -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) || diff --git a/packages/vue3/src/index.ts b/packages/vue3/src/index.ts index 355b44989..ced09daaf 100755 --- a/packages/vue3/src/index.ts +++ b/packages/vue3/src/index.ts @@ -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' diff --git a/packages/vue3/src/useForm.ts b/packages/vue3/src/useForm.ts index 3613c627c..786cbeadb 100644 --- a/packages/vue3/src/useForm.ts +++ b/packages/vue3/src/useForm.ts @@ -6,7 +6,7 @@ import { reactive, watch } from 'vue' type FormDataType = Record type FormOptions = Omit -interface InertiaFormProps { +export interface InertiaFormProps { isDirty: boolean errors: Partial> hasErrors: boolean @@ -80,6 +80,7 @@ export default function useForm( if (typeof fieldOrFields === 'undefined') { defaults = this.data() + this.isDirty = false } else { defaults = Object.assign( {},