| 1 |
/** |
| 2 |
* Course Builder - Form State Management |
| 3 |
* Track unsaved changes and handle tab navigation |
| 4 |
* |
| 5 |
* @since 4.3.0 |
| 6 |
* @version 1.0.0 |
| 7 |
*/ |
| 8 |
|
| 9 |
export class BuilderFormState { |
| 10 |
constructor() { |
| 11 |
this.hasUnsavedChanges = false; |
| 12 |
this.formElements = []; |
| 13 |
this.originalValues = new Map(); |
| 14 |
|
| 15 |
this.init(); |
| 16 |
} |
| 17 |
|
| 18 |
init() { |
| 19 |
this.bindEvents(); |
| 20 |
this.captureOriginalValues(); |
| 21 |
} |
| 22 |
|
| 23 |
/** |
| 24 |
* Capture original form values for comparison |
| 25 |
*/ |
| 26 |
captureOriginalValues() { |
| 27 |
const forms = document.querySelectorAll( '.cb-section__course-edit, .lp-cb-tab-content' ); |
| 28 |
|
| 29 |
forms.forEach( form => { |
| 30 |
const inputs = form.querySelectorAll( 'input, textarea, select' ); |
| 31 |
inputs.forEach( input => { |
| 32 |
const key = input.name || input.id; |
| 33 |
if ( key ) { |
| 34 |
this.originalValues.set( key, this.getInputValue( input ) ); |
| 35 |
} |
| 36 |
}); |
| 37 |
}); |
| 38 |
} |
| 39 |
|
| 40 |
/** |
| 41 |
* Get input value based on type |
| 42 |
*/ |
| 43 |
getInputValue( input ) { |
| 44 |
if ( input.type === 'checkbox' ) { |
| 45 |
return input.checked; |
| 46 |
} |
| 47 |
if ( input.type === 'radio' ) { |
| 48 |
return input.checked ? input.value : null; |
| 49 |
} |
| 50 |
return input.value; |
| 51 |
} |
| 52 |
|
| 53 |
/** |
| 54 |
* Bind all events |
| 55 |
*/ |
| 56 |
bindEvents() { |
| 57 |
// Track form changes |
| 58 |
document.addEventListener( 'input', this.handleFormChange.bind( this ) ); |
| 59 |
document.addEventListener( 'change', this.handleFormChange.bind( this ) ); |
| 60 |
|
| 61 |
// TinyMCE changes (if available) |
| 62 |
this.bindTinyMCEChanges(); |
| 63 |
|
| 64 |
// Tab navigation warning |
| 65 |
document.addEventListener( 'click', this.handleTabClick.bind( this ) ); |
| 66 |
|
| 67 |
// Browser navigation warning |
| 68 |
window.addEventListener( 'beforeunload', this.handleBeforeUnload.bind( this ) ); |
| 69 |
|
| 70 |
// Reset state after successful save |
| 71 |
document.addEventListener( 'lp-course-builder-saved', this.resetState.bind( this ) ); |
| 72 |
} |
| 73 |
|
| 74 |
/** |
| 75 |
* Bind TinyMCE editor changes |
| 76 |
*/ |
| 77 |
bindTinyMCEChanges() { |
| 78 |
// Wait for TinyMCE to be ready |
| 79 |
if ( typeof tinymce !== 'undefined' ) { |
| 80 |
tinymce.on( 'AddEditor', ( e ) => { |
| 81 |
e.editor.on( 'change', () => { |
| 82 |
this.markAsChanged(); |
| 83 |
}); |
| 84 |
e.editor.on( 'keyup', () => { |
| 85 |
this.markAsChanged(); |
| 86 |
}); |
| 87 |
}); |
| 88 |
} |
| 89 |
} |
| 90 |
|
| 91 |
/** |
| 92 |
* Handle form input changes |
| 93 |
*/ |
| 94 |
handleFormChange( e ) { |
| 95 |
const target = e.target; |
| 96 |
|
| 97 |
// Ignore curriculum changes |
| 98 |
if ( target.closest( '#lp-course-edit-curriculum' ) ) { |
| 99 |
return; |
| 100 |
} |
| 101 |
|
| 102 |
// Ignore question settings form - it auto-saves via AJAX |
| 103 |
if ( target.closest( '.lp-form-setting-question' ) ) { |
| 104 |
return; |
| 105 |
} |
| 106 |
|
| 107 |
// Ignore lesson settings form - it auto-saves via AJAX |
| 108 |
if ( target.closest( '.lp-form-setting-lesson' ) ) { |
| 109 |
return; |
| 110 |
} |
| 111 |
|
| 112 |
// Ignore quiz questions list - question changes are tracked separately |
| 113 |
if ( target.closest( '.lp-quiz-questions-list' ) || target.closest( '.lp-edit-quiz-wrap' ) ) { |
| 114 |
return; |
| 115 |
} |
| 116 |
|
| 117 |
// Check if target is within course builder forms |
| 118 |
if ( target.closest( '.cb-section__course-edit' ) || |
| 119 |
target.closest( '.lp-cb-tab-content' ) || |
| 120 |
target.closest( '.lp-form-setting-course' ) || |
| 121 |
target.closest( '.lp-form-setting-quiz' ) || |
| 122 |
target.closest( '.cb-section__quiz-edit' ) ) { |
| 123 |
this.markAsChanged(); |
| 124 |
} |
| 125 |
} |
| 126 |
|
| 127 |
/** |
| 128 |
* Mark form as having unsaved changes |
| 129 |
*/ |
| 130 |
markAsChanged() { |
| 131 |
if ( ! this.hasUnsavedChanges ) { |
| 132 |
this.hasUnsavedChanges = true; |
| 133 |
this.updateSaveButtonState(); |
| 134 |
} |
| 135 |
} |
| 136 |
|
| 137 |
/** |
| 138 |
* Update save button visual state |
| 139 |
*/ |
| 140 |
updateSaveButtonState() { |
| 141 |
const saveButtons = document.querySelectorAll( '.cb-btn-update, .lp-cb-save-btn, .cb-btn-main-action' ); |
| 142 |
|
| 143 |
saveButtons.forEach( btn => { |
| 144 |
if ( this.hasUnsavedChanges ) { |
| 145 |
btn.classList.add( 'has-changes' ); |
| 146 |
} else { |
| 147 |
btn.classList.remove( 'has-changes' ); |
| 148 |
} |
| 149 |
}); |
| 150 |
} |
| 151 |
|
| 152 |
/** |
| 153 |
* Handle tab navigation click |
| 154 |
*/ |
| 155 |
handleTabClick( e ) { |
| 156 |
const tabLink = e.target.closest( '.lp-cb-tabs__item, .lp-cb-sidebar__item a' ); |
| 157 |
|
| 158 |
if ( ! tabLink ) { |
| 159 |
return; |
| 160 |
} |
| 161 |
|
| 162 |
if ( tabLink.hasAttribute( 'data-tab-section' ) || |
| 163 |
tabLink.getAttribute( 'href' ) === '#' ) { |
| 164 |
return; |
| 165 |
} |
| 166 |
|
| 167 |
if ( tabLink.classList.contains( 'is-active' ) || |
| 168 |
tabLink.closest( '.is-active' ) ) { |
| 169 |
return; |
| 170 |
} |
| 171 |
|
| 172 |
if ( this.hasUnsavedChanges ) { |
| 173 |
const confirmLeave = confirm( |
| 174 |
wp?.i18n?.__( 'You have unsaved changes. Are you sure you want to leave this page?', 'learnpress' ) || |
| 175 |
'You have unsaved changes. Are you sure you want to leave this page?' |
| 176 |
); |
| 177 |
|
| 178 |
if ( ! confirmLeave ) { |
| 179 |
e.preventDefault(); |
| 180 |
e.stopPropagation(); |
| 181 |
return false; |
| 182 |
} |
| 183 |
} |
| 184 |
} |
| 185 |
|
| 186 |
/** |
| 187 |
* Handle browser back/forward/close |
| 188 |
*/ |
| 189 |
handleBeforeUnload( e ) { |
| 190 |
if ( this.hasUnsavedChanges ) { |
| 191 |
e.preventDefault(); |
| 192 |
e.returnValue = ''; |
| 193 |
return ''; |
| 194 |
} |
| 195 |
} |
| 196 |
|
| 197 |
/** |
| 198 |
* Reset state after successful save |
| 199 |
*/ |
| 200 |
resetState() { |
| 201 |
this.hasUnsavedChanges = false; |
| 202 |
this.updateSaveButtonState(); |
| 203 |
this.captureOriginalValues(); |
| 204 |
} |
| 205 |
|
| 206 |
/** |
| 207 |
* Check if form has unsaved changes |
| 208 |
*/ |
| 209 |
hasChanges() { |
| 210 |
return this.hasUnsavedChanges; |
| 211 |
} |
| 212 |
|
| 213 |
/** |
| 214 |
* Manually set changed state |
| 215 |
*/ |
| 216 |
setChanged( changed = true ) { |
| 217 |
this.hasUnsavedChanges = changed; |
| 218 |
this.updateSaveButtonState(); |
| 219 |
} |
| 220 |
} |
| 221 |
|
| 222 |
// Export singleton instance |
| 223 |
let formStateInstance = null; |
| 224 |
|
| 225 |
export const getFormState = () => { |
| 226 |
if ( ! formStateInstance ) { |
| 227 |
formStateInstance = new BuilderFormState(); |
| 228 |
} |
| 229 |
return formStateInstance; |
| 230 |
}; |
| 231 |
|
| 232 |
export default BuilderFormState; |
| 233 |
|