11import { Controller } from "@hotwired/stimulus" ;
22
33export default class extends Controller {
4- static targets = [ "input" ] ; // Add Trix as a target
4+ static targets = [ "input" ] ;
55
66 connect ( ) {
77 this . element . setAttribute ( "novalidate" , true ) ; // Disable default HTML5 validation
88 this . element . addEventListener ( "input" , this . checkValidity . bind ( this ) ) ;
99
10- // Track whether the form has been changed
11- this . isDirty = false ;
12- this . isSubmitting = false ; // Flag to track form submission
10+ this . isSubmitting = false ; // Track form submission
11+ this . originalValues = new Map ( ) ; // Store initial field values
12+ this . dirtyFields = new Set ( ) ; // Track which fields have actually changed
1313
14- // Listen for changes in the form to track unsaved changes
15- this . element . addEventListener ( "change" , this . markAsDirty . bind ( this ) ) ;
14+ // Initialize original values for all fields
15+ this . storeInitialValues ( ) ;
1616
17- // Handle form submission to avoid triggering the dirty state warning
17+ // Listen for changes to mark fields dirty
18+ this . element . addEventListener ( "change" , this . markFieldAsDirty . bind ( this ) ) ;
19+
20+ // Handle form submission
1821 this . element . addEventListener ( "submit" , this . handleFormSubmit . bind ( this ) ) ;
1922
20- // Handle Turbo navigation events
23+ // Handle Turbo navigation (unsaved changes warning)
2124 document . addEventListener ( "turbo:before-visit" , this . handleTurboNavigation . bind ( this ) ) ;
2225 }
2326
2427 disconnect ( ) {
25- // Clean up the Turbo event listener
2628 document . removeEventListener ( "turbo:before-visit" , this . handleTurboNavigation . bind ( this ) ) ;
2729 }
2830
29- markAsDirty ( ) {
30- this . isDirty = true ; // Mark the form as "dirty" (changed)
31+ storeInitialValues ( ) {
32+ const fields = this . element . querySelectorAll ( "input, select, textarea" ) ;
33+ fields . forEach ( field => {
34+ this . originalValues . set ( field , field . value ) ;
35+ } ) ;
36+ }
37+
38+ markFieldAsDirty ( event ) {
39+ const field = event . target ;
40+
41+ if ( this . originalValues . get ( field ) !== field . value ) {
42+ this . dirtyFields . add ( field ) ;
43+ } else {
44+ this . dirtyFields . delete ( field ) ;
45+ }
46+ }
47+
48+ isFormDirty ( ) {
49+ return this . dirtyFields . size > 0 ;
3150 }
3251
3352 handleFormSubmit ( event ) {
34- // Check if the form is valid before submission
3553 if ( ! this . element . checkValidity ( ) ) {
36- event . preventDefault ( ) ; // Prevent form submission
37- this . checkAllFields ( ) ; // Manually validate all fields
54+ event . preventDefault ( ) ;
55+ this . checkAllFields ( ) ;
3856 return ;
3957 }
40-
41- this . isSubmitting = true ; // Mark the form as being submitted to prevent warning
58+
59+ this . isSubmitting = true ;
4260 }
4361
4462 handleTurboNavigation ( event ) {
45- // Only show the unsaved changes warning if the form is dirty and not currently submitting
46- if ( this . isDirty && ! this . isSubmitting ) {
63+ if ( this . isFormDirty ( ) && ! this . isSubmitting ) {
4764 const confirmation = confirm ( "You have unsaved changes. Are you sure you want to leave?" ) ;
4865 if ( ! confirmation ) {
49- event . preventDefault ( ) ; // Prevent Turbo from navigating if the user cancels
66+ event . preventDefault ( ) ;
5067 }
5168 }
5269 }
5370
54- // Add a method to check all fields
5571 checkAllFields ( ) {
5672 const fields = this . element . querySelectorAll ( "input, select, textarea" ) ;
57- fields . forEach ( field => {
58- this . checkValidity ( { target : field } ) ;
59- } ) ;
73+ fields . forEach ( field => this . checkValidity ( { target : field } ) ) ;
6074 }
6175
6276 checkValidity ( event ) {
6377 const field = event . target ;
6478
65- // Skip validation for Trix hidden input fields
66- if ( field . closest ( "trix-editor" ) ) return this . checkTrixValidity ( event ) ;
79+ if ( field . closest ( "trix-editor" ) ) {
80+ return this . checkTrixValidity ( event ) ;
81+ }
6782
68- // If field is valid but empty, remove validation classes
6983 if ( field . checkValidity ( ) && field . value . trim ( ) === "" ) {
7084 field . classList . remove ( "is-valid" , "is-invalid" ) ;
7185 this . hideErrorMessage ( field ) ;
72- }
73- // If field is valid and not empty, apply valid state
74- else if ( field . checkValidity ( ) ) {
86+ } else if ( field . checkValidity ( ) ) {
7587 field . classList . remove ( "is-invalid" ) ;
7688 field . classList . add ( "is-valid" ) ;
7789 this . hideErrorMessage ( field ) ;
78- }
79- // If field is invalid, apply invalid state
80- else {
90+ } else {
8191 field . classList . add ( "is-invalid" ) ;
8292 this . showErrorMessage ( field ) ;
8393 }
@@ -86,22 +96,16 @@ export default class extends Controller {
8696 checkTrixValidity ( event ) {
8797 const editor = event . target ;
8898 const field = editor . closest ( "trix-editor" ) ;
89-
9099 const editorContent = editor . editor . getDocument ( ) . toString ( ) . trim ( ) ;
91100
92- // If Trix content is empty, remove validation classes
93101 if ( editorContent === "" ) {
94102 field . classList . remove ( "is-valid" , "is-invalid" ) ;
95103 this . hideErrorMessage ( field ) ;
96- }
97- // If Trix content is not empty, apply valid state
98- else if ( editorContent . length > 0 ) {
104+ } else if ( editorContent . length > 0 ) {
99105 field . classList . remove ( "is-invalid" ) ;
100106 field . classList . add ( "is-valid" ) ;
101107 this . hideErrorMessage ( field ) ;
102- }
103- // If Trix content is considered invalid (you can define conditions here)
104- else {
108+ } else {
105109 field . classList . add ( "is-invalid" ) ;
106110 this . showErrorMessage ( field ) ;
107111 }
@@ -113,18 +117,22 @@ export default class extends Controller {
113117 field . classList . remove ( "is-invalid" , "is-valid" ) ;
114118 this . hideErrorMessage ( field ) ;
115119 } ) ;
120+
121+ // Reset dirty state
122+ this . dirtyFields . clear ( ) ;
123+ this . storeInitialValues ( ) ; // Re-store current values as "original"
116124 }
117125
118126 showErrorMessage ( field ) {
119127 const errorMessage = field . nextElementSibling ;
120- if ( errorMessage && errorMessage . classList . contains ( "invalid-feedback" ) ) {
128+ if ( errorMessage ? .classList . contains ( "invalid-feedback" ) ) {
121129 errorMessage . style . display = "block" ;
122130 }
123131 }
124132
125133 hideErrorMessage ( field ) {
126134 const errorMessage = field . nextElementSibling ;
127- if ( errorMessage && errorMessage . classList . contains ( "invalid-feedback" ) ) {
135+ if ( errorMessage ? .classList . contains ( "invalid-feedback" ) ) {
128136 errorMessage . style . display = "none" ;
129137 }
130138 }
0 commit comments