From b81f8f9729cd577c5356a60b96a0c5a748b95130 Mon Sep 17 00:00:00 2001 From: Joe Tannenbaum Date: Wed, 15 Jan 2025 15:33:42 -0500 Subject: [PATCH 01/11] preserve scroll on initial page load --- packages/core/src/initialVisit.ts | 2 +- packages/core/src/page.ts | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/core/src/initialVisit.ts b/packages/core/src/initialVisit.ts index d4689801f..566fceb33 100644 --- a/packages/core/src/initialVisit.ts +++ b/packages/core/src/initialVisit.ts @@ -92,7 +92,7 @@ export class InitialVisit { currentPage.setUrlHash(window.location.hash) } - currentPage.set(currentPage.get(), { preserveState: true }).then(() => { + currentPage.set(currentPage.get(), { preserveScroll: true, preserveState: true }).then(() => { fireNavigateEvent(currentPage.get()) }) } diff --git a/packages/core/src/page.ts b/packages/core/src/page.ts index a8f95f3c6..8265d7007 100644 --- a/packages/core/src/page.ts +++ b/packages/core/src/page.ts @@ -80,9 +80,7 @@ class CurrentPage { this.isFirstPageLoad = false return this.swap({ component, page, preserveState }).then(() => { - if (!preserveScroll) { - Scroll.reset() - } + preserveScroll ? Scroll.restore(history.getScrollRegions()) : Scroll.reset() eventHandler.fireInternalEvent('loadDeferredProps') From 5125df496b6665e6f90eca8770a0e31230013d94 Mon Sep 17 00:00:00 2001 From: Joe Tannenbaum Date: Wed, 15 Jan 2025 15:36:56 -0500 Subject: [PATCH 02/11] in ssr mode return early --- packages/core/src/router.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/core/src/router.ts b/packages/core/src/router.ts index 586b6d918..fdd2f0aff 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) } From a305357d07ceeda68ba52bd078f5e51dc468c032 Mon Sep 17 00:00:00 2001 From: Joe Tannenbaum Date: Wed, 15 Jan 2025 15:37:15 -0500 Subject: [PATCH 03/11] parens --- packages/core/src/router.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/router.ts b/packages/core/src/router.ts index fdd2f0aff..f1f3c1cc8 100644 --- a/packages/core/src/router.ts +++ b/packages/core/src/router.ts @@ -271,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( { From 24f40c80830090ff2160158905d734d27335a47a Mon Sep 17 00:00:00 2001 From: Joe Tannenbaum Date: Wed, 15 Jan 2025 15:49:31 -0500 Subject: [PATCH 04/11] fix chrome ios navigation --- packages/core/src/history.ts | 66 +++++++++++++++++++++++------------- 1 file changed, 42 insertions(+), 24 deletions(-) diff --git a/packages/core/src/history.ts b/packages/core/src/history.ts index f74683eeb..ff560f5c1 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 @@ -37,25 +37,28 @@ class History { return } - if (this.preserveUrl) { - cb && cb() + cb = cb ?? (() => {}) - return + if (this.preserveUrl) { + return cb() } 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() + } + + if (isChromeIOS) { + setTimeout(doPush) + } else { + doPush() + } }) }) } @@ -139,24 +142,28 @@ class History { return } - if (this.preserveUrl) { - cb && cb() + cb = cb ?? (() => {}) - return + if (this.preserveUrl) { + return cb() } 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() + } + + if (isChromeIOS) { + setTimeout(doReplace) + } else { + doReplace() + } }) }) } @@ -180,6 +187,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 } From f673747671e9100dc9deea137e9ab3b4582eef4e Mon Sep 17 00:00:00 2001 From: Joe Tannenbaum Date: Wed, 15 Jan 2025 15:53:24 -0500 Subject: [PATCH 05/11] fix navigation type for safari 10 --- packages/core/src/navigationType.ts | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) 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 { From 18ae08d4c16ef00a8d4a7fd37985101a073233b4 Mon Sep 17 00:00:00 2001 From: Joe Tannenbaum Date: Wed, 15 Jan 2025 15:56:45 -0500 Subject: [PATCH 06/11] export form props for vue adapter --- packages/vue3/src/index.ts | 2 +- packages/vue3/src/useForm.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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..3289c2b32 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 From 3a56916d2013552135dd9862ea901b19fc893b1a Mon Sep 17 00:00:00 2001 From: Joe Tannenbaum Date: Wed, 15 Jan 2025 15:58:07 -0500 Subject: [PATCH 07/11] stop ts complaining --- packages/core/src/history.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/core/src/history.ts b/packages/core/src/history.ts index ff560f5c1..6534e3bb9 100644 --- a/packages/core/src/history.ts +++ b/packages/core/src/history.ts @@ -37,10 +37,9 @@ class History { return } - cb = cb ?? (() => {}) - if (this.preserveUrl) { - return cb() + cb && cb() + return } this.current = page @@ -51,7 +50,7 @@ class History { // Ensure any previous history.replaceState completes before pushState is executed. const doPush = () => { this.doPushState({ page: data }, page.url) - cb() + cb && cb() } if (isChromeIOS) { @@ -142,10 +141,9 @@ class History { return } - cb = cb ?? (() => {}) - if (this.preserveUrl) { - return cb() + cb && cb() + return } this.current = page @@ -156,7 +154,7 @@ class History { // Ensure any previous history.pushState completes before replaceState is executed. const doReplace = () => { this.doReplaceState({ page: data }, page.url) - cb() + cb && cb() } if (isChromeIOS) { From 6ad0d05d7237784f0583b9a9f0e23d2807aaad5b Mon Sep 17 00:00:00 2001 From: Joe Tannenbaum Date: Wed, 15 Jan 2025 16:00:27 -0500 Subject: [PATCH 08/11] fix vue is dirty after form defaults --- packages/vue3/src/useForm.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/vue3/src/useForm.ts b/packages/vue3/src/useForm.ts index 3289c2b32..786cbeadb 100644 --- a/packages/vue3/src/useForm.ts +++ b/packages/vue3/src/useForm.ts @@ -80,6 +80,7 @@ export default function useForm( if (typeof fieldOrFields === 'undefined') { defaults = this.data() + this.isDirty = false } else { defaults = Object.assign( {}, From db95d4cda8b860b3084c6c5497d5561bca9a2108 Mon Sep 17 00:00:00 2001 From: Joe Tannenbaum Date: Wed, 15 Jan 2025 16:37:57 -0500 Subject: [PATCH 09/11] move scroll restore to initial page handler --- packages/core/src/initialVisit.ts | 1 + packages/core/src/page.ts | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/core/src/initialVisit.ts b/packages/core/src/initialVisit.ts index 566fceb33..afc43082d 100644 --- a/packages/core/src/initialVisit.ts +++ b/packages/core/src/initialVisit.ts @@ -93,6 +93,7 @@ export class InitialVisit { } currentPage.set(currentPage.get(), { preserveScroll: true, preserveState: true }).then(() => { + Scroll.restore(history.getScrollRegions()) fireNavigateEvent(currentPage.get()) }) } diff --git a/packages/core/src/page.ts b/packages/core/src/page.ts index 8265d7007..a8f95f3c6 100644 --- a/packages/core/src/page.ts +++ b/packages/core/src/page.ts @@ -80,7 +80,9 @@ class CurrentPage { this.isFirstPageLoad = false return this.swap({ component, page, preserveState }).then(() => { - preserveScroll ? Scroll.restore(history.getScrollRegions()) : Scroll.reset() + if (!preserveScroll) { + Scroll.reset() + } eventHandler.fireInternalEvent('loadDeferredProps') From f61d2f9da263ddd01f788aa6206ad1647daf4225 Mon Sep 17 00:00:00 2001 From: Joe Tannenbaum Date: Wed, 15 Jan 2025 16:40:45 -0500 Subject: [PATCH 10/11] prevent double hash in react strict mode --- packages/core/src/page.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 { From da7ec4c9f5b20de0ac7efb8b41ba88191d97dba9 Mon Sep 17 00:00:00 2001 From: Joe Tannenbaum Date: Wed, 15 Jan 2025 16:53:51 -0500 Subject: [PATCH 11/11] fix cmd + click for react --- packages/core/src/shouldIntercept.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) 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) ||