MDL-31655 Warn users of unblurred but modified fields
[moodle.git] / lib / yui / formchangechecker / formchangechecker.js
1 YUI.add('moodle-core-formchangechecker',
2     function(Y) {
3         // The CSS selectors we use
4         var CSS = {
5         };
7         var FORMCHANGECHECKERNAME = 'core-formchangechecker';
9         var FORMCHANGECHECKER = function() {
10             FORMCHANGECHECKER.superclass.constructor.apply(this, arguments);
11         }
13         Y.extend(FORMCHANGECHECKER, Y.Base, {
14                 /**
15                  * Initialize the module
16                  */
17                 initializer : function(config) {
18                     var formid = 'form#' + this.get('formid');
20                     // Add change events to the form elements
21                     Y.all(formid + ' input').once('change', M.core_formchangechecker.set_form_changed, this);
22                     Y.all(formid + ' textarea').once('change', M.core_formchangechecker.set_form_changed, this);
23                     Y.all(formid + ' select').once('change', M.core_formchangechecker.set_form_changed, this);
25                     // Add a focus event to check for changes which are made without triggering a change event
26                     Y.all(formid + ' input').on('focus', this.store_initial_value, this);
27                     Y.all(formid + ' textarea').on('focus', this.store_initial_value, this);
28                     Y.all(formid + ' select').on('focus', this.store_initial_value, this);
30                     // We need any submit buttons on the form to set the submitted flag
31                     Y.one(formid).on('submit', M.core_formchangechecker.set_form_submitted, this);
33                     // YUI doesn't support onbeforeunload properly so we must use the DOM to set the onbeforeunload. As
34                     // a result, the has_changed must stay in the DOM too
35                     window.onbeforeunload = M.core_formchangechecker.report_form_dirty_state;
36                 },
38                 /**
39                  * Store the initial value of the currently focussed element
40                  *
41                  * If an element has been focussed and changed but not yet blurred, the on change
42                  * event won't be fired. We need to store it's initial value to compare it in the
43                  * get_form_dirty_state function later.
44                  */
45                 store_initial_value : function(e) {
46                     if (M.core_formchangechecker.get_form_dirty_state()) {
47                         // Clear the store_initial_value listeners as the form is already dirty so
48                         // we no longer need to call this function
49                         var formid = 'form#' + this.get('formid');
51                         Y.all(formid + ' input').detach('focus', this.store_initial_value, this);
52                         Y.all(formid + ' textarea').detach('focus', this.store_initial_value, this);
53                         Y.all(formid + ' select').detach('focus', this.store_initial_value, this);
54                         return;
55                     }
57                     // Make a note of the current element so that it can be interrogated and
58                     // compared in the get_form_dirty_state function
59                     M.core_formchangechecker.stateinformation.focused_element = {
60                         element : e.target,
61                         initial_value : e.target.get('value')
62                     }
63                 }
64             },
65             {
66                 NAME : FORMCHANGECHECKERNAME,
67                 ATTRS : {
68                     formid : {
69                         'value' : ''
70                     }
71                 }
72             }
73         );
75         M.core_formchangechecker = M.core_formchangechecker || {};
77         // We might have multiple instances of the form change protector
78         M.core_formchangechecker.instances = M.core_formchangechecker.instances || [];
79         M.core_formchangechecker.init = function(config) {
80             var formchangechecker = new FORMCHANGECHECKER(config);
81             M.core_formchangechecker.instances.push(formchangechecker);
82             return formchangechecker;
83         }
85         // Store state information
86         M.core_formchangechecker.stateinformation = [];
88         /**
89          * Set the form changed state to true
90          */
91         M.core_formchangechecker.set_form_changed = function() {
92             M.core_formchangechecker.stateinformation.formchanged = 1;
94             // Once the form has been marked as dirty, we no longer need to keep track of form elements
95             // which haven't yet blurred
96             delete M.core_formchangechecker.stateinformation.focused_element;
97         }
99         /**
100          * Set the form submitted state to true
101          */
102         M.core_formchangechecker.set_form_submitted = function() {
103             M.core_formchangechecker.stateinformation.formsubmitted = 1;
104         }
106         /**
107          * Attempt to determine whether the form has been modified in any way and
108          * is thus 'dirty'
109          *
110          * @return Integer 1 is the form is dirty; 0 if not
111          */
112         M.core_formchangechecker.get_form_dirty_state = function() {
113             var state = M.core_formchangechecker.stateinformation;
115             // If the form was submitted, then return a non-dirty state
116             if (state.formsubmitted) {
117                 return 0;
118             }
120             // If any fields have been marked dirty, return a dirty state
121             if (state.formchanged) {
122                 return 1;
123             }
125             // If a field has been focused and changed, but still has focus then the browser won't fire the
126             // onChange event. We check for this eventuality here
127             if (state.focused_element) {
128                 if (state.focused_element.element.get('value') != state.focused_element.initial_value) {
129                     return 1;
130                 }
131             }
133             // Handle TinyMCE editor instances
134             // We can't add a listener in the initializer as the editors may not have been created by that point
135             // so we do so here instead
136             if (typeof tinyMCE != 'undefined') {
137                 for (var editor in tinyMCE.editors) {
138                     if (tinyMCE.editors[editor].isDirty()) {
139                         return 1;
140                     }
141                 }
142             }
144             // If we reached here, then the form hasn't met any of the dirty conditions
145             return 0;
146         };
148         /**
149          * Return a suitable message if changes have been made to a form
150          */
151         M.core_formchangechecker.report_form_dirty_state = function(e) {
152             if (!M.core_formchangechecker.get_form_dirty_state()) {
153                 // the form is not dirty, so don't display any message
154                 return;
155             }
157             // This is the error message that we'll show to browsers which support it
158             var warningmessage = M.util.get_string('changesmadereallygoaway', 'moodle');
160             // Most browsers are happy with the returnValue being set on the event
161             // But some browsers do not consistently pass the event
162             if (e) {
163                 e.returnValue = warningmessage;
164             }
166             // But some require it to be returned instead
167             return warningmessage;
168         };
169     },
170     '@VERSION@', {
171         requires : ['base']
172     }
173 );