diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 8b4dcfa9a..3a53d73bf 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -158,24 +158,6 @@ $(document.body).ready(function () { }); }); -/* - * Truncate long strings where is necessary - */ - -function truncateLongString( el, chars ) { - var input = $.trim(el.text()); - - if( input.length >= chars){ - var newText = el.text().slice(0, chars); - for( var i = newText.length; i > 0; i--){ - if(newText[i] === ' '){ - newText = newText.slice(0, i); - break; - } - } - el.text(newText + '...'); - } -} $(document).ready(function(){ $('.tree-link a').each( function(){ truncateLongString( $(this), 30); diff --git a/app/assets/javascripts/direct-upload.js b/app/assets/javascripts/direct-upload.js index 353211ae6..aa5278567 100644 --- a/app/assets/javascripts/direct-upload.js +++ b/app/assets/javascripts/direct-upload.js @@ -102,10 +102,10 @@ exports.directUpload = function (form, origId, signUrl, cb, cbErr, errKey) { - var fileInputs = $(form).find("input[type=file]"); - var file = fileInputs.get(0).files[0]; + var $fileInputs = $(form).find("input[type=file]"); + var file = $fileInputs.get(0).files[0]; - var isValid = checkFilesValidity(fileInputs); + var isValid = filesValidator($fileInputs); if (!isValid) { cbErr(); diff --git a/app/assets/javascripts/protocols/steps.js b/app/assets/javascripts/protocols/steps.js index 36f3c2be5..8daf5368c 100644 --- a/app/assets/javascripts/protocols/steps.js +++ b/app/assets/javascripts/protocols/steps.js @@ -185,9 +185,6 @@ function formEditAjax($form) { reorderCheckboxData(this); }); }) - .on("ajax:send", function(e, data) { - selectedTabIndex = $form.find("li.active").index() + 1; - }) .on("ajax:success", function(e, data) { $(this).after(data.html); var $new_step = $(this).next(); @@ -212,23 +209,19 @@ function formEditAjax($form) { var $form = $(this).next(); $(this).remove(); + $errInput = $form.find(".form-group.has-error").first().find("input"); + renderFormError($errInput); + formCallback($form); initEditableHandsOnTable($form); applyCancelCallBack(); formEditAjax($form); - tabsPropagateErrorClass($form); //Rerender tables $form.find("[data-role='step-hot-table']").each(function() { renderTable($(this)); }); - // Select the same tab pane as before - $form.find("ul.nav-tabs li.active").removeClass("active"); - $form.find(".tab-content div.active").removeClass("active"); - $form.find("ul.nav-tabs li:nth-child(" + selectedTabIndex + ")").addClass("active"); - $form.find(".tab-content div:nth-child(" + selectedTabIndex + ")").addClass("active"); - animateSpinner(null, false); }); } @@ -262,10 +255,12 @@ function formNewAjax($form) { var $form = $(this).next(); $(this).remove(); + $errInput = $form.find(".form-group.has-error").first().find("input"); + renderFormError($errInput); + formCallback($form); formNewAjax($form); applyCancelOnNew(); - tabsPropagateErrorClass($form); animateSpinner(null, false); }); @@ -583,89 +578,37 @@ $("[data-action='new-step']").on("ajax:success", function(e, data) { }); }); -function nameValidator(event) { - var $form = $(event.target.form); - var nameTooShort = $( "#step_name" ).val().length === 0; - var nameTooLong = $( "#step_name" ).val().length > 50; - var errMsg; - if (nameTooShort) { - errMsg = I18n.t("devise.names.not_blank"); - } else if (nameTooLong) { - errMsg = I18n.t("devise.names.length_too_long", { max_length: 50 }); - animateSpinner(null,false); - } - - var hasErrors = !_.isUndefined(errMsg); - if (hasErrors) { - renderError($("#step_name"), errMsg); - } - return !hasErrors; -} - -function checklistsValidator(event, editMode) { - var $form = $(event.target.form); - var noErrors = true; - - // For every visible (i.e. not removed) checklist - $form.find(".nested_step_checklists[style!='display: none']").each(function() { - var $checklistNameInput = $(this).find(".checklist_name"); - var checklistNameEmpty = !$checklistNameInput.val(); - anyChecklistItemFilled = false; - - // For every ckecklist item input - $(" .checklist-item-text", $(this)).each(function() { - if($(this).val()) { - anyChecklistItemFilled = true; - } else { - // Remove empty checklist item - $(this).closest("fieldset").remove(); - } - }) - - if(checklistNameEmpty) { - if(anyChecklistItemFilled || editMode) { - // In edit mode, name can't be blank - var errMsg = I18n.t("devise.names.not_blank"); - renderError($checklistNameInput, errMsg); - noErrors = false; - } else { - // Hide empty checklist - $(this).hide(); - } - } - }); - - $(event.target).blur(); - return noErrors; -} - // Needed because server-side validation failure clears locations of // files to be uploaded and checklist's items etc. Also user // experience is improved -function stepValidator(event, editMode, forS3) { - var $form = $(event.target.form); - $form.clear_form_errors(); - if(!editMode) { - // Most td's disappear when editing step and not pressing on - // tables tab, so we can't use this function - clearBlankTables($form) - } - clearBlankFileForms($form); - // TODO File type check - var fileSizeValid = uploadFileSizeCheck(event); - var checklistsValid = checklistsValidator(event, editMode); - var nameValid = nameValidator(event); +function stepValidator(ev, editMode, forS3) { + var $form = $(ev.target.form); - if(fileSizeValid && checklistsValid && nameValid) { + $form.clear_form_errors(); + $tables = $form.find(".nested_step_tables"); + removeBlankExcelTables($tables, editMode); + removeBlankFileForms($form); + + var $fileInputs = $form.find("input[type=file]"); + var filesValid = filesValidator($fileInputs); + var $checklists = $form.find(".nested_step_checklists"); + var checklistsValid = checklistsValidator($checklists, editMode); + var $nameInput = $form.find("#step_name"); + var nameValid = nameValidator($nameInput); + + if(filesValid && checklistsValid && nameValid) { if(forS3) { // Needed to redirect uploaded files to S3 - startFileUpload(event, event.target); + startFileUpload(ev, ev.target); } else { // Files are saved locally // Validations passed, so animate spinner for possible file uploading // (startFileUpload already calls it) animateSpinner(); } + } else { + // Don't submit form + ev.preventDefault(); } } @@ -727,8 +670,6 @@ function startFileUpload(ev, btn) { var inputPos = 0; var inputPointer = 0; - $form.clear_form_errors(); - clearBlankFileForms(form); animateSpinner(); function processFile () { @@ -763,7 +704,7 @@ function startFileUpload(ev, btn) { var $el = $(el); $form.clear_form_errors(); - renderError($el, assetError); + renderFormError($el, assetError); } else { tabsPropagateErrorClass($form); } @@ -777,30 +718,3 @@ function startFileUpload(ev, btn) { ev.preventDefault(); return noErrors; } - -// Remove empty file forms in step -function clearBlankFileForms(form) { - $(form).find("input[type='file']").each(function () { - if (!this.files[0]) { - $(this).closest("fieldset").remove(); - } - }); -} - -// Remove empty tables in step -function clearBlankTables(form) { - $(form).find(".nested_step_tables").each(function () { - if (!$(this).find("td:not(:empty)").length) { - $(this).closest("fieldset").remove(); - } - }); -} - -// This checks if the ctarget param exist in the -// rendered url and opens the comment tab -$(document).ready(function(){ - if( getParam('ctarget') ){ - var target = "#"+ getParam('ctarget'); - $(target).find('a.comment-tab-link').click(); - } -}); diff --git a/app/assets/javascripts/results/result_assets.js b/app/assets/javascripts/results/result_assets.js index 4cf0a0550..518fb5db3 100644 --- a/app/assets/javascripts/results/result_assets.js +++ b/app/assets/javascripts/results/result_assets.js @@ -3,7 +3,7 @@ $("#new-result-asset").on("ajax:success", function(e, data) { var $form = $(data.html); $("#results").prepend($form); - $form.add_upload_file_size_check(); + $form.files_validator(); formAjaxResultAsset($form); // Cancel button @@ -30,7 +30,7 @@ function applyEditResultAssetCallback() { $result.after($form); $result.remove(); - $form.add_upload_file_size_check(); + $form.files_validator(); formAjaxResultAsset($form); // Cancel button diff --git a/app/assets/javascripts/sitewide/file_upload_handling.js.erb b/app/assets/javascripts/sitewide/file_upload_handling.js.erb deleted file mode 100644 index b445a9477..000000000 --- a/app/assets/javascripts/sitewide/file_upload_handling.js.erb +++ /dev/null @@ -1,27 +0,0 @@ -function checkFilesValidity(fileInputs) { - - function getFileError(file) { - if (!file) { - return ; - } - var size = parseInt(file.size); - <% sizeLimit = FILE_SIZE_LIMIT %>; - if (size > <%= sizeLimit.megabytes %>) { - return "<%= I18n.t 'general.file_size_exceeded', file_size: sizeLimit %>"; - } - }; - - var isValid = true; - _.each(fileInputs, function(fileInput) { - var file = fileInput.files[0]; - var assetError = getFileError(file); - var input = $(fileInput); - input.closest('.form-group').removeClass('has-error'); - input.parent().find("[data-error='file-size']").remove(); - if (assetError) { - renderError(input, assetError, "data-error='file-size'"); - isValid = false; - } - }); - return isValid; -} \ No newline at end of file diff --git a/app/assets/javascripts/sitewide/form_cleanups.js b/app/assets/javascripts/sitewide/form_cleanups.js new file mode 100644 index 000000000..d65ebf9cb --- /dev/null +++ b/app/assets/javascripts/sitewide/form_cleanups.js @@ -0,0 +1,35 @@ +$.fn.clear_form_errors = function() { + $(this).find('.nav.nav-tabs li').removeClass('has-error'); + $(this).find('.form-group').removeClass('has-error'); + $(this).find('span.help-block').remove(); +}; + +$.fn.clear_form_fields = function() { + $(this).find("input") + .not("button") + .not('input[type="submit"], input[type="reset"], input[type="hidden"]') + .not('input[type="radio"]') // Leave out radios as this messes up Bootstrap btn-groups + .val('') + .removeAttr('checked') + .removeAttr('selected'); +}; + +function removeBlankFileForms(form) { + $(form).find("input[type='file']").each(function () { + if (!this.files[0]) { + $(this).closest("fieldset").remove(); + } + }); +} + +// Not the actual Excel tables, but are similar +function removeBlankExcelTables(tables, editMode) { + if(!editMode) { + // In edit mode, tables can't be blank + tables.each(function () { + if (!$(this).find("td:not(:empty)").length) { + $(this).closest("fieldset").remove(); + } + }); + } +} \ No newline at end of file diff --git a/app/assets/javascripts/sitewide/form_errors.js b/app/assets/javascripts/sitewide/form_errors.js index 9566bf26a..fba7b50b4 100644 --- a/app/assets/javascripts/sitewide/form_errors.js +++ b/app/assets/javascripts/sitewide/form_errors.js @@ -36,73 +36,26 @@ $.fn.render_form_errors_no_clear = function(model_name, errors, input_group) { }); }; -$.fn.clear_form_errors = function() { - $(this).find('.nav.nav-tabs li').removeClass('has-error'); - $(this).find('.form-group').removeClass('has-error'); - $(this).find('span.help-block').remove(); -}; - -$.fn.clear_form_fields = function() { - $(this).find("input") - .not("button") - .not('input[type="submit"], input[type="reset"], input[type="hidden"]') - .not('input[type="radio"]') // Leave out radios as this messes up Bootstrap btn-groups - .val('') - .removeAttr('checked') - .removeAttr('selected'); -}; - -// Add JavaScript client-side upload file size checking -// Callback function can be provided to be called -// any time at least one file size is too large -$.fn.add_upload_file_size_check = function(callback) { - var $form = $(this); - - if ($form.length && $form.length > 0) { - $form.submit(function (ev) { - uploadFileSizeCheck(ev, callback); - }); - } -}; - -function uploadFileSizeCheck(ev, callback) { - var $fileInputs = $(ev.target.form).find("input[type='file']"); - if ($fileInputs.length && $fileInputs.length > 0) { - var isValid = checkFilesValidity($fileInputs); - - if (!isValid) { - // Don't submit form - ev.preventDefault(); - ev.stopPropagation(); - - if (callback) { - callback(); - } - - return false; - } - } - return true; -} - - // Show error message and mark error element and, if present, mark - // and show the tab where the error occured. + // Show error message and mark error input (if errMsg is defined) + // and, if present, mark and show the tab where the error occured, + // and go to the input, if it is the most upper one or if errMsg is + // undefined // NOTE: Similar to $.fn.render_form_errors, except here we process // one error at a time, which is not read from the form but is // specified manually. -function renderError(nameInput, errMsg, errAttributes) { - var $errMsgSpan = $(nameInput).next(".help-block"); - if(!$errMsgSpan.length) { - errAttributes = (_.isUndefined(errAttributes)) ? "" : " " + errAttributes; +function renderFormError(nameInput, errMsg, errAttributes) { + if(!_.isUndefined(errMsg)) { + var $errMsgSpan = $(nameInput).next(".help-block"); + errAttributes = _.isUndefined(errAttributes) ? "" : " " + errAttributes; + if (!$errMsgSpan.length) { + $(nameInput).closest(".form-group").addClass("has-error"); + } $(nameInput).after("" + errMsg + ""); - $(nameInput).closest(".form-group").addClass("has-error"); - } else { - $errMsgSpan.html(errMsg); } $form = $(nameInput).closest("form"); $tab = $(nameInput).closest(".tab-pane"); - if($tab.length) { + if ($tab.length) { tabsPropagateErrorClass($form); $parent = $tab; } else { @@ -110,27 +63,27 @@ function renderError(nameInput, errMsg, errAttributes) { } // Focus and scroll to the error if it is the first (most upper) one - if($parent.find(".form-group.has-error").length === 1) { + if ($parent.find(".form-group.has-error").length === 1 || _.isUndefined(errMsg)) { goToFormElement(nameInput); } event.preventDefault(); } -// If any of tabs (if exist) has errors, add has-error class to -// parent tab navigation link and show the tab (if not already) -function tabsPropagateErrorClass(parent) { - var $contents = parent.find("div.tab-pane"); +// If any of tabs (if exist) has errors, mark parent tab +// navigation link and show the tab (if not already) +function tabsPropagateErrorClass($form) { + var $contents = $form.find("div.tab-pane"); _.each($contents, function(tab) { var $tab = $(tab); var $errorFields = $tab.find(".has-error"); - if ($errorFields.length > 0) { + if ($errorFields.length) { var id = $tab.attr("id"); - var navLink = parent.find("a[href='#" + id + "'][data-toggle='tab']"); - if (navLink.parent().length > 0) { + var navLink = $form.find("a[href='#" + id + "'][data-toggle='tab']"); + if (navLink.parent().length) { navLink.parent().addClass("has-error"); } } }); - $(".nav-tabs .has-error:first:not(.active) > a", parent).tab("show"); + $(".nav-tabs .has-error:first:not(.active) > a", $form).tab("show"); } diff --git a/app/assets/javascripts/sitewide/gestures.js b/app/assets/javascripts/sitewide/gestures.js index f897ff5a1..f541a06d3 100644 --- a/app/assets/javascripts/sitewide/gestures.js +++ b/app/assets/javascripts/sitewide/gestures.js @@ -1,13 +1,13 @@ // Scroll to and focus on element -function goToFormElement(el) { +function goToFormElement(input) { $("html, body").animate( { - scrollTop: $(el).closest(".form-group").offset().top + scrollTop: $(input).closest(".form-group").offset().top - ($(".navbar-fixed-top").outerHeight(true) + $(".navbar-secondary").outerHeight(true) + $(".alert-dismissable").outerHeight(true)) }, "slow", - function() { $(el).focus(); } + function() { $(input).focus(); } ); } \ No newline at end of file diff --git a/app/assets/javascripts/sitewide/string_utils.js b/app/assets/javascripts/sitewide/string_utils.js new file mode 100644 index 000000000..67156d05c --- /dev/null +++ b/app/assets/javascripts/sitewide/string_utils.js @@ -0,0 +1,25 @@ +// Truncate long strings where is necessary +function truncateLongString( el, chars ) { + var input = $.trim(el.text()); + + if( input.length >= chars){ + var newText = el.text().slice(0, chars); + for( var i = newText.length; i > 0; i--){ + if(newText[i] === ' '){ + newText = newText.slice(0, i); + break; + } + } + el.text(newText + '...'); + } +} + +// Usefull for converting locals messages to form +// format (i.e. lower cased capital and no dot) +String.prototype.strToFormFormat = function() { + var length = this.length; + if (this[length - 1] === ".") { + length -= 1; + } + return this.charAt(0).toLowerCase() + this.slice(1, length); +} \ No newline at end of file diff --git a/app/assets/javascripts/sitewide/validators.js.erb b/app/assets/javascripts/sitewide/validators.js.erb new file mode 100644 index 000000000..121177670 --- /dev/null +++ b/app/assets/javascripts/sitewide/validators.js.erb @@ -0,0 +1,116 @@ +function nameValidator($nameInput) { + var nameTooShort = $nameInput.val().length === 0; + var nameTooLong = $nameInput.val().length > 50; + var errMsg; + if (nameTooShort) { + errMsg = I18n.t("devise.names.not_blank"); + } else if (nameTooLong) { + errMsg = I18n.t("devise.names.length_too_long", { max_length: 50 }); + } + + var hasErrors = !_.isUndefined(errMsg); + if (hasErrors) { + renderFormError($nameInput, errMsg); + } + return !hasErrors; +} + +function checklistsValidator(checklists, editMode) { + var noErrors = true; + // For every visible (i.e. not removed) checklist + checklists.each(function() { + $checklist = $(this); + if ($checklist.css('display') != 'none') { + var $checklistNameInput = $checklist.find(".checklist_name"); + anyChecklistItemFilled = false; + + // For every ckecklist item input + $(" .checklist-item-text", $checklist).each(function() { + $checklistItemInput = $(this); + if ($checklistItemInput.val()) { + anyChecklistItemFilled = true; + } else { + // Remove empty checklist item input + $checklistItemInput.closest("fieldset").remove(); + } + }) + + if (!$checklistNameInput.val()) { + if (anyChecklistItemFilled || editMode) { + // In edit mode, checklist's name can't be blank + var errMsg = I18n.t("devise.names.not_blank"); + renderFormError($checklistNameInput, errMsg); + noErrors = false; + } else { + // Hide empty checklist (remove would break logic) + $checklist.hide(); + } + } + } + }); + + return noErrors; +} + +// Add JavaScript client-side upload file checking +// Callback function can be provided to be called +// any time at least one file size is not valid +$.fn.files_validator = function(callback) { + var $form = $(this); + if ($form.length) { + $form.submit(function (ev) { + var $fileInputs = $form.find("input[type=file]"); + filesValidator($fileInputs, callback); + }); + } +}; + +function filesValidator(fileInputs, callback) { + var filesSizeValid = true; + if (fileInputs.length) { + var filesSizeValid = filesSizeValidator(fileInputs); + // TODO File content check + + if (!filesSizeValid && callback) { + callback(); + } + } + return filesSizeValid; +} + +function filesSizeValidator(fileInputs) { + + function getFileTooBigError(file) { + if (!file) { + return ; + } + var size = parseInt(file.size); + <% sizeLimit = FILE_SIZE_LIMIT %>; + if (size > <%= sizeLimit.megabytes %>) { + return "<%= I18n.t 'general.file_size_exceeded', file_size: sizeLimit %>".strToFormFormat(); + } + }; + + // Check if any file exceeds allowed size limit + var fileSizeValid = true; + _.each(fileInputs, function(fileInput) { + var file = fileInput.files[0]; + var assetError = getFileTooBigError(file); + var input = $(fileInput); + if (assetError) { + renderFormError(input, assetError, "data-error='file-size'"); + fileSizeValid = false; + } + }); + if(fileSizeValid) { + // Check if there is enough free space for the files + fileSizeValid = enaughSpaceValidator(fileInputs); + } + return fileSizeValid; +} + +// Overriden in billing module for checking +// whether enough organization space is free +function enaughSpaceValidator(fileInputs) { + return true; +} \ No newline at end of file diff --git a/app/assets/javascripts/users/registrations/edit.js b/app/assets/javascripts/users/registrations/edit.js index 7e02bceb1..136ef43a7 100644 --- a/app/assets/javascripts/users/registrations/edit.js +++ b/app/assets/javascripts/users/registrations/edit.js @@ -63,7 +63,7 @@ forms }); // Add upload file size checking -$("form[data-for='avatar']").add_upload_file_size_check(); +$("form[data-for='avatar']").files_validator(); // S3 direct uploading function startFileUpload(ev, btn) { @@ -103,7 +103,7 @@ function startFileUpload(ev, btn) { var $el = $form.find("input[type=file]"); $form.clear_form_errors(); - renderError($el, avatarError); + renderFormError($el, avatarError); } }, "avatar"); diff --git a/config/locales/en.yml b/config/locales/en.yml index b7afdd4bf..d393a8f8e 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -19,8 +19,8 @@ en: title: "Forgot your password?" submit: "Send me reset password instructions" names: - not_blank: "Name can't be blank" - length_too_long: "Name is too long (maximum is %{max_length} characters)" + not_blank: "name can't be blank" + length_too_long: "name is too long (maximum is %{max_length} characters)" registrations: password_changed: "Password successfully updated." sessions: