jquery-dirtyforms-rails 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,159 @@
1
+ /*!
2
+ PNotify dialog module (for jQuery Dirty Forms) | v | github.com/snikch/jquery.dirtyforms
3
+ (c) 2015-2016 Shad Storhaug
4
+ License MIT
5
+ */
6
+
7
+ (function($, window, document, undefined) {
8
+ // Can't use ECMAScript 5's strict mode because several apps
9
+ // including ASP.NET trace the stack via arguments.caller.callee
10
+ // and Firefox dies if you try to trace through "use strict" call chains.
11
+ // See jQuery issue (#13335)
12
+ // Support: Firefox 18+
13
+ //"use strict";
14
+
15
+ var modal_overlay,
16
+ notice,
17
+ isPN1 = typeof PNotify !== 'function';
18
+
19
+ $.DirtyForms.dialog = {
20
+
21
+ // Custom properties and methods to allow overriding (may differ per dialog)
22
+ title: 'Are you sure you want to do that?',
23
+ class: 'dirty-dialog',
24
+ proceedButtonText: 'Leave This Page',
25
+ stayButtonText: 'Stay Here',
26
+ styling: 'bootstrap3',
27
+ width: '330',
28
+
29
+ // Typical Dirty Forms Properties and Methods
30
+ open: function (choice, message, ignoreClass) {
31
+ var content = $.extend(true, {}, {
32
+ title: this.title,
33
+ hide: false,
34
+ styling: this.styling,
35
+ width: this.width,
36
+
37
+ // 3.x and 2.x confirm buttons
38
+ confirm: {
39
+ confirm: true,
40
+ align: 'center',
41
+ buttons: [
42
+ {
43
+ text: this.proceedButtonText,
44
+ addClass: 'dirty-proceed ' + ignoreClass
45
+ },
46
+ {
47
+ text: this.stayButtonText,
48
+ addClass: 'dirty-stay ' + ignoreClass
49
+ }
50
+ ]
51
+ },
52
+ // 3.x don't use history
53
+ history: {
54
+ history: false
55
+ },
56
+ // 3.x modal dialog
57
+ addclass: 'stack-modal ' + this.class,
58
+ stack: { 'dir1': 'down', 'dir2': 'right', 'modal': true },
59
+
60
+ // 3.x and 2.x hide closer and sticker
61
+ buttons: {
62
+ closer: false,
63
+ sticker: false
64
+ },
65
+ // 1.x hide closer and sticker
66
+ closer: false,
67
+ sticker: false,
68
+
69
+ // NOTE: Animate does not seem to work in 3.x in conjunction with confirm,
70
+ // but this is being added so the settings can be supplied externally should
71
+ // this issue be fixed. https://github.com/sciactive/pnotify/issues/224
72
+ animate: this.animate === undefined ? undefined : this.animate,
73
+
74
+ text: !isPN1 ? message :
75
+ '<span class="' + this.class + '">' +
76
+ '<p>' + message + '</p>' +
77
+ '<span style="display:block;text-align:center;">' +
78
+ '<button type="button" class="btn btn-default dirty-proceed ' + ignoreClass + '">' + this.proceedButtonText + '</button> ' +
79
+ '<button type="button" class="btn btn-default dirty-stay ' + ignoreClass + '">' + this.stayButtonText + '</button>' +
80
+ '</span>' +
81
+ '</span>',
82
+
83
+ // Not supported in 3.x
84
+ before_open: function (PNotify) {
85
+ if (!isPN1) {
86
+ // Position this notice in the center of the screen.
87
+ PNotify.get().css({
88
+ "top": ($(window).height() / 2) - (PNotify.get().height() / 2),
89
+ "left": ($(window).width() / 2) - (PNotify.get().width() / 2)
90
+ });
91
+ }
92
+
93
+ // Make a modal screen overlay.
94
+ if (modal_overlay) modal_overlay.fadeIn("fast");
95
+ else modal_overlay = $("<div />", {
96
+ "class": "ui-widget-overlay",
97
+ "css": {
98
+ "display": "none",
99
+ "position": "fixed",
100
+ "top": "0",
101
+ "bottom": "0",
102
+ "right": "0",
103
+ "left": "0"
104
+ }
105
+ }).appendTo("body").fadeIn("fast");
106
+ }
107
+ });
108
+
109
+ // Patch for PNotify 1.x
110
+ notice = !isPN1 ? new PNotify(content) : $.pnotify(content);
111
+
112
+ // Bind Events
113
+ choice.bindEnterKey = true;
114
+ choice.proceedSelector = '.' + this.class + ' .dirty-proceed';
115
+ choice.staySelector = '.' + this.class + ' .dirty-stay,.ui-widget-overlay';
116
+
117
+ // Support for Dirty Forms < 2.0
118
+ if (choice.isDF1) {
119
+ var close = function (decision) {
120
+ return function (e) {
121
+ if (e.type !== 'keydown' || (e.type === 'keydown' && (e.which == 27 || e.which == 13))) {
122
+ notice.remove();
123
+ if (modal_overlay) modal_overlay.fadeOut("fast");
124
+ decision(e);
125
+ return false;
126
+ }
127
+ };
128
+ };
129
+ // Trap the escape key and force a close. Cancel it so PNotify doesn't intercept it.
130
+ var decidingCancel = $.DirtyForms.decidingCancel;
131
+ $(document).keydown(close(decidingCancel));
132
+ $(choice.staySelector).click(close(decidingCancel));
133
+ $(choice.proceedSelector).click(close($.DirtyForms.decidingContinue));
134
+ }
135
+ },
136
+ close: function () {
137
+ notice.remove();
138
+ if (modal_overlay) modal_overlay.fadeOut("fast");
139
+ },
140
+
141
+ // Support for Dirty Forms < 2.0
142
+ fire: function (message, title) {
143
+ this.title = title;
144
+ this.open({ isDF1: true }, message, $.DirtyForms.ignoreClass);
145
+ },
146
+
147
+ // Support for Dirty Forms < 1.2
148
+ bind: function () {
149
+ },
150
+ stash: function () {
151
+ return false;
152
+ },
153
+ refire: function () {
154
+ return false;
155
+ },
156
+ selector: 'no-op'
157
+ };
158
+
159
+ })(jQuery, window, document);
@@ -0,0 +1,27 @@
1
+ /*!
2
+ Always dirty helper module (for jQuery Dirty Forms) | v | github.com/snikch/jquery.dirtyforms
3
+ (c) 2012-2016 Mal Curtis
4
+ License MIT
5
+ */
6
+
7
+ // Example helper, the form is always considered dirty
8
+
9
+ (function($, window, document, undefined) {
10
+ // Can't use ECMAScript 5's strict mode because several apps
11
+ // including ASP.NET trace the stack via arguments.caller.callee
12
+ // and Firefox dies if you try to trace through "use strict" call chains.
13
+ // See jQuery issue (#13335)
14
+ // Support: Firefox 18+
15
+ //"use strict";
16
+
17
+ // Create a new object, with an isDirty method
18
+ var alwaysDirty = {
19
+ isDirty: function (node) {
20
+ // Perform dirty check on a given node (usually a form element)
21
+ return true;
22
+ }
23
+ };
24
+ // Push the new object onto the helpers array
25
+ $.DirtyForms.helpers.push(alwaysDirty);
26
+
27
+ })(jQuery, window, document);
@@ -0,0 +1,67 @@
1
+ /*!
2
+ CkEditor helper module (for jQuery Dirty Forms) | v | github.com/snikch/jquery.dirtyforms
3
+ (c) 2012-2016 Mal Curtis
4
+ License MIT
5
+ */
6
+
7
+ (function($, window, document, undefined) {
8
+ // Can't use ECMAScript 5's strict mode because several apps
9
+ // including ASP.NET trace the stack via arguments.caller.callee
10
+ // and Firefox dies if you try to trace through "use strict" call chains.
11
+ // See jQuery issue (#13335)
12
+ // Support: Firefox 18+
13
+ //"use strict";
14
+
15
+ var ignoreSelector = '.cke_dialog_ui_button,.cke_tpl_list a';
16
+
17
+ var ckeditor = {
18
+ ignoreSelector: ignoreSelector,
19
+ isDirty: function ($form) {
20
+ var $editors = ckeditors($form),
21
+ isDirty = false;
22
+ if ($editors.length > 0) {
23
+ $.DirtyForms.dirtylog('Checking ' + $editors.length + ' ckeditors for dirtyness.');
24
+ $editors.each(function (editorIndex) {
25
+ if (this.checkDirty()) {
26
+ isDirty = true;
27
+
28
+ $.DirtyForms.dirtylog('CKEditor with index ' + editorIndex + ' was dirty, exiting...');
29
+ // Return false to break out of the .each() function
30
+ return false;
31
+ }
32
+ });
33
+ }
34
+ return isDirty;
35
+ },
36
+ setClean: function ($form) {
37
+ ckeditors($form).each(function () { this.resetDirty(); });
38
+ },
39
+
40
+ // Support for Dirty Forms < 2.0
41
+ ignoreAnchorSelector: ignoreSelector
42
+ };
43
+ var ckeditors = function (form) {
44
+ var $form = form.jquery ? form : $(form);
45
+ var editors = [];
46
+ if (!window.CKEDITOR || !window.CKEDITOR.instances) {
47
+ return $(editors);
48
+ }
49
+ try {
50
+ for (var key in window.CKEDITOR.instances) {
51
+ if (window.CKEDITOR.instances.hasOwnProperty(key)) {
52
+ var editor = window.CKEDITOR.instances[key];
53
+ if ($(editor.element.$).parents().index($form) != -1) {
54
+ $.DirtyForms.dirtylog('Adding CKEditor with key ' + key);
55
+ editors.push(editor);
56
+ }
57
+ }
58
+ }
59
+ }
60
+ catch (e) {
61
+ // Ignore, means there was no CKEDITOR variable
62
+ }
63
+ return $(editors);
64
+ };
65
+ $.DirtyForms.helpers.push(ckeditor);
66
+
67
+ })(jQuery, window, document);
@@ -0,0 +1,158 @@
1
+ /*!
2
+ TinyMCE helper module (for jQuery Dirty Forms) | v | github.com/snikch/jquery.dirtyforms
3
+ (c) 2013-2016 Mal Curtis
4
+ License MIT
5
+ */
6
+
7
+ (function($, window, document, undefined) {
8
+ // Can't use ECMAScript 5's strict mode because several apps
9
+ // including ASP.NET trace the stack via arguments.caller.callee
10
+ // and Firefox dies if you try to trace through "use strict" call chains.
11
+ // See jQuery issue (#13335)
12
+ // Support: Firefox 18+
13
+ //"use strict";
14
+
15
+ var tinymceSelector = ':tinymce:not(.dirty-forms-temp)',
16
+ ignoreSelector = '.mceEditor a,.mceMenu a,[name^="mce_"]';
17
+
18
+ // Create a new object, with an isDirty method
19
+ var tinymce = {
20
+ // Dirty Forms properties and methods
21
+ ignoreSelector: ignoreSelector,
22
+ isDirty: function ($node) {
23
+ var isDirty = false;
24
+ if (hasTinyMCE($node)) {
25
+ // Search the current node and all descendant nodes that match the selector
26
+ $node.filter(tinymceSelector).add($node.find(tinymceSelector)).each(function () {
27
+ var $field = $(this);
28
+
29
+ $.DirtyForms.dirtylog('Checking node ' + $field.attr('id'));
30
+ if (typeof $field.data('df-tinymce-orig') === 'undefined') {
31
+ // For Dirty Forms < 2.0 and TinyMCE elements that were added via AJAX,
32
+ // we default to using TinyMCE's isDirty behavior (which is stateless).
33
+ if ($field.tinymce().isDirty()) {
34
+ isDirty = true;
35
+ $.DirtyForms.dirtylog('Node was totally dirty.');
36
+ // Return false to stop iterating.
37
+ return false;
38
+ }
39
+ } else {
40
+ // For Dirty Forms >= 2.0, we compare hash codes with the original content
41
+ var content = getTinyMceContent($field);
42
+ $.DirtyForms.dirtylog('TinyMCE content: ' + content);
43
+ var hash = getHashCode(content);
44
+ $.DirtyForms.dirtylog('TinyMCE hash: ' + hash);
45
+ var originalHash = $field.data('df-tinymce-orig');
46
+ $.DirtyForms.dirtylog('Original TinyMCE hash: ' + originalHash);
47
+
48
+ if (hash !== originalHash) {
49
+ isDirty = true;
50
+ $.DirtyForms.dirtylog('Node was totally dirty.');
51
+ // Return false to stop iterating.
52
+ return false;
53
+ }
54
+ }
55
+ });
56
+ }
57
+ return isDirty;
58
+ },
59
+ setClean: function ($node) {
60
+ if (hasTinyMCE($node)) {
61
+
62
+ // Search the current node and all descendant nodes that match the selector
63
+ $node.filter(tinymceSelector).add($node.find(tinymceSelector)).each(function () {
64
+ var $field = $(this);
65
+
66
+ // Set TinyMCE clean
67
+ if ($field.tinymce().isDirty()) {
68
+ $.DirtyForms.dirtylog('Resetting isDirty on node ' + $field.attr('id'));
69
+ $field.tinymce().isNotDirty = 1; //Force not dirty state
70
+ }
71
+
72
+ // Forget the original value and reset to the current state
73
+ storeOriginalValue($field);
74
+ });
75
+ }
76
+ },
77
+ rescan: function ($node) {
78
+ if (hasTinyMCE($node)) {
79
+ $node.filter(tinymceSelector).add($node.find(tinymceSelector)).each(function () {
80
+ var $field = $(this);
81
+
82
+ if (typeof $field.data('df-tinymce-orig') !== 'undefined') {
83
+ storeOriginalValue($field);
84
+ }
85
+ });
86
+ }
87
+ },
88
+
89
+ // Patch for Dirty Forms < 2.0
90
+ ignoreAnchorSelector: ignoreSelector
91
+ };
92
+
93
+ // Push the new object onto the helpers array
94
+ $.DirtyForms.helpers.push(tinymce);
95
+
96
+ // Fix: tinymce throws an error if the selector doesn't match anything
97
+ // (such as when there are no textareas on the current page)
98
+ var hasTinyMCE = function ($node) {
99
+ try {
100
+ return $node.filter(tinymceSelector).length > 0 || $node.find(tinymceSelector).length > 0;
101
+ }
102
+ catch (e) {
103
+ return false;
104
+ }
105
+ };
106
+
107
+ var getTinyMceContent = function ($field) {
108
+ // Hack: TinyMCE puts an extra <br> tag at the end of a paragraph when it is edited, so ignore that case.
109
+ return $field.tinymce().getContent({ format: 'raw' }).replace(/<br><\/p>/mg, '</p>');
110
+ };
111
+
112
+ var storeOriginalValue = function ($field) {
113
+ var content = getTinyMceContent($field);
114
+ $.DirtyForms.dirtylog('Original TinyMCE content: ' + content);
115
+
116
+ var hash = getHashCode(content);
117
+ $.DirtyForms.dirtylog('Original TinyMCE hash: ' + hash);
118
+
119
+ $field.data('df-tinymce-orig', hash);
120
+ };
121
+
122
+ // When TinyMCE is found, store the original value as a hash so we can see if there are changes later.
123
+ var init = function ($node) {
124
+ if (hasTinyMCE($node)) {
125
+ $node.filter(tinymceSelector).add($node.find(tinymceSelector)).each(function () {
126
+ storeOriginalValue($(this));
127
+ });
128
+ }
129
+ };
130
+
131
+ $(document).bind('scan.dirtyforms', function (ev) {
132
+ // Hack: TinyMCE doesn't have a global init event. So, we create a new
133
+ // TinyMCE editor within an invisible div and respond to its init event.
134
+ // There doesn't seem to be a reasonable way
135
+ // to remove the control again, so we simply ignore it.
136
+ var $form = $(ev.target);
137
+ var $editor = $('<div style="display:none;" class="dirty-forms-temp"></div>');
138
+ $form.append($editor);
139
+ $editor.tinymce({
140
+ oninit: function () {
141
+ init($form);
142
+ }
143
+ });
144
+ });
145
+
146
+ // Simple way to hash a string: http://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript-jquery
147
+ var getHashCode = function (str) {
148
+ var hash = 0, i, chr, len;
149
+ if (str.length === 0) return hash;
150
+ for (i = 0, len = str.length; i < len; i++) {
151
+ chr = str.charCodeAt(i);
152
+ hash = ((hash << 5) - hash) + chr;
153
+ hash |= 0; // Convert to 32bit integer
154
+ }
155
+ return hash;
156
+ };
157
+
158
+ })(jQuery, window, document);
@@ -0,0 +1,596 @@
1
+ /*!
2
+ Dirty Forms jQuery Plugin | v | github.com/snikch/jquery.dirtyforms
3
+ (c) 2010-2016 Mal Curtis
4
+ License MIT
5
+ */
6
+
7
+ (function($, window, document, undefined) {
8
+ // Can't use ECMAScript 5's strict mode because several apps
9
+ // including ASP.NET trace the stack via arguments.caller.callee
10
+ // and Firefox dies if you try to trace through "use strict" call chains.
11
+ // See jQuery issue (#13335)
12
+ // Support: Firefox 18+
13
+ //"use strict";
14
+
15
+ if (!$.fn.on) {
16
+ // Patch jQuery 1.4.2 - 1.7 with an on function (that uses delegate).
17
+ $.fn.on = function (events, selector, data, handler) {
18
+ return this.delegate(selector, events, data, handler);
19
+ };
20
+ }
21
+
22
+ $.fn.dirtyForms = function (method) {
23
+ // Method calling logic
24
+ if (methods[method]) {
25
+ return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
26
+ } else if (typeof method === 'object' || !method) {
27
+ return methods.init.apply(this, arguments);
28
+ } else {
29
+ $.error('Method ' + method + ' does not exist on jQuery.dirtyForms');
30
+ }
31
+ };
32
+
33
+ // Public Element methods ( $('form').dirtyForms('methodName', args) )
34
+ var methods = {
35
+ init: function (options) {
36
+ var data = {};
37
+
38
+ if (!state.initialized) {
39
+ // Override any default options
40
+ $.extend(true, $.DirtyForms, options);
41
+
42
+ $(document).trigger('bind.dirtyforms', [events]);
43
+ events.bind(window, document, data);
44
+
45
+ state.initialized = true;
46
+ }
47
+
48
+ this.filter('form').not(':dirtylistening').each(function () {
49
+ var $form = $(this);
50
+ dirtylog('Adding form ' + $form.attr('id') + ' to forms to watch');
51
+
52
+ // Store original values of the fields
53
+ $form.find($.DirtyForms.fieldSelector).each(function () {
54
+ storeOriginalValue($(this));
55
+ });
56
+
57
+ $form.trigger('scan.dirtyforms');
58
+ events.bindForm($form, data);
59
+ });
60
+ return this;
61
+ },
62
+ // Returns true if any of the selected elements or their children are dirty
63
+ isDirty: function (excludeHelpers) {
64
+ var ignoreSelector = getIgnoreSelector(),
65
+ dirtyClass = $.DirtyForms.dirtyClass,
66
+ isDirty = false;
67
+
68
+ this.each(function (index) {
69
+ var $node = $(this),
70
+ ignored = isFieldIgnored($node, ignoreSelector);
71
+
72
+ if ($node.hasClass(dirtyClass) && !ignored) {
73
+ isDirty = true;
74
+ // Exit out of the .each() function
75
+ return false;
76
+ }
77
+
78
+ // Check any descendant nodes (if this is a container element)
79
+ $node.find('.' + dirtyClass).each(function () {
80
+ if (!isFieldIgnored($(this), ignoreSelector)) {
81
+ isDirty = true;
82
+ // Exit out of the .each() function
83
+ return false;
84
+ }
85
+ });
86
+ // Exit out of the .each() function
87
+ if (isDirty) return false;
88
+
89
+ if (!ignored && !excludeHelpers) {
90
+ // Test helpers for this node.
91
+ $.each($.DirtyForms.helpers, function (i, helper) {
92
+ if (helper.isDirty && helper.isDirty($node, index)) {
93
+ isDirty = true;
94
+ // Exit out of the .each() function
95
+ return false;
96
+ }
97
+ });
98
+
99
+ // Exit out of the .each() function
100
+ if (isDirty) return false;
101
+ }
102
+ });
103
+
104
+ return isDirty;
105
+ },
106
+ // Marks the element(s) and any helpers within the element not dirty.
107
+ // If all of the fields in a form are marked not dirty, the form itself will be marked not dirty even
108
+ // if it is not included in the selector. Also resets original values to the current state -
109
+ // essentially "forgetting" the node or its descendants are dirty.
110
+ setClean: function (excludeIgnored, excludeHelpers) {
111
+ dirtylog('setClean called');
112
+
113
+ var doSetClean = function () {
114
+ var $field = $(this);
115
+
116
+ // Reset by storing the original value again
117
+ storeOriginalValue($field);
118
+
119
+ // Remove the dirty class
120
+ setDirtyStatus($field, false);
121
+ };
122
+
123
+ elementsInRange(this, $.DirtyForms.fieldSelector, excludeIgnored)
124
+ .each(doSetClean)
125
+ .parents('form').trigger('setclean.dirtyforms', [excludeIgnored]);
126
+
127
+ if (excludeHelpers) return this;
128
+ return fireHelperMethod(this, 'setClean', excludeIgnored, getIgnoreSelector());
129
+ },
130
+ // Scans the selected elements and descendants for any new fields and stores their original values.
131
+ // Ignores any original values that had been set previously. Also resets the dirty status of all fields
132
+ // whose ignore status has changed since the last scan.
133
+ rescan: function (excludeIgnored, excludeHelpers) {
134
+ dirtylog('rescan called');
135
+
136
+ var doRescan = function () {
137
+ var $field = $(this);
138
+
139
+ // Skip previously added fields
140
+ if (!hasOriginalValue($field)) {
141
+ // Store the original value
142
+ storeOriginalValue($field);
143
+ }
144
+
145
+ // Set the dirty status
146
+ setDirtyStatus($field, isFieldDirty($field));
147
+ };
148
+
149
+ elementsInRange(this, $.DirtyForms.fieldSelector, excludeIgnored)
150
+ .each(doRescan)
151
+ .parents('form').trigger('rescan.dirtyforms', [excludeIgnored]);
152
+
153
+ if (excludeHelpers) return this;
154
+ return fireHelperMethod(this, 'rescan', excludeIgnored, getIgnoreSelector());
155
+ }
156
+ };
157
+
158
+ // Custom selectors $('form:dirty')
159
+ $.extend($.expr[":"], {
160
+ dirty: function (element) {
161
+ var $element = $(element);
162
+ return $element.hasClass($.DirtyForms.dirtyClass) && !$element.is(':dirtyignored');
163
+ },
164
+ dirtylistening: function (element) {
165
+ return $(element).hasClass($.DirtyForms.listeningClass);
166
+ },
167
+ dirtyignored: function (element) {
168
+ return isFieldIgnored($(element), false);
169
+ }
170
+ });
171
+
172
+ // Public General Plugin properties and methods $.DirtyForms
173
+ $.DirtyForms = {
174
+ message: "You've made changes on this page which aren't saved. If you leave you will lose these changes.",
175
+ dirtyClass: 'dirty',
176
+ listeningClass: 'dirtylisten',
177
+ ignoreClass: 'dirtyignore',
178
+ ignoreSelector: '',
179
+ // exclude all HTML 4 except checkbox, option, text and password, but include HTML 5 except search
180
+ fieldSelector: "input:not([type='button'],[type='image'],[type='submit']," +
181
+ "[type='reset'],[type='file'],[type='search']),select,textarea",
182
+ /*<log>*/
183
+ debug: false,
184
+ dirtylog: function (msg) {
185
+ dirtylog(msg);
186
+ },
187
+ /*</log>*/
188
+ helpers: [],
189
+ dialog: false
190
+ };
191
+
192
+ // Private State Management
193
+ var state = {
194
+ initialized: false,
195
+ formStash: false,
196
+ dialogStash: false,
197
+ deciding: false,
198
+ decidingEvent: false
199
+ };
200
+
201
+ // Dialog Decision Management
202
+ var choice;
203
+
204
+ var bindKeys = function (ev) {
205
+ if (ev.data.bindEscKey && ev.which == 27 || ev.data.bindEnterKey && ev.which == 13) {
206
+ return doCommit(ev, false);
207
+ }
208
+ };
209
+
210
+ var bindDialog = function (choice) {
211
+ var staySelector = choice.staySelector,
212
+ proceedSelector = choice.proceedSelector;
213
+
214
+ if (staySelector !== '') {
215
+ $(staySelector).unbind('click', doCommit)
216
+ .click(doCommit);
217
+ }
218
+ if (proceedSelector !== '') {
219
+ $(proceedSelector).unbind('click', doProceed)
220
+ .click(doProceed);
221
+ }
222
+ if (choice.bindEscKey || choice.bindEnterKey) {
223
+ $(document).unbind('keydown', bindKeys)
224
+ .keydown(choice, bindKeys);
225
+ }
226
+ };
227
+
228
+ var callDialogClose = function (proceeding, unstashing) {
229
+ if ($.isFunction($.DirtyForms.dialog.close)) {
230
+ dirtylog('Calling dialog close');
231
+ $.DirtyForms.dialog.close(proceeding, unstashing);
232
+ }
233
+ };
234
+
235
+ var doProceed = function (ev) {
236
+ return doCommit(ev, true);
237
+ };
238
+
239
+ var doCommit = function (ev, proceeding) {
240
+ if (!state.deciding) return;
241
+ ev.preventDefault();
242
+
243
+ if (proceeding === true) {
244
+ var refireEvent = state.decidingEvent;
245
+ $(document).trigger('proceed.dirtyforms', [refireEvent]);
246
+ events.clearUnload(); // fix for chrome/safari
247
+ callDialogClose(proceeding, false);
248
+ refire(refireEvent);
249
+ } else {
250
+ $(document).trigger('stay.dirtyforms');
251
+ var isUnstashing = $.DirtyForms.dialog !== false && state.dialogStash !== false && $.isFunction($.DirtyForms.dialog.unstash);
252
+ callDialogClose(proceeding, isUnstashing);
253
+ if (isUnstashing) {
254
+ dirtylog('Refiring the dialog with stashed content');
255
+ $.DirtyForms.dialog.unstash(state.dialogStash, ev);
256
+ }
257
+ $(document).trigger('afterstay.dirtyforms');
258
+ }
259
+
260
+ state.deciding = state.decidingEvent = state.dialogStash = state.formStash = false;
261
+ return false;
262
+ };
263
+
264
+ // Event management
265
+ var events = {
266
+ bind: function (window, document, data) {
267
+ $(window).bind('beforeunload', data, events.onBeforeUnload);
268
+ $(document).on('click', 'a:not([target="_blank"])', data, events.onAnchorClick)
269
+ .on('submit', 'form', data, events.onSubmit);
270
+ },
271
+ bindForm: function ($form, data) {
272
+ var dirtyForms = $.DirtyForms;
273
+
274
+ // Test whether we are dealing with IE < 10
275
+ var isIE8_9 = ('onpropertychange' in document.createElement('input'));
276
+ var inputEvents = 'change input' + (isIE8_9 ? ' keyup selectionchange cut paste' : '');
277
+ $form.addClass(dirtyForms.listeningClass)
278
+ .on('focus keydown', dirtyForms.fieldSelector, data, events.onFocus)
279
+ .on(inputEvents, dirtyForms.fieldSelector, data, events.onFieldChange)
280
+ .bind('reset', data, events.onReset);
281
+ },
282
+ // For any fields added after the form was initialized, store the value when focused.
283
+ onFocus: function (ev) {
284
+ var $field = $(ev.target);
285
+ if (!hasOriginalValue($field)) {
286
+ storeOriginalValue($field);
287
+ }
288
+ },
289
+ onFieldChange: function (ev) {
290
+ var $field = $(ev.target);
291
+ if (ev.type !== 'change') {
292
+ delay(function () { setFieldStatus($field); }, 100);
293
+ } else {
294
+ setFieldStatus($field);
295
+ }
296
+ },
297
+ onReset: function (ev) {
298
+ var $form = $(ev.target).closest('form');
299
+ // Need a delay here because reset is called before the state of the form is reset.
300
+ setTimeout(function () { $form.dirtyForms('setClean'); }, 100);
301
+ },
302
+ onAnchorClick: function (ev) {
303
+ bindFn(ev);
304
+ },
305
+ onSubmit: function (ev) {
306
+ bindFn(ev);
307
+ },
308
+ onBeforeUnload: function (ev) {
309
+ var result = bindFn(ev);
310
+
311
+ if (result && state.doubleunloadfix !== true) {
312
+ dirtylog('Before unload will be called, resetting');
313
+ state.deciding = false;
314
+ }
315
+
316
+ state.doubleunloadfix = true;
317
+ setTimeout(function () { state.doubleunloadfix = false; }, 200);
318
+
319
+ // Only return the result if it is a string, otherwise don't return anything.
320
+ if (typeof result === 'string') {
321
+ // For IE and Firefox prior to version 4, set the returnValue.
322
+ ev.returnValue = result;
323
+ return result;
324
+ }
325
+ },
326
+ onRefireClick: function (ev) {
327
+ var event = new $.Event('click');
328
+ $(ev.target).trigger(event);
329
+ if (!event.isDefaultPrevented()) {
330
+ events.onRefireAnchorClick(ev);
331
+ }
332
+ },
333
+ onRefireAnchorClick: function (ev) {
334
+ var href = $(ev.target).closest('a[href]').attr('href');
335
+ if (href !== undefined) {
336
+ dirtylog('Sending location to ' + href);
337
+ window.location.href = href;
338
+ }
339
+ },
340
+ clearUnload: function () {
341
+ // I'd like to just be able to unbind this but there seems
342
+ // to be a bug in jQuery which doesn't unbind onbeforeunload
343
+ dirtylog('Clearing the beforeunload event');
344
+ $(window).unbind('beforeunload', events.onBeforeUnload);
345
+ window.onbeforeunload = null;
346
+ $(document).trigger('beforeunload.dirtyforms');
347
+ }
348
+ };
349
+
350
+ var elementsInRange = function ($this, selector, excludeIgnored) {
351
+ var $elements = $this.filter(selector).add($this.find(selector));
352
+ if (excludeIgnored) {
353
+ $elements = $elements.not(':dirtyignored');
354
+ }
355
+ return $elements;
356
+ };
357
+
358
+ var fireHelperMethod = function ($this, method, excludeIgnored, ignoreSelector) {
359
+ return $this.each(function (index) {
360
+ var $node = $(this);
361
+
362
+ if (!excludeIgnored || !isFieldIgnored($node, ignoreSelector)) {
363
+ $.each($.DirtyForms.helpers, function (i, helper) {
364
+ if (helper[method]) { helper[method]($node, index, excludeIgnored); }
365
+ });
366
+ }
367
+ });
368
+ };
369
+
370
+ var getFieldValue = function ($field) {
371
+ var value;
372
+ if ($field.is('select')) {
373
+ value = '';
374
+ $field.find('option').each(function () {
375
+ var $option = $(this);
376
+ if ($option.is(':selected')) {
377
+ if (value.length > 0) { value += ','; }
378
+ value += $option.val();
379
+ }
380
+ });
381
+ } else if ($field.is(":checkbox,:radio")) {
382
+ value = $field.is(':checked');
383
+ } else {
384
+ value = $field.val();
385
+ }
386
+
387
+ return value;
388
+ };
389
+
390
+ var storeOriginalValue = function ($field) {
391
+ dirtylog('Storing original value for ' + $field.attr('name'));
392
+ $field.data('df-orig', getFieldValue($field));
393
+ var isEmpty = ($field.data('df-orig') === undefined);
394
+ $field.data('df-empty', isEmpty);
395
+ };
396
+
397
+ var hasOriginalValue = function ($field) {
398
+ return ($field.data('df-orig') !== undefined || $field.data('df-empty') === true);
399
+ };
400
+
401
+ var getIgnoreSelector = function () {
402
+ var dirtyForms = $.DirtyForms,
403
+ result = dirtyForms.ignoreSelector;
404
+ $.each(dirtyForms.helpers, function (key, obj) {
405
+ if ('ignoreSelector' in obj) {
406
+ if (result.length > 0) { result += ','; }
407
+ result += obj.ignoreSelector;
408
+ }
409
+ });
410
+ return result;
411
+ };
412
+
413
+ var isFieldIgnored = function ($field, ignoreSelector) {
414
+ if (!ignoreSelector) {
415
+ ignoreSelector = getIgnoreSelector();
416
+ }
417
+ return $field.is(ignoreSelector) || $field.closest('.' + $.DirtyForms.ignoreClass).length > 0;
418
+ };
419
+
420
+ var isFieldDirty = function ($field, ignoreSelector) {
421
+ if (!hasOriginalValue($field) || isFieldIgnored($field, ignoreSelector)) return false;
422
+ return (getFieldValue($field) != $field.data('df-orig'));
423
+ };
424
+
425
+ var setFieldStatus = function ($field, ignoreSelector) {
426
+ if (isFieldIgnored($field, ignoreSelector)) return;
427
+
428
+ // Option groups are a special case because they change more than the current element.
429
+ if ($field.is(':radio[name]')) {
430
+ var name = $field.attr('name'),
431
+ $form = $field.parents('form');
432
+
433
+ $form.find(":radio[name='" + name + "']").each(function () {
434
+ var $radio = $(this);
435
+ setDirtyStatus($radio, isFieldDirty($radio, ignoreSelector));
436
+ });
437
+ } else {
438
+ setDirtyStatus($field, isFieldDirty($field, ignoreSelector));
439
+ }
440
+ };
441
+
442
+ var setDirtyStatus = function ($field, isDirty) {
443
+ dirtylog('Setting dirty status to ' + isDirty + ' on field ' + $field.attr('id'));
444
+ var dirtyClass = $.DirtyForms.dirtyClass,
445
+ $form = $field.parents('form');
446
+
447
+ // Mark the field dirty/clean
448
+ $field.toggleClass(dirtyClass, isDirty);
449
+ var changed = (isDirty !== ($form.hasClass(dirtyClass) && $form.find(':dirty').length === 0));
450
+
451
+ if (changed) {
452
+ dirtylog('Setting dirty status to ' + isDirty + ' on form ' + $form.attr('id'));
453
+ $form.toggleClass(dirtyClass, isDirty);
454
+
455
+ if (isDirty) $form.trigger('dirty.dirtyforms');
456
+ if (!isDirty) $form.trigger('clean.dirtyforms');
457
+ }
458
+ };
459
+
460
+ // A delay to keep the key events from slowing down when changing the dirty status on the fly.
461
+ var delay = (function () {
462
+ var timer = 0;
463
+ return function (callback, ms) {
464
+ clearTimeout(timer);
465
+ timer = setTimeout(callback, ms);
466
+ };
467
+ })();
468
+
469
+ var bindFn = function (ev) {
470
+ var $element = $(ev.target),
471
+ eventType = ev.type,
472
+ dirtyForms = $.DirtyForms;
473
+ dirtylog('Entering: Leaving Event fired, type: ' + eventType + ', element: ' + ev.target + ', class: ' + $element.attr('class') + ' and id: ' + ev.target.id);
474
+
475
+ // Important: Do this check before calling events.clearUnload()
476
+ if (ev.isDefaultPrevented()) {
477
+ dirtylog('Leaving: Event has been stopped elsewhere');
478
+ return false;
479
+ }
480
+
481
+ if (eventType == 'beforeunload' && state.doubleunloadfix) {
482
+ dirtylog('Skip this unload, Firefox bug triggers the unload event multiple times');
483
+ state.doubleunloadfix = false;
484
+ return false;
485
+ }
486
+
487
+ if ($element.is(':dirtyignored')) {
488
+ dirtylog('Leaving: Element has ignore class or a descendant of an ignored element');
489
+ events.clearUnload();
490
+ return false;
491
+ }
492
+
493
+ if (state.deciding) {
494
+ dirtylog('Leaving: Already in the deciding process');
495
+ return false;
496
+ }
497
+
498
+ if (!$('form:dirtylistening').dirtyForms('isDirty')) {
499
+ dirtylog('Leaving: Not dirty');
500
+ events.clearUnload();
501
+ return false;
502
+ }
503
+
504
+ if (eventType == 'submit' && $element.dirtyForms('isDirty')) {
505
+ dirtylog('Leaving: Form submitted is a dirty form');
506
+ events.clearUnload();
507
+ return true;
508
+ }
509
+
510
+ // Callback for page access in current state
511
+ $(document).trigger('defer.dirtyforms');
512
+
513
+ if (eventType == 'beforeunload') {
514
+ dirtylog('Returning to beforeunload browser handler with: ' + dirtyForms.message);
515
+ return dirtyForms.message;
516
+ }
517
+ if (!dirtyForms.dialog) return;
518
+
519
+ // Using the GUI dialog...
520
+ ev.preventDefault();
521
+ ev.stopImmediatePropagation();
522
+
523
+ dirtylog('Setting deciding active');
524
+ state.deciding = true;
525
+ state.decidingEvent = ev;
526
+
527
+ // Stash the dialog (with a form). This is done so it can be shown again via unstash().
528
+ if ($.isFunction(dirtyForms.dialog.stash)) {
529
+ dirtylog('Stashing dialog content');
530
+ state.dialogStash = dirtyForms.dialog.stash();
531
+ dirtylog('Dialog Stash: ' + state.dialogStash);
532
+ }
533
+
534
+ // Stash the form from the dialog. This is done so we can fire events on it if the user makes a proceed choice.
535
+ var stashSelector = dirtyForms.dialog.stashSelector;
536
+ if (typeof stashSelector === 'string' && $element.is('form') && $element.parents(stashSelector).length > 0) {
537
+ dirtylog('Stashing form');
538
+ state.formStash = $element.clone(true).hide();
539
+ } else {
540
+ state.formStash = false;
541
+ }
542
+
543
+ dirtylog('Deferring to the dialog');
544
+
545
+ // Create a new choice object
546
+ choice = {
547
+ proceed: false,
548
+ commit: function (ev) {
549
+ return doCommit(ev, choice.proceed);
550
+ },
551
+ bindEscKey: true,
552
+ bindEnterKey: false,
553
+ proceedSelector: '',
554
+ staySelector: ''
555
+ };
556
+
557
+ dirtyForms.dialog.open(choice, dirtyForms.message, dirtyForms.ignoreClass);
558
+ bindDialog(choice);
559
+ };
560
+
561
+ var refire = function (ev) {
562
+ if (ev.type === 'click') {
563
+ dirtylog("Refiring click event");
564
+ events.onRefireClick(ev);
565
+ } else {
566
+ dirtylog("Refiring " + ev.type + " event on " + ev.target);
567
+ var target;
568
+ if (state.formStash) {
569
+ dirtylog('Appending stashed form to body');
570
+ target = state.formStash;
571
+ $('body').append(target);
572
+ }
573
+ else {
574
+ target = $(ev.target).closest('form');
575
+ }
576
+ target.trigger(ev.type);
577
+ }
578
+ };
579
+
580
+ /*<log>*/
581
+ var dirtylog = function (msg) {
582
+ if (!$.DirtyForms.debug) return;
583
+ var hasFirebug = 'console' in window && 'firebug' in window.console,
584
+ hasConsoleLog = 'console' in window && 'log' in window.console;
585
+ msg = '[DirtyForms] ' + msg;
586
+ if (hasFirebug) {
587
+ console.log(msg);
588
+ } else if (hasConsoleLog) {
589
+ window.console.log(msg);
590
+ } else {
591
+ alert(msg);
592
+ }
593
+ };
594
+ /*</log>*/
595
+
596
+ })(jQuery, window, document);