From fba4ee35041c1db92672dbadb2502ba96d51ef00 Mon Sep 17 00:00:00 2001 From: Akzhan Abdulin Date: Sun, 3 Jul 2011 14:03:37 +0200 Subject: [PATCH 1/7] Use Deferred object as result of $.rails.confirm. It allows to override confirm in an asynchronic fashion. --- src/rails.js | 162 +++++++++++++++++++++++++++++++++++---------------- 1 file changed, 113 insertions(+), 49 deletions(-) diff --git a/src/rails.js b/src/rails.js index d6e585fa..9eee8f1b 100644 --- a/src/rails.js +++ b/src/rails.js @@ -51,8 +51,8 @@ // Link elements bound by jquery-ujs linkClickSelector: 'a[data-confirm], a[data-method], a[data-remote]', - // Select elements bound by jquery-ujs - selectChangeSelector: 'select[data-remote]', + // Select elements bound by jquery-ujs + selectChangeSelector: 'select[data-remote]', // Form elements bound by jquery-ujs formSubmitSelector: 'form', @@ -85,9 +85,21 @@ return event.result !== false; }, + resolveOrReject: function(deferred, resolved) { + if (resolved) { + deferred.resolve(); + } else { + deferred.reject(); + } + return deferred; + }, + // Default confirm dialog, may be overridden with custom confirm dialog in $.rails.confirm confirm: function(message) { - return confirm(message); + var res = confirm(message), + answer = $.Deferred(); + rails.resolveOrReject(answer, res); + return answer.promise(); }, // Default ajax function, may be overridden with custom function in $.rails.ajax @@ -207,14 +219,25 @@ */ allowAction: function(element) { var message = element.data('confirm'), - answer = false, callback; - if (!message) { return true; } + confirmAnswer, answer = $.Deferred(); + if (!message) { return $.when(true); } if (rails.fire(element, 'confirm')) { - answer = rails.confirm(message); - callback = rails.fire(element, 'confirm:complete', [answer]); + confirmAnswer = rails.confirm(message); + confirmAnswer.then( + function() { + var callbackOk = rails.fire(element, 'confirm:complete', [ true ]); + rails.resolveOrReject(answer, callbackOk); + }, + function() { + rails.fire(element, 'confirm:complete', [ false ]); + answer.reject(); + } + ); + return answer.promise(); } - return answer && callback; + answer.reject(); + return answer.promise(); }, // Helper function which checks for blank inputs in a form that match the specified CSS selector @@ -260,65 +283,105 @@ $(rails.linkClickSelector).live('click.rails', function(e) { var link = $(this); - if (!rails.allowAction(link)) return rails.stopEverything(e); - - if (link.data('remote') !== undefined) { - rails.handleRemote(link); - return false; - } else if (link.data('method')) { - rails.handleMethod(link); - return false; + if (link.data('rails:click:inner')) { + return; } + e.preventDefault(); + + rails.allowAction(link).then( + function() { + if (link.data('remote') !== undefined) { + rails.handleRemote(link); + } else if (link.data('method')) { + rails.handleMethod(link); + } else { + link.data('rails:click:inner', true); + link.click(); + link.data('rails:click:inner', false); + } + }, + function() { + rails.stopEverything(e); + }); }); - $(rails.selectChangeSelector).live('change.rails', function(e) { + $(rails.selectChangeSelector).live('change.rails', function(e) { var link = $(this); - if (!rails.allowAction(link)) return rails.stopEverything(e); - - rails.handleRemote(link); - return false; - }); + e.preventDefault(); + rails.allowAction(link).then( + function() { + rails.handleRemote(link); + }, + function() { + rails.stopEverything(e); + } + ); + }); $(rails.formSubmitSelector).live('submit.rails', function(e) { var form = $(this), - remote = form.data('remote') !== undefined, + remote = (form.data('remote') !== undefined), blankRequiredInputs = rails.blankInputs(form, rails.requiredInputSelector), nonBlankFileInputs = rails.nonBlankInputs(form, rails.fileInputSelector); - if (!rails.allowAction(form)) return rails.stopEverything(e); - - // skip other logic when required values are missing or file upload is present - if (blankRequiredInputs && form.attr("novalidate") == undefined && rails.fire(form, 'ajax:aborted:required', [blankRequiredInputs])) { - return rails.stopEverything(e); + if (form.data('rails:form:submit:inner')) { + return; } - if (remote) { - if (nonBlankFileInputs) { - return rails.fire(form, 'ajax:aborted:file', [nonBlankFileInputs]); - } + e.preventDefault(); - // If browser does not support submit bubbling, then this live-binding will be called before direct - // bindings. Therefore, we should directly call any direct bindings before remotely submitting form. - if (!$.support.submitBubbles && rails.callFormSubmitBindings(form) === false) return rails.stopEverything(e); - rails.handleRemote(form); - return false; - } else { - // slight timeout so that the submit button gets properly serialized - setTimeout(function(){ rails.disableFormElements(form); }, 13); - } - }); + rails.allowAction(form).then( + function() { + // skip other logic when required values are missing or file upload is present + if (blankRequiredInputs && form.attr("novalidate") == undefined && rails.fire(form, 'ajax:aborted:required', [blankRequiredInputs])) { + return rails.stopEverything(e); + } - $(rails.formInputClickSelector).live('click.rails', function(event) { - var button = $(this); + if (remote) { + if (nonBlankFileInputs) { + return rails.fire(form, 'ajax:aborted:file', [nonBlankFileInputs]); + } - if (!rails.allowAction(button)) return rails.stopEverything(event); + // If browser does not support submit bubbling, then this live-binding will be called before direct + // bindings. Therefore, we should directly call any direct bindings before remotely submitting form. + if (!$.support.submitBubbles && rails.callFormSubmitBindings(form) === false) return rails.stopEverything(e); - // register the pressed submit button - var name = button.attr('name'), - data = name ? {name:name, value:button.val()} : null; + rails.handleRemote(form); + } else { + // slight timeout so that the submit button gets properly serialized + setTimeout(function() { + rails.disableFormElements(form); + form.data('rails:form:submit:inner', true); + form.submit(); + form.data('rails:form:submit:inner', false); + }, 13); + } + }, + function() { + rails.stopEverything(e); + } + ); + }); - button.closest('form').data('ujs:submit-button', data); + $(rails.formInputClickSelector).live('click.rails', function(event) { + var button = $(this); + e.preventDefault(); + + rails.allowAction(button).then( + function() { + // register the pressed submit button + var name = button.attr('name'), form, + data = name ? {name:name, value:button.val()} : null; + + form = button.closest('form'); + form.data('ujs:submit-button', data); + form.submit(); + }, + function() { + rails.stopEverything(event); + } + ); }); $(rails.formSubmitSelector).live('ajax:beforeSend.rails', function(event) { @@ -330,3 +393,4 @@ }); })( jQuery ); + From e090f2beb5cc377a8ba74c00f66d656499240665 Mon Sep 17 00:00:00 2001 From: Steve Schwartz Date: Sun, 4 Sep 2011 16:29:44 -0400 Subject: [PATCH 2/7] Reformatted some stuff and moved preventDefault events to be less obtrusive --- src/rails.js | 76 +++++++++++++++++++++++++++++++--------------------- 1 file changed, 46 insertions(+), 30 deletions(-) diff --git a/src/rails.js b/src/rails.js index 9eee8f1b..a2cee9f0 100644 --- a/src/rails.js +++ b/src/rails.js @@ -97,7 +97,8 @@ // Default confirm dialog, may be overridden with custom confirm dialog in $.rails.confirm confirm: function(message) { var res = confirm(message), - answer = $.Deferred(); + answer = $.Deferred(); + rails.resolveOrReject(answer, res); return answer.promise(); }, @@ -109,10 +110,10 @@ // Submits "remote" forms and links with ajax handleRemote: function(element) { - var method, url, data, - crossDomain = element.data('cross-domain') || null, - dataType = element.data('type') || ($.ajaxSettings && $.ajaxSettings.dataType), - options; + var method, url, data, button, + crossDomain = element.data('cross-domain') || null, + dataType = element.data('type') || ($.ajaxSettings && $.ajaxSettings.dataType), + options; if (rails.fire(element, 'ajax:before')) { @@ -121,7 +122,7 @@ url = element.attr('action'); data = element.serializeArray(); // memoized value from clicked submit button - var button = element.data('ujs:submit-button'); + button = element.data('ujs:submit-button'); if (button) { data.push(button); element.data('ujs:submit-button', null); @@ -132,9 +133,9 @@ data = element.serialize(); if (element.data('params')) data = data + "&" + element.data('params'); } else { - method = element.data('method'); - url = element.attr('href'); - data = element.data('params') || null; + method = element.data('method'); + url = element.attr('href'); + data = element.data('params') || null; } options = { @@ -167,11 +168,11 @@ // Delete handleMethod: function(link) { var href = link.attr('href'), - method = link.data('method'), - csrf_token = $('meta[name=csrf-token]').attr('content'), - csrf_param = $('meta[name=csrf-param]').attr('content'), - form = $('
'), - metadata_input = ''; + method = link.data('method'), + csrf_token = $('meta[name=csrf-token]').attr('content'), + csrf_param = $('meta[name=csrf-param]').attr('content'), + form = $('
'), + metadata_input = ''; if (csrf_param !== undefined && csrf_token !== undefined) { metadata_input += ''; @@ -188,7 +189,9 @@ */ disableFormElements: function(form) { form.find(rails.disableSelector).each(function() { - var element = $(this), method = element.is('button') ? 'html' : 'val'; + var element = $(this), + method = element.is('button') ? 'html' : 'val'; + element.data('ujs:enable-with', element[method]()); element[method](element.data('disable-with')); element.attr('disabled', 'disabled'); @@ -201,7 +204,9 @@ */ enableFormElements: function(form) { form.find(rails.enableSelector).each(function() { - var element = $(this), method = element.is('button') ? 'html' : 'val'; + var element = $(this), + method = element.is('button') ? 'html' : 'val'; + if (element.data('ujs:enable-with')) element[method](element.data('ujs:enable-with')); element.removeAttr('disabled'); }); @@ -219,7 +224,9 @@ */ allowAction: function(element) { var message = element.data('confirm'), - confirmAnswer, answer = $.Deferred(); + confirmAnswer, + answer = $.Deferred(); + if (!message) { return $.when(true); } if (rails.fire(element, 'confirm')) { @@ -235,15 +242,18 @@ } ); return answer.promise(); + // If `confirm` event handler returned false... + } else { + answer.reject(); + return answer.promise(); } - answer.reject(); - return answer.promise(); }, // Helper function which checks for blank inputs in a form that match the specified CSS selector blankInputs: function(form, specifiedSelector, nonBlank) { var inputs = $(), input, - selector = specifiedSelector || 'input,textarea'; + selector = specifiedSelector || 'input,textarea'; + form.find(selector).each(function() { input = $(this); // Collect non-blank inputs if nonBlank option is true, otherwise, collect blank inputs @@ -270,6 +280,7 @@ // manually invoke them. If anyone returns false then stop the loop callFormSubmitBindings: function(form) { var events = form.data('events'), continuePropagation = true; + if (events !== undefined && events['submit'] !== undefined) { $.each(events['submit'], function(i, obj){ if (typeof obj.handler === 'function') return continuePropagation = obj.handler(obj.data); @@ -283,10 +294,10 @@ $(rails.linkClickSelector).live('click.rails', function(e) { var link = $(this); + if (link.data('rails:click:inner')) { return; } - e.preventDefault(); rails.allowAction(link).then( function() { @@ -302,12 +313,15 @@ }, function() { rails.stopEverything(e); - }); + } + ); + + e.preventDefault(); }); $(rails.selectChangeSelector).live('change.rails', function(e) { var link = $(this); - e.preventDefault(); + rails.allowAction(link).then( function() { rails.handleRemote(link); @@ -316,21 +330,20 @@ rails.stopEverything(e); } ); + + e.preventDefault(); }); $(rails.formSubmitSelector).live('submit.rails', function(e) { var form = $(this), - remote = (form.data('remote') !== undefined), - blankRequiredInputs = rails.blankInputs(form, rails.requiredInputSelector), - nonBlankFileInputs = rails.nonBlankInputs(form, rails.fileInputSelector); + remote = (form.data('remote') !== undefined), + blankRequiredInputs = rails.blankInputs(form, rails.requiredInputSelector), + nonBlankFileInputs = rails.nonBlankInputs(form, rails.fileInputSelector); if (form.data('rails:form:submit:inner')) { return; } - e.preventDefault(); - - rails.allowAction(form).then( function() { // skip other logic when required values are missing or file upload is present @@ -362,11 +375,12 @@ rails.stopEverything(e); } ); + + e.preventDefault(); }); $(rails.formInputClickSelector).live('click.rails', function(event) { var button = $(this); - e.preventDefault(); rails.allowAction(button).then( function() { @@ -382,6 +396,8 @@ rails.stopEverything(event); } ); + + e.preventDefault(); }); $(rails.formSubmitSelector).live('ajax:beforeSend.rails', function(event) { From bffb813c1ec115f866025381c8da275a8c6a7826 Mon Sep 17 00:00:00 2001 From: Steve Schwartz Date: Mon, 3 Oct 2011 19:18:13 -0400 Subject: [PATCH 3/7] Rewrote portion of rails.confirm deferred calls to eliminate double-triggering of directly bound handlers outside of jquery-ujs. See #196. --- src/rails.js | 36 +++++++++++-------------- test/public/test/settings.js | 51 +++++++++++++++++++++++++++--------- 2 files changed, 54 insertions(+), 33 deletions(-) diff --git a/src/rails.js b/src/rails.js index a2cee9f0..01829a6f 100644 --- a/src/rails.js +++ b/src/rails.js @@ -168,14 +168,19 @@ // Delete handleMethod: function(link) { var href = link.attr('href'), - method = link.data('method'), + method = link.data('method') || 'GET', csrf_token = $('meta[name=csrf-token]').attr('content'), csrf_param = $('meta[name=csrf-param]').attr('content'), - form = $('
'), - metadata_input = ''; + form = $('
', { action: href }), + metadata_input = ''; - if (csrf_param !== undefined && csrf_token !== undefined) { - metadata_input += ''; + if (method !== 'GET') { + form.attr('method', 'POST'); + metadata_input += ''; + + if (csrf_param !== undefined && csrf_token !== undefined) { + metadata_input += ''; + } } form.hide().append(metadata_input).appendTo('body'); @@ -295,20 +300,12 @@ $(rails.linkClickSelector).live('click.rails', function(e) { var link = $(this); - if (link.data('rails:click:inner')) { - return; - } - rails.allowAction(link).then( function() { if (link.data('remote') !== undefined) { rails.handleRemote(link); - } else if (link.data('method')) { - rails.handleMethod(link); } else { - link.data('rails:click:inner', true); - link.click(); - link.data('rails:click:inner', false); + rails.handleMethod(link); } }, function() { @@ -340,10 +337,6 @@ blankRequiredInputs = rails.blankInputs(form, rails.requiredInputSelector), nonBlankFileInputs = rails.nonBlankInputs(form, rails.fileInputSelector); - if (form.data('rails:form:submit:inner')) { - return; - } - rails.allowAction(form).then( function() { // skip other logic when required values are missing or file upload is present @@ -365,9 +358,10 @@ // slight timeout so that the submit button gets properly serialized setTimeout(function() { rails.disableFormElements(form); - form.data('rails:form:submit:inner', true); - form.submit(); - form.data('rails:form:submit:inner', false); + // Submit the form from dom-level js (i.e. *not* via jquery), + // which will skip all submit bindings (including this live-binding), + // since they have already been called. + form.get(0).submit(); }, 13); } }, diff --git a/test/public/test/settings.js b/test/public/test/settings.js index c58734d7..488cf241 100644 --- a/test/public/test/settings.js +++ b/test/public/test/settings.js @@ -22,15 +22,42 @@ App.assert_request_path = function(request_env, path) { // hijacks normal form submit; lets it submit to an iframe to prevent // navigating away from the test suite -$(document).bind('submit', function(e) { - if (!e.isDefaultPrevented()) { - var form = $(e.target), action = form.attr('action'), - name = 'form-frame' + jQuery.guid++, - iframe = $('