/*
- This is the code for the Forms
- The code is based of Kelly Drinkwater's code, which can be found here: https://igem.org/Template:JS/KForms_2pt4?action=raw&ctype=text/javascript
- /
function AjaxError(message) { this.name = "AjaxError"; this.message = message; this.stack = (new Error()).stack; } AjaxError.prototype = Object.create(Error.prototype);
function PerlError(message) { this.name = "PerlError"; this.message = message; this.stack = (new Error()).stack; } PerlError.prototype = Object.create(Error.prototype);
function FormError(message) { this.name = "FormError"; this.message = message; this.stack = (new Error()).stack; } FormError.prototype = Object.create(Error.prototype);
// =====================================================
// BLOCK
// URL parser and String.startsWith
// =====================================================
//This function parses a URL (or the current page's URL, if no argument is passed in)
//and returns an object full of useful properties.
//Reference:
// http://www.abeautifulsite.net/parsing-urls-in-javascript/
// https://gist.github.com/jlong/2428561
function parseURL(url) { if (typeof url == "undefined") { parser = window.location; } else { var parser = document.createElement('a'); // Let the browser do the work parser.href = url; }
var searchObject = {}, queries, split, i;
// Convert query string to object queries = parser.search.replace(/^\?/, ).split('\u0026'); for( i = 0; i < queries.length; i++ ) { split = queries[i].split('='); searchObject[split[0]] = split[1]; } return { protocol: parser.protocol, host: parser.host, hostname: parser.hostname, port: parser.port, pathname: parser.pathname, search: parser.search, searchObject: searchObject, hash: parser.hash }; }
if (typeof String.prototype.startsWith != "function") {
String.prototype.startsWith = function(str) {
return this.slice(0, str.length) == str;
}
}
//VARIABLES
var default_messages = { kicking_to_view_mode: "Returning to View mode.", not_kicking_to_view_mode: "Please return to View mode.", user_cannot_edit: "You are not a member of the selected team.\nIf this is an error, make sure you are logged in and your account is included on your team's roster.", user_cannot_submit: "You do not have permission to submit this form.", user_cannot_use_admin_mode: "You are not an administrator.", user_cannot_edit_admin_fields: "That field is only for use by iGEM Headquarters.",
//used by get_user_info for the case of a user-owned form only (right now, those can't be made public) not_logged_in: "You are not logged in. Please log in (or <a href='http:\/\/igem.org/Account_Apply.cgi'>sign up for an account</a>) to complete this form.", //to be used by OIC if the user is mid-editing and the silent logout bug happens. silent_logout_bug: "Oops, the silent logout bug happened!",
form_validation_fail_beginning: "Some required answers are missing:", unparseable_ajax_response: "There was an error in FORM.CGI. Please inform us of this error by emailing hq (at) igem (dot) org.", please_confirm_submission: "Please check the box to confirm your submission.", confirm_deletion: "Are you sure you want to delete this form?\nYou cannot undo this action.", field_validation_fail_beginning: "Answer missing:", invalid_email_address: "Invalid email address" };
var messages = jQuery.extend({}, default_messages, form_info.messages);
var non_admin_DFFs = "[data-form-field]:not([data-form-field^='admin'])"; //convenient selector for non-admin DFFs
var valid_modes = ["view", "edit", "admin"]; var valid_owner_types = ["user", "judge", "team", "lab", "course"];
//this is where the valid user/viewer categories are defined var valid_permission_groups = ["author", "group_members", "super_users", "any_users", "public"]; var valid_roles = ["Primary Contact", "Primary PI", "Secondary PI", "Student Leader", "Student", "Instructor", "Advisor", "Other", "None"]; var valid_required = ["required", "optional"];
//Explicitly initialize global variables var mode; var team_id; var author; var owner_info; var user_info; var form_displayed = false; var form_submitted = false; //sigh, it looks like I can't get away without this one
var permissions = {view: false, edit: false, submit: false, admin: false};
var parsed_query_string = parseURL().searchObject; mode = parsed_query_string["mode"];
if (form_info.owner_type == "team") { if (typeof team_id == "undefined") { team_id = parseInt(parsed_query_string["team_id"]); } } else if (form_info.owner_type == "user") { author = parsed_query_string["author"]; } else { throw new FormError("Judge, Lab, and Course owned forms are not implemented yet"); }
// =====================================================
// BLOCK
// VALIDATING INDIVIDUAL FORMS CONFIG
// =====================================================
function validate_form_info() { assert(typeof form_info == "object", "form_info is not an object"); assert(typeof form_info.name == "string", "form_info.name is not a string"); assert(typeof form_info.display_title == "string", "form_info.display_title is not a string"); assert(valid_modes.indexOf(form_info.default_mode) > -1, "form_info.default_mode is not valid"); assert(valid_owner_types.indexOf(form_info.owner_type) > -1, "form_info.owner_type is not valid"); assert(form_info.permissions instanceof Object, "form_info.permissions is not an object"); ["view", "edit", "submit", "admin"].forEach(function(action, index, array) { assert(form_info.permissions[action] instanceof Array, "form_info.permissions." + action + " is not an array"); form_info.permissions[action].forEach(function(perm_group, index, array) { assert(valid_permission_groups.indexOf(perm_group) > -1 || valid_roles.indexOf(perm_group) > -1 , "invalid permission group in form_info.permissions." + action); }); }); assert(typeof form_info.ajax_URL == "string", "form_info.ajax_URL is not a string"); assert(valid_required.indexOf(form_info.validate_unspecified_fields) > -1, "form_info.validate_unspecified_fields is not valid"); }
validate_form_info();
// ===================================================== // BLOCK // SETUP ON DOCUMENT.READY (yes it is the kitchen sink) // ===================================================== function document_ready_handler() {
//Validate mode and default to something if invalid
check_mode();
//NOW A WHOLE BUNCH OF RANDOM ELEMENT SETUP
//Display form title $("#form_title_display").text(form_info.display_title).show();
//read the nokick checkbox and bind it to change nokick nokick = $("#nokick").prop("checked"); $("#nokick").change(function(event) { nokick = $(this).prop("checked"); });
//bind author-select input on enter key $("#author_select").keyup(function(event) { if (event.which == 13) { change_author($(this).val()); } });
//apply wrapper divs for feedback function $("[data-form-field]:not(textarea)").wrap("<\/span>");
$("textarea[data-form-field]").wrap("$("p").find(".wrapper").has("[type=radio], [type=checkbox]").css("margin-left", "2em");
//force create AJ log and error divs
//$("#formbody").before("").after("");//display log div //(it will be hidden from non-admin users by default, but if you're debugging for such a user //then you can go into the console and show it -- you do want it to EXIST for all users. //So don't move this into an if-user-is-admin clause!) aj_display_log_div();
//bind click events for .change_mode(team,author) links //needs to delegate from #bodyContent because not all .change_mode links exist //at document.ready $("#bodyContent").on("click", "a.change_mode", function(e) { var new_action = $(this).attr("data-mode"); change_mode(new_action); }); $("#bodyContent").on("click", "a.change_team", function(e) { var new_team = $(this).attr("data-team"); change_team(new_team); }); $("#bodyContent").on("click", "a.change_author", function(e) { var new_author = $(this).attr("data-author"); change_author(new_author); });
//bind hide_admin $("#hide_admin").click(function(event) { event.preventDefault(); $(".admins_only").hide(); });
//The event handlers for data-form-fields used to be bound here. //But because some of them depend on user_info, I've moved them into their own function //which is called by get_user_info when it's done. //**This change was dumb. Revert it. But it's tangled, so that will take some doing.
refresh_form();
//Start the chain by getting user info get_user_info();
$("#team_list_dropdown").on("change", function(e) { var team_id_chosen = $(this).val(); change_team(parseInt(team_id_chosen)); });
if (form_info.owner_type == "team") {
//put teams in the team list dropdown
jQuery.ajax({
url: "https://igem.org/aj/team_list.cgi",
type: "GET",
timeout: 30000,
dataType: "json",
data: {command: "get_team_list", year: form_info.year},
error: function(jqxhr, textStatus, errorThrown) {
general_ajax_error( jqxhr, textStatus, errorThrown,
"Failed to get list of teams");
aj_log("Failed to get list of teams");
},
success: function(data, textStatus, jqxhr) {
put_teams_in_selector(data);
}
});
}
else {
$("#team_list_dropdown").hide();
}
//if it's a user-owned form and no author is specified, it defaults to self.
//but that happens later, in get_user_info success
//(because it can't default to self if you're not logged in)
}
function put_teams_in_selector(team_list) {
//Each element of team_list is an object like
//{ region: "Europe", name: "Aachen", id: "1585" }
var optgroups = { "Africa": "<optgroup label='Africa'>",
"Asia": "<optgroup label='Asia'>",
"Europe": "<optgroup label='Europe'>",
"Latin America": "<optgroup label='Latin America'>",
"North America": "<optgroup label='North America'>"
};
team_list.forEach(function(team) {
optgroups[team.region] += "<option value='" + team.id + "'>" + team.name + "</option>";
});
//I'm not using a for..in loop for this because order is important
$("#team_list_dropdown").append(optgroups["Africa"] + "</optgroup>");
$("#team_list_dropdown").append(optgroups["Asia"] + "</optgroup>");
$("#team_list_dropdown").append(optgroups["Europe"] + "</optgroup>");
$("#team_list_dropdown").append(optgroups["Latin America"] + "</optgroup>");
$("#team_list_dropdown").append(optgroups["North America"] + "</optgroup>");
}
function bind_DFF_event_handlers() { //bind change handler to all data-form-field elements //except admin fields and submit-family buttons //and validate-on-change fields $(non_admin_DFFs).not("[type='submit']").not(".validate-on-change").change(change_handler_for_most_DFFs); $(non_admin_DFFs).filter(".validate-on-change").change(DFF_change_handler_with_validation);
//bind handler to admin data-form-fields according to whether user is admin if(user_info.is_super) { $("[data-form-field^='admin']").change(admin_DFF_change_handler); } else { $("[data-form-field^='admin']").click(admin_lockout_click_handler); }
//bind click handlers for submit/delete/return buttons //These are bound regardless of permissions, BUT //one_input_changed will reject these changes if the user doesn't have submit permissions $("[data-form-field=submit][type='submit']").click(click_handler_for_submit); $("[data-form-field=delete_form][type='submit']").click(click_handler_for_delete); $("[data-form-field=return_form][type='submit']").click(click_handler_for_return); $("#popwin_unsubmit").on("click", click_handler_for_return);
$("#popwin_dismiss").on("click", function(event) { event.preventDefault(); $("#submitted_warning").hide(); });
//bind click handler for submit confirmation box $("#" + $("[data-form-field=submit]").attr("data-confirmation")).on("click", click_handler_for_confirm_submit);
//bind handler for custom new subform event //this event is right now triggered by code that's inline in the form page, //which also handles validation of new sub form fields //(eventually should be moved into here, though) $("[data-form-field=sub_form]").on("kforms:newsubform", newsf_handler); }
$(document).ready(document_ready_handler);
function change_handler_for_most_DFFs(event) { //console.log("change_handler_for_most_DFFs"); if ($(this).attr("data-form-field") == "sub_form") { console.log("change handler on sub_form"); var offon = ($(this).val() == "") ? "on" : "off"; $("[data-form-field^='sub_form_']").each(function(index, field) { dffswitch(field, offon); }); one_input_changed(this, 'store', , false, false); } else if ($(this).attr("data-form-field").startsWith("sub_form_")) { if ($("[data-form-field=sub_form]").val() == "") { return; } one_input_changed(this, 'store', , false, true); } else { one_input_changed(this, 'store', , false, true); } }
function DFF_change_handler_with_validation(event) { var validation_result = validate_one_field(this); if (validation_result == "") { one_input_changed(this, 'store', , false, true); } }
function admin_DFF_change_handler(event) { //console.log("admin_DFF_change_handler"); one_input_changed(this, 'store', , false, true); }
function admin_lockout_click_handler(event) { //console.log("admin_lockout_click_handler"); event.preventDefault(); alert(messages.user_cannot_edit_admin_fields); }
function click_handler_for_submit(event) { event.preventDefault();
//if there are any validation error messages, show them and don't submit var errors = validate_form_entries(); if(errors.length > 0) { alert(messages.form_validation_fail_beginning + "\n\n" + errors.join("\n")); return; }
//moved confirmation-box-polling to OIC so it happens AFTER permission checking //finally, actually submit the form one_input_changed(this, 'submit', , false, true); }
function click_handler_for_confirm_submit(event) { if (!permissions.submit) { alert(messages.user_cannot_submit); event.preventDefault(); } }
function click_handler_for_delete(event) {
//console.log("starting delete click handler"); event.preventDefault(); var confirmed = window.confirm(messages.confirm_deletion); if (confirmed) { one_input_changed(this, 'delete_form', , false, true); } }
function click_handler_for_return(event) { event.preventDefault(); one_input_changed(this, 'return_form', , false, true); }
function newsf_handler(event, newname) { console.log("newsf_handler, value " + newname); make_new_sub_form(newname, false, true); one_input_changed(this, 'store', , true, true); }
// ===================================================== // BLOCK // USER AND OWNER INFO // =====================================================
//Get information about the logged-in user //(nb: it looks at the login cookie, not at wgUserName) function get_user_info() { aj_log("Getting user info"); var request = jQuery.ajax({ url: form_info.ajax_URL, type: "POST", timeout: 30000, data: {command: 'get_user_info'}, dataType: "json", error: function(jqxhr, textStatus, errorThrown) { general_ajax_error( jqxhr, textStatus, errorThrown, "Failed to get information about the user"); }, success: function(data, textStatus, jqxhr) { aj_log("Received user info: " + jqxhr.responseText); if(try_parse_received_info(jqxhr.responseText)) { user_info = jqxhr.responseJSON; bind_DFF_event_handlers();
//Show hide admin stuff if (user_info.is_super == "true") { $(".admins_only").show(); } else { $(".admins_only").hide(); }
//now, if it's a user-owned form, we can handle some special cases at this point if (form_info.owner_type == "user") {
//If they're not logged in, //Yell at them and stop the setup chain. //**eventually I may want to change this, if I want to allow the public to view //any user-owned forms if (user_info.user_name == "") { aj_display_error(messages.not_logged_in); return; }
//Now we know they must be logged in, and we can default to self-author if no author specified. else if (typeof author == "undefined") { author = user_info.user_name; } }
//allow individual forms to have a custom extra response at this point if (typeof custom_get_user_info_response == "function") { custom_get_user_info_response(data, textStatus, jqxhr); } get_owner_info(); } else { aj_display_error(messages.unparseable_ajax_response); } } }); }
//Get information about the form owner
//This calls one of several specialized ajax getters, and then triggers the whole
//rest of form setup upon success of the request.
function get_owner_info(query_param) {
aj_log("Getting owner info");
owner_info = undefined;
//refresh_form();
var ajax_getter;
if (form_info.owner_type == "team") {ajax_getter = get_team_info;} else if (form_info.owner_type == "user") {ajax_getter = get_author_info;} else if (form_info.owner_type == "judge") {ajax_getter = get_judge_info;} else if (form_info.owner_type == "lab") {ajax_getter = get_lab_info;} else if (form_info.owner_type == "course") {ajax_getter = get_course_info;}
var request = ajax_getter(query_param); request.done( function (data, textStatus, jqxhr) { if(try_parse_received_info(jqxhr.responseText)) { if (data.error) { //we did the query wrong, oh no //this case will catch invalid team/lab/course IDs respond_to_error_in_owner_info(); } else { //special case: invalid usernames //will return empty strings for user_name and full_name if (form_info.owner_type == "user" && data.user_name == "") { respond_to_error_in_owner_info(); } else { owner_info = data; //put owner info into global object new_form_setup(); //this used to be set_up_form_and_mode } } if (typeof custom_get_owner_info_response == "function") { custom_get_owner_info_response(data, textStatus, jqxhr); } } else { aj_display_error(messages.unparseable_ajax_response); } }); }
//**These get_foo_info functions are kind of repetitive. Maybe rewrite later.
//Get information about a user of a specified username. //If none is specified, the function will look at global var author. function get_author_info(query_author) { query_author = (typeof query_author == "undefined") ? author : query_author return jQuery.ajax({ url: form_info.ajax_URL, type: "POST", timeout: 30000, dataType: "json", data: {command: 'get_user_info', username: query_author}, error: function(jqxhr, textStatus, errorThrown) { general_ajax_error( jqxhr, textStatus, errorThrown, "Failed to get information about author " + query_author); aj_log("Failed to get author info"); }, success: function(data, textStatus, jqxhr) { aj_log("Received author info: " + jqxhr.responseText); } }); }
function get_judge_info(query_judge_username) { query_judge_username = (typeof query_judge_username == "undefined") ? judge_username : query_judge_username; return jQuery.ajax({ url: form_info.ajax_URL, type: 'POST', timeout: 30000, dataType: "json", data: {command: "get_judge_info", judge_username: query_judge_username}, error: function(jqxhr, textStatus, errorThrown) { general_ajax_error(jqxhr, textStatus, errorThrown, "Failed to get judge information"); aj_log("Failed to get judge info"); }, success: function(data, textStatus, jqxhr) { aj_log("Received judge info: " + jqxhr.responseText); } }); }
// Get information about a team (and about the user w/r/t that team) function get_team_info(query_team_id) { query_team_id = (typeof query_team_id == "undefined") ? team_id : query_team_id; return jQuery.ajax({ url: form_info.ajax_URL, type: 'POST', timeout: 30000, dataType: "json", data: {command: 'get_info', form_name: form_info.name, team_id: parseInt(query_team_id)}, error: function(jqxhr, textStatus, errorThrown) { general_ajax_error(jqxhr, textStatus, errorThrown, "Failed to get team information"); aj_log("Failed to get team info"); }, success: function(data, textStatus, jqxhr) { aj_log("Received team info: " + jqxhr.responseText); } }); }
function get_lab_info(query_lab_id) { query_lab_id = (typeof query_lab_id == "undefined") ? lab_id : query_lab_id; return jQuery.ajax({ url: form_info.ajax_URL, type: 'POST', timeout: 30000, dataType: "json", data: {command: 'get_info', form_name: form_info.name, lab_id: parseInt(query_lab_id)}, error: function(jqxhr, textStatus, errorThrown) { general_ajax_error(jqxhr, textStatus, errorThrown, "Failed to get lab information"); aj_log("Failed to get lab info"); }, success: function(data, textStatus, jqxhr) { aj_log("Received lab info: " + jqxhr.responseText); } }); }
function get_course_info(query_course_id) { query_course_id = (typeof query_course_id == "undefined") ? course_id : query_course_id; return jQuery.ajax({ url: form_info.ajax_URL, type: 'POST', timeout: 30000, dataType: "json", data: {command: 'get_info', form_name: form_info.name, course_id: parseInt(query_course_id)}, error: function(jqxhr, textStatus, errorThrown) { general_ajax_error(jqxhr, textStatus, errorThrown, "Failed to get course information"); aj_log("Failed to get course info"); }, success: function(data, textStatus, jqxhr) { aj_log("Received course info: " + jqxhr.responseText); } }); }
function get_sub_form_list() { aj_log("Getting sub form list"); return jQuery.ajax({ url: form_info.ajax_URL, type: 'POST', timeout: 30000, dataType: "json", data: {command: 'get_sub_form_list', form_name: form_info.name, team_id: team_id}, //**THIS DATA DOES NOT WORK FOR ANY OWNER TYPE EXCEPT TEAMS. error: function(jqxhr, textStatus, errorThrown) { general_ajax_error(jqxhr, textStatus, errorThrown, "Failed to get subforms"); aj_log("Failed to get sub form list"); }, success: function(data, textStatus, jqxhr) { aj_log("Received sub_form list: " + jqxhr.responseText); } }); }
function respond_to_error_in_owner_info() { aj_display_error("Invalid " + form_info.owner_type + " selected"); }
//**Moe some of this down in to subform utility functions
function display_form() {
if ($("[data-form-field='sub_form']").length == 1) {
var sfreq = get_sub_form_list();
sfreq.done(function(data, textStatus, jqxhr){
data.sub_forms.forEach(function(sf) {
if (sf.sub_form == "") {
return;
}
make_new_sub_form(sf.sub_form, sf.submitted == "1", false);
});
});
initially_get_old_answers();
}
else {
initially_get_old_answers();
}
}
//**Okay so this needs MAJOR testing.
//The problem is that the interaction of form_submitted, form_displayed, display_form, and OIC
//may create an infinite loop. If we're gonna call this on initial setup AND every time someone hits
//the submit button... can we not call it twice? Problem is OIC calls this when someone hits the su
function new_form_setup() {
determine_permissions();
//special-case because it's common to poke around a bunch of teams, then go back to your team //and be confused that you can't edit if (mode == "view" && form_info.default_mode == "edit" && permissions["edit"]) { mode = "edit"; }
show_owner(); show_mode();
if (!permissions[mode]) { deny_permission(); }
//Here's where we handle the visibility of all the accessory crap that ISN'T a dff. if (form_info.owner_type == "team") { $("#team_select_inner").slideUp(); //this won't get called unless a team has been picked } if (form_submitted) { $("#submitted_warning").show(); } else { $("#submitted_warning").hide(); }
if (!form_displayed) {display_form();}
set_up_dffs_for_mode(); }
function set_up_dffs_for_mode(mymode) { mymode = (typeof mymode == "undefined") ? mode : mymode; if (!permissions[mode]) { mymode = "view"; }
//**Carefully review all these conditions against what they should be. Oh god. $("[data-form-field]").each(function(index, field) { field = $(field); var dff = field.attr("data-form-field"); if (dff == "sub_form") { //the subform select dffswitch(field, "on"); } else if (dff.startsWith("sub_form")) { //subform accessory fields dffswitch(field, ($("[data-form-field='sub_form']").val() == "" && mode != "view") ? "on" : "off"); } else if (dff.startsWith("admin")) { //admin dffs dffswitch(field, (mode == "admin" && user_info.is_super == "true") ? "on" : "off"); } else if (dff == "submit") { //submit button dffswitch(field, (mode != "view" && permissions.submit && !form_submitted) ? "on" : "off"); } else if (dff == "return_form") { //unsubmit button dffswitch(field, (mode != "view" && permissions.edit && form_submitted) ? "on" : "off"); } else if (dff == "delete_form") { //delete button dffswitch(field, (mode != "view" && permissions.edit) ? "on" : "off"); } else { //Regular DFF //This will ALSO check for an empty sub_form (value ""). //Note that a NONEXISTENT sub_form would have val() undefined. dffswitch(field, (mode != "view" && !form_submitted && $("[data-form-field=sub_form]").val() != "") ? "on" : "off"); } }); }
// ===================================================== // BLOCK // OWNER/MODE SHOWING/CHANGING // =====================================================
//modes! //All this does is reconcile the view/edit/admin mode indicator at the top of the page function show_mode() { //console.log("show_mode"); var m = $("#modes"); if (mode == "view") { m.html(jQuery.parseHTML("Mode: View <a class='change_mode' data-mode='edit'>(click for edit mode)</a>")); } else if (mode == "edit") { m.html(jQuery.parseHTML("Mode: Edit <a class='change_mode' data-mode='view'>(click for view mode)</a>")); } else if (mode == "admin") { m.html(jQuery.parseHTML("Mode: Admin <a class='change_mode' data-mode='view'>(view)</a> <a class='change_mode' data-mode='edit'>(edit)</a>")); }
//if the user can't edit, color edit links gray if (!permissions.edit) { $("[data-mode='edit']").css("color", "gray"); } }
function show_owner() { var owner_name; var display_text = "[none]";
//case team if (form_info.owner_type == "team") { $("#prompt_to_choose_team").hide(); owner_name = owner_info.team_name; var this_teams_profile_href = "https://igem.org/Team.cgi?id=" + team_id; display_html = "for Team <a target='_blank' href='" + this_teams_profile_href + "'>" + owner_name + "</a>"; } //case author else if (form_info.owner_type == "user") { owner_name = owner_info.full_name; display_html = "for user " + owner_name; } //case lab, course, judge else { //owner_name = display_html = "oops, this form is owned by a lab, course, or judge"; throw new FormError("Can't show_owner for lab/course/judge: not implemented yet"); }
$("#owner_name_display").html(display_html).show(); }
function prompt_to_choose_team() { //console.log("prompt_to_choose_team"); $("#owner_name_display").hide(); $("#prompt_to_choose_team").show(); $("#team_select_inner").show(); }
function change_mode(new_mode) { if (form_info.owner_type == "team" && typeof owner_info == "undefined") { alert("Please choose a team first!"); //**unify? or make team-choosing better return; } //console.log("change_mode"); refresh_form(false); mode = new_mode; check_mode(); //set_up_form_and_mode(); new_form_setup(); }
function change_team(new_team_id) { //console.log("change_team"); refresh_form(); team_id = new_team_id; get_owner_info(); }
//Change to a different user's form function change_author(new_author) { refresh_form(); author = new_author; get_owner_info(); }
//Checks the mode and sets to default_mode if it's invalid or unspecified function check_mode() { if (valid_modes.indexOf(mode) == -1) { mode = form_info.default_mode; } }
//This function goes into the team_select_container and replaces all the //links with a link to the current page / query string with the selected team_id //and the current mode. function replace_links_in_team_selector() { $("#team_select_inner").find("a").each(function() { var the_link = $(this); var existing_href = the_link.attr("href"); var team_id_from_this_link = existing_href.split("?id=")[1]; the_link.removeAttr("href").addClass("change_team").attr("data-team", team_id_from_this_link); }); }
// ===================================================== // BLOCK // SENDING AND RECEIVING FORM ANSWERS // =====================================================
function one_input_changed( input_element, command, effective_date, send_sub_form_accessories, check_for_silent_logout ) { send_sub_form_accessories = (typeof send_sub_form_accessories == "undefined") ? false : send_sub_form_accessories; check_for_silent_logout = (typeof check_for_silent_logout == "undefined") ? true : check_for_silent_logout; var input_element = $( input_element );
//permissions if (command == "submit" || command == "delete_form") { if (!permissions["submit"] && !permissions["admin"]) { alert(messages.user_cannot_submit); if (typeof custom_refuse_submission == "function") { custom_refuse_submission(input_element, command, effective_date); } return; } }
if (command == "submit") { //check if it has a confirmation box and if it's checked //if not, refuse submission //(currently unable to handle multiple confirmation boxes) var confirmation_box_id = input_element.attr("data-confirmation"); if(typeof confirmation_box_id != "undefined") { var confirmed = $("#" + confirmation_box_id).prop("checked"); if(!confirmed) { submission_not_confirmed(); return; } } }
//warning on unsubmitting a signed form if (command == "return_form") { if (permissions.edit && !permissions.submit) { //**can't unify this msg yet var confirm_string = "Are you sure you want to unsubmit this form?\n\n"; confirm_string += "You will need a " + form_info.permissions.submit.join(" / ") + " to submit the form again." if (!confirm(confirm_string)) { return; } } }
feedback(input_element, "sending");
if (owner_info == undefined) { aj_log( "Error: no form owner specified"); return; }
//Uncheck all confirmation boxes, //except on submit, return, and the initial "store" request when input_element is if (command != "submit" && command != "return_form" && input_element.length != 0) { uncheck_all_confirmations(); }
// Step 1: Format information about the element that changed and send it to the server // If a sub_form element exists, get its value var sub_form = $("[data-form-field=sub_form]").val() || ; var eff_date = ( typeof effective_date !== 'undefined' ) ? effective_date : '2004-01-01'; if (send_sub_form_accessories) { //in this condition we want to package all the values of the subform accessory fields into a single entry_list var entry_list = $("[data-form-field^=sub_form_]").get().reduce(function(previous, current, index, array) { var tmp = get_one_entry($(current)); if (typeof tmp == "undefined") { return previous; } else { return previous.concat(tmp); } }, []); } else { var entry_list = get_one_entry( input_element ); } var json_entries = JSON.stringify( entry_list ); aj_log( "OIC: " + command + ' '+ json_entries );
// When the sub_form changes, we clear all the inputs and they are reloaded by ajax if ( input_element.attr('data-form-field') == 'sub_form' ) { clear_form_but_keep_sub_form(); } // When the sub-form is deleted, we clear all the inputs and they are reloaded by ajax if ( input_element.attr('data-form-field') == 'delete_form') { //and delete the corresponding subform, unless it's the empty one $("[data-form-field=sub_form]").find(":selected").not("[value=]").remove(); clear_form(); }
var data = {command: command, form_name: form_info.name, sub_form: sub_form, eff_date: eff_date, entry_list: json_entries}; if (form_info.owner_type == "team") { data.team_id = team_id; } else if (form_info.owner_type == "user") { data.author_username = author; } else { throw new FormError("OIC can't deal with labs, courses, or judges"); }
var stor_req = jQuery.ajax({url: form_info.ajax_URL, type: 'POST', timeout: 30000, data: data, dataType: "json", error: function(jqxhr, textStatus, errorThrown) { feedback(input_element, "invalid"); general_ajax_error(jqxhr, textStatus, errorThrown, "Failed to save entry " + input_element.attr("name")); aj_log("Failed one_input_changed on entry " + input_element.attr("name")); }, success: function(data, textStatus, jqxhr) { aj_log( "Received: " +jqxhr.responseText); if (check_for_silent_logout && data.return_error == "Not logged in") { respond_to_silent_logout(input_element); return; } form_submitted = parseInt(data.submitted) > 0; //console.log("Done with OIC! Command was " + command); if (command == "submit" || command == "return_form") { //console.log(typeof data.submitted); //show_hide_submitted_status( data.submitted ); new_form_setup(); if (command == "submit") { mark_current_sub_form_as_submitted(); } else { mark_current_sub_form_as_unsubmitted(); } } if (input_element.get(0) == $("[data-form-field=sub_form]").get(0)) { //set_up_dffs_for_mode(); new_form_setup(); } process_received_data( jqxhr.responseText );
// Removes any warnings from other elements of the same name (radio buttons) unwarn($("[name='" + input_element.attr("name") + "']").not(input_element)); feedback(input_element, "sent"); aj_clear_error(); if(typeof custom_one_input_changed_response == "function") { //Allow individual forms to have a custom response custom_one_input_changed_response(input_element, command, effective_date, data, textStatus, jqxhr); } } });
stor_req.done(function(data, textStatus, jqxhr) { jQuery.noop(); }); }
// Step 1: Format up one entry function get_one_entry(input_element) { //console.log("get_one_entry"); var name = input_element.attr("data-form-field"); var answer = input_element.val();
//if the answer is an array, as it is from a select multiple, //then preemptively JSON.stringify it if (answer instanceof Array) { // console.log("Found an Array answer, converting to string"); answer = answer.toString(); // console.log(answer); }
if (input_element.attr('type') == 'checkbox') { answer = input_element.prop('checked'); } else if (input_element.attr('type') == 'radio') { if (!input_element.prop('checked')) { return; } answer = input_element.prop('value'); } else if (input_element.attr('type') == 'submit') { answer = ; }
return [ {'field_name': name, 'answer':answer } ]; }
// Step: 2 Process the received data for all the inputs // The received data looks like { ... , : [ { input_name: 'name', answers: 'answer'}, .... ] function process_received_data( data ) { //console.log("process_received_data"); try { data = JSON.parse( data ); //**Rewrite this to use try_parse_received_data } catch (err) { aj_log( 'JSON.parse error: ' + err ); return; } aj_display_error(data.error);
var entry_list = data.entry_list || []; for (var i = 0; i < entry_list.length; i++) { process_one_received_entry( entry_list[i] ); } // if (!form_displayed) { // new_form_setup(); // } form_displayed = true; return; }
//**Make this not fill in admin fields if you're not an admin! Or only some of them. Or something. function process_one_received_entry(entry) { var field_name = entry["field_name"] || ; var answer = entry["answer"] || ; var input_element = $("[data-form-field='"+ field_name +"']"); var input_type = input_element.attr('type'); if (input_type == 'submit') { return; }
//Special case for select-multiples if (input_element.is('select') && input_element.prop("multiple")) { // console.log("Hey, this element is a select-multiple, I'm gonna split the answer"); // console.log("The old answer is " + answer); answer = answer.split(","); // console.log("The new answer is " + answer); } aj_log("Processing " + field_name + " = '" + answer + "' (Found: " +input_element.length+ " element Type: "+input_type+")");
// Radio buttons are identified by form-field and value if (input_type == 'radio') { input_element = input_element.filter("[value='"+answer+"']"); input_element.prop('checked', true); } else if (input_type == "checkbox") { if (answer == 'false' || answer == '0') { answer = false; } input_element.prop("checked", answer ); } else { input_element.val(answer); } }
//Helper function for initially loading in the answers from a new form function initially_get_old_answers() { one_input_changed(, 'store', , false, false); //Store nothing, process all returned answers feedback($("[data-form-field]"), "none"); //Clear green/yellow feedback divs //form_displayed = true; //moved to OIC.done }
function respond_to_silent_logout(input_element) { input_element = (input_element instanceof jQuery) ? input_element : $(input_element); var msg = "Your login session has expired.\n Answer to question \"" + input_element.attr("name") + "\" was not saved.\n Please log out and then log in to continue."; warn(input_element, "Answer not saved. Please log out and log back in."); alert(msg); aj_display_error(msg); }
//
// ===========================================================================================================================================
// DETERMINE PERMISSIONS
//ana
//Check the view/edit/submit/admin permissions of the current user on the current form. //Puts them in global variable permissions. function determine_permissions() {
var perm_group; $.each(form_info.permissions, function(perm_name, groups_having_this_perm) { console.log("Permission to " + perm_name); perm_group = groups_having_this_perm[index];
for (index in groups_having_this_perm) { switch(perm_group){
case "public": { return true; break; }
case "any_users": { permissions.view = true; return user_info.user_name != ""; break; }
case "super_users": { permissions.admin = true; permissions.submit = true; permissions.edit = true; permissions.view = true;
return user_info.is_super == "true"; break; }
case "group_members": {
permissions.edit = true; permissions.view = true; return (typeof owner_info.role == "string" && owner_info.role != "None"); break; }
case "author": {
permissions.submit = true; permissions.edit = true; permissions.view = true; return user_info.user_name.toLowerCase() == author.toLowerCase(); break; } }}
});
return permissions; }
//Determine whether the current user falls into a given permission group on the current form. //groups are ["individualOwner", "group_members", "super_users", "any_users", "public"] function user_is_in_permission_group(perm_group) {
if (perm_group == "public") { return true; }
else if (perm_group == "any_users") {
permissions.view = true; return user_info.user_name != ""; }
else if (perm_group == "super_users") {
permissions.admin = true; permissions.submit = true; permissions.edit = true; permissions.view = true;
return user_info.is_super == "true";
}
else if (perm_group == "group_members") {
permissions.edit = true; permissions.view = true; return (typeof owner_info.role == "string" && owner_info.role != "None"); }
else if (perm_group == "author") {
permissions.submit = true; permissions.edit = true; permissions.view = true; return user_info.user_name.toLowerCase() == author.toLowerCase(); }
else if (valid_roles.indexOf(perm_group) > -1) {
return owner_info.role == perm_group;
}
}
//when the user doesn't have permission to view/ edit the form, display the appropiate message
function deny_permission() {
// alert message to give to user var alert_string = "Sorry, you don't have permission to " + mode + " that form.";
//because I got rid of the no kick checkbox nokick = false;
//if the user is not a super user, hide admin view if (!user_info.is_super) { $(".admins_only").hide(); }
//**figure out how to unify this string because it depends on current mode if (permissions.view) { if (nokick) { alert_string += "\n" + messages.not_kicking_to_view_mode; } else { alert_string += "\n" + messages.kicking_to_view_mode; } }
if (nokick) { aj_display_error(alert_string); } else { alert(alert_string); change_mode("view"); } }
//
// ===========================================================================================================================================
//
// =====================================================
// BLOCK
// AJAX HELPERS
// =====================================================
//General helper for ajax errors. //In every case, it will display an alert and an aj_display_error. //Not unifying these because form writers shouldn't change them. function general_ajax_error(jqxhr, textStatus, errorThrown, custom_msg) { //case: timeout error if (textStatus == "timeout") { alert("Timeout error"); aj_display_error("Timeout error: there could be a problem with your internet connection, or the server could be under a heavy load. Please try again later."); } //case: internet probably unplugged or something else if (jqxhr.status == 0) { alert("Unable to connect to server. Please check your Internet connection and try again."); aj_display_error("Unable to connect to server. Please check your Internet connection and try again."); } //case: 4xx/5xx error else if (400 <= jqxhr.status <= 599) { alert("Server error [" + jqxhr.status + " " + jqxhr.statusText + "]"); aj_display_error("Server error [" + jqxhr.status + " " + jqxhr.statusText + "]. Please report this error to iGEM HQ. In your message, please include the URL of this page, and the date/time of the error."); }
//other error else { alert("Error in ajax\n[" + textStatus + "]"); custom_msg = (typeof custom_msg == "undefined") ? "general AJAX error" : custom_msg; aj_display_error(custom_msg); } }
//All our ajax requests will receive both a responseJSON and a responseText. //Here, we attempt parsing the responseText. If there is an error in between //"ajax request fail" and "you gave wrong parameters to form.cgi", the responseText //will not be parseable as JSON. function try_parse_received_info(responseText) { try { JSON.parse(responseText); return true; } catch (err) { aj_log("JSON.parse error: " + err); aj_display_error(unparseable_ajax_response); return false; } }
// =====================================================
// BLOCK
// SUBFORM STUFF
// =====================================================
//Accepts a string and makes a new choice in the subform dropdown. //Boolean gotoit (default false) causes that choice to then be selected immediately. function make_new_sub_form(name, submitted, gotoit) { gotoit = (typeof gotoit == "undefined") ? false : gotoit;
var option = $("<option>").text(name).attr("value", name); option.appendTo( submitted ? $("#sub_forms_submitted") : $("#sub_forms_unsubmitted") ); if (gotoit) { option.prop("selected", true);//.trigger("change"); } }
//Function and wrappers for moving subforms around.
// - option should be a jQuery object containing an option, or a string equaling the value of an option
// - move_to should be "submitted" or "unsubmitted"
function move_sub_form(option, move_to) {
if (typeof option == "string") {
option = $("[data-form-field=sub_form]").find("[value='" + option + "']");
}
option.detach().appendTo($("#sub_forms_" + move_to));
}
function mark_current_sub_form_as_submitted() {
move_sub_form($("[data-form-field=sub_form]").find(":selected"), "submitted");
}
function mark_current_sub_form_as_unsubmitted() {
move_sub_form($("[data-form-field=sub_form]").find(":selected"), "unsubmitted");
}
function empty_sub_form_list() { $("[data-form-field=sub_form]").children("optgroup").empty(); }
// =====================================================
// BLOCK
// MISC UTILITY FUNCTIONS
// =====================================================
//Fake "assert" function for assertion testing. //It is best practice to strip out assertions before code goes to real users. function assert(condition, message) { if (!condition) { message = message || "Assertion failed"; if (typeof FormError !== "undefined") { throw new FormError(message); } throw new Error(message); } }
function clear_form_but_keep_sub_form() { $('[data-form-field][type!=checkbox][type!=radio][type!=submit][data-form-field!=sub_form]').val(); $('[data-form-field][type=checkbox]').prop('checked', false); $("[data-form-field][type=radio]").prop('checked', false); feedback($("[data-form-field]"), "none"); }
function clear_form() { $("[data-form-field][type!=checkbox][type!=radio][type!=submit][data-form-field!='sub_form']").val(); $('[data-form-field][type=checkbox]').prop('checked', false); $("[data-form-field][type=radio]").prop('checked', false); $("[data-form-field='sub_form']").val(""); feedback($("[data-form-field]"), "none"); }
//Upon first page load, or upon changing owner, we must clear the form to keep the browser from //autofilling fields, and to keep the last owner's answers from leaving droppings in the new form //also we readonly all DFFs -- they will be made un-readonly upon successful entry into //Edit or Admin mode function refresh_form(also_clear_answers) { also_clear_answers = (typeof also_clear_answers == "undefined") ? true : false; if (also_clear_answers) { empty_sub_form_list(); clear_form(); form_displayed = false; } readonly_all_DFFs(); aj_clear_error(); }
//function simply unchecks all confirmation boxes (radio or checkbox) with
//class="confirmation"
function uncheck_all_confirmations() {
$(".confirmation").prop("checked", false);
}
function submission_not_confirmed() { if (typeof custom_submission_not_confirmed == "function") { custom_submission_not_confirmed(); } else { alert(messages.please_confirm_submission); } }
//gets all sets of radio buttons that have a data-form-field attribute //returns them as an array of "name"s function get_all_sets_of_radios() { var names_list = [];
$("[data-form-field][type='radio']").each(function(index) { //get the name var this_radio_name = $(this).attr("name");
//check if the name is already in the list. if not, add it if(names_list.indexOf(this_radio_name) == -1) { names_list.push(this_radio_name); } //else skip it }); return names_list; }
// ===================================================== // BLOCK // READONLY/DISABLE FUNCTIONS // =====================================================
//Simply disables everything with a data-form-field function disable_all_DFFs() { $('[data-form-field]').prop('disabled', true); }
//Function makes all DFF elements either readonly (for text boxes) or disabled (everything else). //It does not change any of their event bindings. function readonly_all_DFFs() { $('[data-form-field]').each(function() { var field = $(this); if (field.is("textarea") || field.is("input[type='text']")) { field.prop('readonly', true); } else { field.prop('disabled', true); } }); }
//undisable all DFFs except admin ones and static ones function undisable_all_DFFs() { $(non_admin_DFFs).not(".static").prop('disabled', false).prop('readonly', false); }
//undisable admin DFFs, except static ones function undisable_admin_DFFs() { $('[data-form-field^="admin"]').not(".static").prop("disabled", false).prop("readonly", false); }
//switch one field to "off" or "on" (tested, works) function dffswitch(field, offon) { field = (field instanceof jQuery) ? field : $(field); if (offon == "off") { if (field.is("textarea") || field.is("input[type='text']")) { field.prop('readonly', true); } else { field.prop('disabled', true); } } else if (offon == "on") { field.prop("disabled", false).attr("readonly", false); } else { throw new FormError("I can't switch field " + field.attr("data-form-field") + " to state " + offon); } }
// =====================================================
// BLOCK
// LOG ENTRIES AND ERROR MESSAGES
// =====================================================
// If you wish to Display a Log of Transactions add a div with id= 'aj_form_log' function aj_display_log_div() { var div = $('#aj_form_log'); div.addClass("admins_only");
div.html("+ "<\/DIV>" ); jq_versions(); aj_display_log_buttons(); }
function aj_display_log_buttons() { var div = $('#aj_form_log_inner_div'); div.before("<button type='button' id='clear_log'>Clear Log<\/button>"); div.before("<button type='button' id='toggle_log'>Toggle Log<\/button>"); $("#clear_log").on("click", aj_display_log_div); $("#toggle_log").on("click", function(event) { $("#aj_form_log_inner_div").toggle(); }); }
function aj_log(text) { var div = $('#aj_form_log_inner_div'); if (div.length == 0) { return; } var old_text = div.html();
div.html(old_text + "" + text + "<\/P>"); } // Put the jQuery versions in the log box function jq_versions() { var jq_v='None'; try { jq_v = jQuery.fn.jquery || ; } catch (err) {} var ui_v = 'None'; try { ui_v= jQuery.ui ? jQuery.ui.version || 'pre 1.6' : ; } catch (err) {} aj_log( 'AJ Form Log: [ jQuery v '+ jq_v + ' jQuery UI v ' + ui_v + ' ]' ); } function aj_display_error(error_text){ if (!error_text) { return; } var div = $('#aj_form_errors'); div.html("
Software error:
" + error_text + "div.show(); }
function aj_clear_error(){
//aj_display_error_text("");
$("#aj_form_errors").text("").hide();
}
// =====================================================
// BLOCK Hey I know you don't like this but don't get rid of it
// SHIELDING It could be useful for sign/lock/submit!
// =====================================================
//All these shield-manipulating functions expect a jQuery object as an argument. //But if none is given, they default to the single shield on a form, which is assumed //to have id="shield". //Shield divs should have class "shield-active" or "shield-inactive" as defined in //Template:CSS/SafetyForms function toggle_shield(s) { s = (typeof s == "undefined") ? $("#shield") : s; if(s.hasClass("shield-inactive")) { add_shield(s); } else { remove_shield(s); } }
function add_shield(s) { s = (typeof s == "undefined") ? $("#shield") : s; s.removeClass("shield-inactive"); s.addClass("shield-active"); }
function remove_shield(s) { s = (typeof s == "undefined") ? $("#shield") : s; s.removeClass("shield-active"); s.addClass("shield-inactive"); }
// ===================================================== // BLOCK // VALIDATION // =====================================================
function validate_form_entries() { var fields_to_validate; var results = [];
$(non_admin_DFFs).each(function(index, field){ field = $(field); var this_result = ""; var what_the_field_says = field.attr("data-validation"); if (what_the_field_says == "required") { this_result = validate_one_field(field); } else if (what_the_field_says == "optional") { return; } else if (typeof what_the_field_says == "undefined") { if (form_info.validate_unspecified_fields == "required") { this_result = validate_one_field(field); } }
//If we reach this case, data-validation is set to something custom //It should be a selector (NOT JUST AN ID) for a radio or checkbox input elsewhere on the page. //We will require field to be nonempty if the indicated radio/checkbox is checked. else { //console.log("Custom validating field " + field.attr("data-form-field") + " with respect to " + what_the_field_says); var other_field = $(what_the_field_says); if (other_field.length == 0) { throw new FormError("Can't validate field " + field.attr("data-form-field") + " with respect to selector '" + what_the_field_says + "'; that field does not exist"); } else if (other_field.attr('type') != "radio" && other_field.attr('type') != "checkbox") { throw new FormError("Can't validate field " + field.attr("data-form-field") + " with respect to selector '" + what_the_field_says + "'; that field is not a radio or checkbox"); } else { if (other_field.prop("checked")) { this_result = validate_one_field(field); } } }
//Only add nonempty, nonduplicate strings if (this_result != "" && results.indexOf(this_result) == -1) { results.push(this_result); } });
return results; }
function validate_one_field(fld) { fld = (fld instanceof jQuery) ? fld : $(fld); var type = fld.attr("type"); var name = fld.attr("name"); var result = ""; var isValid;
if (fld.attr("data-validate-as") == "email") { return validate_email(fld); }
if (type == "radio" || type == "checkbox") { isValid = $("[name='"+name+"']:checked").length > 0; } else { isValid = (fld.val() != "" && fld.val() != null && typeof fld.val() != "undefined"); }
if (!isValid) { result = messages.field_validation_fail_beginning + " " + name; feedback(fld, "invalid"); } return result }
var email_regex = /^.+@.+\..+/;
function validate_email(fld) {
var result;
if (email_regex.test(fld.val())) {
result = "";
unwarn(fld);
}
else {
result = messages.invalid_email_address;
feedback(fld, "invalid");
warn(fld, messages.invalid_email_address);
}
return result;
}
// ===================================================== // YELLOW-AND-GREEN FEEDBACK // =====================================================
var wrapper_none_color = ""; var wrapper_sending_color = "#FFFF99"; var wrapper_sent4 = "#B3FF66"; var wrapper_sent3 = "#C6FF8C"; var wrapper_sent2 = "#D9FFB3"; var wrapper_sent1 = "#ECFFD9"; var wrapper_invalid_color = "pink"; var wrapper_readonly_color = "#EEEEEE";
function feedback(element, command) { element = (element instanceof jQuery) ? element : $(element);
var wrapper = element.parent(".wrapper");
if(command == "none") { wrapper.css("background-color", wrapper_none_color); } else if(command == "sending") { wrapper.children(".dff-warning").remove(); wrapper.css("background-color", wrapper_sending_color); } else if(command == "sent") { wrapper.children(".dff-warning").remove(); fade_sent(wrapper); } else if(command == "invalid") { wrapper.css("background-color", wrapper_invalid_color); } else if(command == "readonly") { wrapper.css("background-color", wrapper_readonly_color); } else { throw new FormError("Invalid command " + command + " for feedback on element #" + element.attr("id")); } }
//Fades a wrapper from fresh green thru pale green back to white. //**We should really be using the jQuery Color() plugin, or jQuery UI, for this. //(because native jQuery cannot animate a non-numeric CSS property like background-color) //ORRRRR just fade the OPACITY function fade_sent(wrapper) { var interval = 100; wrapper.css("background-color", wrapper_sent4); setTimeout(function() { wrapper.css("background-color", wrapper_sent3); setTimeout(function() { wrapper.css("background-color", wrapper_sent2); setTimeout(function() { wrapper.css("background-color", wrapper_sent1); setTimeout(function() { wrapper.css("background-color", wrapper_none_color); }, interval); }, interval); }, interval); }, 3 * interval); }
function warn(element, warning_text) { if (!warning_text || typeof warning_text != "string") { unwarn(element); } element = (element instanceof jQuery) ? element : $(element); var wrapper = element.parent(".wrapper"); if (wrapper.length == 0) { return; }
//First, remove any warnings that exist already wrapper.children(".dff-warning").remove();
if (wrapper.is("span") && element.attr("type") != "text") { //everything except textarea, select, and input type="text" wrapper.prepend($("" + warning_text + "")); } else if (element.attr("type") == "text") { wrapper.append($("" + warning_text + "")); } else { //textarea, select
wrapper.prepend($("<p class='dff-warning'>" + warning_text + ""));}
feedback(element, "invalid"); }
function unwarn(element) { element = (element instanceof jQuery) ? element : $(element); var wrapper = element.parent(".wrapper"); if (wrapper.length == 0) { return; } wrapper.children(".dff-warning").remove(); feedback(element, "none");
}