@@ -12,6 +12,8 @@ export default class extends Controller {
1212 this . _onSubmit = this . handleFormSubmit . bind ( this ) ;
1313 this . _onSubmitEnd = this . handleSubmitEnd . bind ( this ) ;
1414 this . _onBeforeVisit = this . handleTurboNavigation . bind ( this ) ;
15+ this . _onBeforeCache = this . handleBeforeCache ?. bind ( this ) || this . handleBeforeCache . bind ( this ) ;
16+ this . _onBeforeUnload = this . handleBeforeUnload . bind ( this ) ;
1517
1618 this . element . addEventListener ( "input" , this . _onInput ) ;
1719
@@ -31,28 +33,37 @@ export default class extends Controller {
3133
3234 // Handle Turbo navigation (unsaved changes warning)
3335 document . addEventListener ( "turbo:before-visit" , this . _onBeforeVisit ) ;
36+ // Clean up transient UI before Turbo caches the page
37+ document . addEventListener ( "turbo:before-cache" , this . _onBeforeCache ) ;
38+ // Warn on full page unload if there are unsaved changes
39+ window . addEventListener ( "beforeunload" , this . _onBeforeUnload ) ;
3440 }
3541
3642 disconnect ( ) {
3743 // Remove all listeners using the cached handler references
3844 if ( this . _onBeforeVisit ) document . removeEventListener ( "turbo:before-visit" , this . _onBeforeVisit ) ;
45+ if ( this . _onBeforeCache ) document . removeEventListener ( "turbo:before-cache" , this . _onBeforeCache ) ;
3946 if ( this . _onSubmitEnd ) this . element . removeEventListener ( "turbo:submit-end" , this . _onSubmitEnd ) ;
4047 if ( this . _onSubmit ) this . element . removeEventListener ( "submit" , this . _onSubmit ) ;
4148 if ( this . _onChange ) this . element . removeEventListener ( "change" , this . _onChange ) ;
4249 if ( this . _onInput ) this . element . removeEventListener ( "input" , this . _onInput ) ;
50+ if ( this . _onBeforeUnload ) window . removeEventListener ( "beforeunload" , this . _onBeforeUnload ) ;
4351 }
4452
4553 storeInitialValues ( ) {
4654 const fields = this . element . querySelectorAll ( "input, select, textarea" ) ;
4755 fields . forEach ( field => {
48- this . originalValues . set ( field , field . value ) ;
56+ this . originalValues . set ( field , this . getFieldValue ( field ) ) ;
4957 } ) ;
5058 }
5159
5260 markFieldAsDirty ( event ) {
5361 const field = event . target ;
5462
55- if ( this . originalValues . get ( field ) !== field . value ) {
63+ const original = this . originalValues . get ( field ) ;
64+ const current = this . getFieldValue ( field ) ;
65+
66+ if ( ! this . valuesEqual ( original , current ) ) {
5667 this . dirtyFields . add ( field ) ;
5768 } else {
5869 this . dirtyFields . delete ( field ) ;
@@ -150,6 +161,56 @@ export default class extends Controller {
150161 this . storeInitialValues ( ) ; // Re-store current values as "original"
151162 }
152163
164+ // Normalize field value for dirty tracking
165+ getFieldValue ( field ) {
166+ const tag = field . tagName . toLowerCase ( ) ;
167+ if ( tag === "input" ) {
168+ const type = ( field . getAttribute ( "type" ) || "text" ) . toLowerCase ( ) ;
169+ if ( type === "checkbox" || type === "radio" ) {
170+ return field . checked ;
171+ }
172+ return field . value ;
173+ }
174+ if ( tag === "select" ) {
175+ if ( field . multiple ) {
176+ return Array . from ( field . options )
177+ . filter ( o => o . selected )
178+ . map ( o => o . value )
179+ . sort ( ) ;
180+ }
181+ return field . value ;
182+ }
183+ // textarea or others
184+ return field . value ;
185+ }
186+
187+ valuesEqual ( a , b ) {
188+ if ( Array . isArray ( a ) && Array . isArray ( b ) ) {
189+ if ( a . length !== b . length ) return false ;
190+ for ( let i = 0 ; i < a . length ; i ++ ) {
191+ if ( a [ i ] !== b [ i ] ) return false ;
192+ }
193+ return true ;
194+ }
195+ return a === b ;
196+ }
197+
198+ // Clear transient UI before Turbo caches the page
199+ handleBeforeCache ( ) {
200+ this . resetValidation ( ) ;
201+ this . isSubmitting = false ;
202+ }
203+
204+ // Show native prompt on hard reload/close if form is dirty
205+ handleBeforeUnload ( event ) {
206+ if ( this . isFormDirty ( ) && ! this . isSubmitting ) {
207+ event . preventDefault ( ) ;
208+ event . returnValue = "" ; // Required for Chrome to show prompt
209+ return "" ; // For older browsers
210+ }
211+ return undefined ;
212+ }
213+
153214 showErrorMessage ( field ) {
154215 const errorMessage = field . nextElementSibling ;
155216 if ( errorMessage ?. classList . contains ( "invalid-feedback" ) ) {
0 commit comments