Files
edx-platform/lms/static/js/student_account/views/FormView.js
Ned Batchelder 761306114d New LOGIN_ISSUE_SUPPORT_LINK setting
The login page had a hard-coded support.edx.org URL.  This changes it to
a configurable setting. BTR-32.
2020-12-04 14:55:27 -05:00

373 lines
13 KiB
JavaScript

(function(define) {
'use strict';
define([
'jquery',
'underscore',
'backbone',
'common/js/utils/edx.utils.validate',
'edx-ui-toolkit/js/utils/html-utils',
'edx-ui-toolkit/js/utils/string-utils',
'text!templates/student_account/form_errors.underscore'
], function($, _, Backbone, EdxUtilsValidate, HtmlUtils, StringUtils, formErrorsTpl) {
return Backbone.View.extend({
tagName: 'form',
el: '',
tpl: '',
fieldTpl: '#form_field-tpl',
formErrorsTpl: formErrorsTpl,
formErrorsJsHook: 'js-form-errors',
defaultFormErrorsTitle: gettext('An error occurred.'),
events: {},
errors: [],
formType: '',
$form: {},
fields: [],
liveValidationFields: [],
// String to append to required label fields
requiredStr: '',
/*
Translators: This string is appended to optional field labels on the student login, registration, and
profile forms.
*/
optionalStr: gettext('(optional)'),
submitButton: '',
initialize: function(data) {
this.model = data.model;
this.preRender(data);
this.tpl = $(this.tpl).html();
this.fieldTpl = $(this.fieldTpl).html();
this.buildForm(data.fields);
this.listenTo(this.model, 'error', this.saveError);
},
/* Allows extended views to add custom
* init steps without needing to repeat
* default init steps
*/
preRender: function(data) {
/* Custom code goes here */
return data;
},
render: function(html) {
var fields = html || '';
HtmlUtils.setHtml(
$(this.el),
HtmlUtils.HTML(
_.template(this.tpl)({
fields: fields,
HtmlUtils: HtmlUtils
})
)
)
this.postRender();
return this;
},
postRender: function() {
var $container = $(this.el);
this.$form = $container.find('form');
this.$formFeedback = $container.find('.js-form-feedback');
this.$submitButton = $container.find(this.submitButton);
},
buildForm: function(data) {
var html = [],
i,
len = data.length,
fieldTpl = this.fieldTpl;
this.fields = data;
for (i = 0; i < len; i++) {
if (data[i].errorMessages) {
data[i].errorMessages = this.escapeStrings(data[i].errorMessages);
}
html.push(HtmlUtils.template(fieldTpl)($.extend(data[i], {
form: this.formType,
requiredStr: this.requiredStr,
optionalStr: this.optionalStr,
supplementalText: data[i].supplementalText || '',
supplementalLink: data[i].supplementalLink || '',
loginIssueSupportLink: data[i].loginIssueSupportLink || ''
})));
}
this.render(html.join(''));
},
/* Helper method to toggle display
* including accessibility considerations
*/
element: {
hide: function($el) {
if ($el) {
$el.addClass('hidden');
}
},
scrollTop: function($el) {
// Scroll to top of selected element
$('html,body').animate({
scrollTop: $el.offset().top
}, 'slow');
},
show: function($el) {
if ($el) {
$el.removeClass('hidden');
}
}
},
escapeStrings: function(obj) {
_.each(obj, function(val, key) {
obj[key] = _.escape(val);
});
return obj;
},
forgotPassword: function(event) {
event.preventDefault();
this.trigger('password-help');
},
getFormData: function() {
var obj = {},
$form = this.$form,
elements = $form[0].elements,
i,
$n,
tpl,
len = elements.length,
$el,
$label,
key = '',
errors = [],
validation = {},
$desc,
$validationNode;
for (i = 0; i < len; i++) {
$el = $(elements[i]);
$label = $form.find('label[for=' + $el.attr('id') + ']');
key = $el.attr('name') || false;
// Due to a bug in firefox, whitespaces in email type field are not removed.
// TODO: Remove this code once firefox bug is resolved.
if (key === 'email') {
$el.val($el.val().trim());
}
if (key) {
if (this.interesting_fields($el)) {
this.remove_validation_error($el, $form);
}
validation = this.validate(elements[i]);
if (validation.isValid) {
obj[key] = $el.attr('type') === 'checkbox' ? $el.is(':checked') : $el.val();
$el.removeClass('error');
$label.removeClass('error');
} else {
if (this.interesting_fields($el)) {
$validationNode = this.get_error_validation_node($el, $form);
if ($validationNode) {
$n = $.parseHTML(validation.message);
tpl = HtmlUtils.template('<i class="fa fa-exclamation-triangle"></i>');
HtmlUtils.prepend($n, tpl());
HtmlUtils.append($validationNode, HtmlUtils.HTML($n));
}
$desc = $form.find('#' + $el.attr('id') + '-desc');
$desc.remove();
}
errors.push(validation.message);
$el.addClass('error');
$label.addClass('error');
}
}
}
this.errors = _.uniq(errors);
return obj;
},
remove_validation_error: function($el, $form) {
var $validationNode = this.get_error_validation_node($el, $form);
if ($validationNode && $validationNode.find('li').length > 0) {
$validationNode.empty();
}
},
get_error_validation_node: function($el, $form) {
var $node = $form.find('#' + $el.attr('id') + '-validation-error-msg');
return $node.find('ul');
},
interesting_fields: function($el) {
return ($el.attr('name') === 'email' || $el.attr('name') === 'password');
},
toggleHelp: function(event, $help) {
var $el = $(event.currentTarget);
var $i = $el.find('i');
if ($help.css('display') === 'block') {
$help.css('display', 'none');
$i.addClass('fa-caret-right').removeClass('fa-caret-down');
} else {
$help.css('display', 'block');
$i.addClass('fa-caret-down').removeClass('fa-caret-right');
}
},
saveError: function(error) {
this.errors = [
StringUtils.interpolate(
'<li>{error}</li>', {
error: error.responseText
}
)
];
this.renderErrors(this.defaultFormErrorsTitle, this.errors);
this.scrollToFormFeedback();
this.toggleDisableButton(false);
},
/* Wrapper for renderFormFeedback provided for convenience since the majority of
* our calls to renderFormFeedback are for rendering error messages.
*/
renderErrors: function(title, errorMessages) {
this.clearFormErrors();
this.renderFormFeedback(this.formErrorsTpl, {
jsHook: this.formErrorsJsHook,
title: title,
messagesHtml: HtmlUtils.HTML(errorMessages.join(''))
});
},
renderFormFeedback: function(template, context) {
var tpl = HtmlUtils.template(template);
HtmlUtils.prepend(this.$formFeedback, tpl(context));
},
/* Allows extended views to add non-form attributes
* to the data before saving it to model
*/
setExtraData: function(data) {
return data;
},
submitForm: function(event) {
var data = this.getFormData();
if (!_.isUndefined(event)) {
event.preventDefault();
}
this.toggleDisableButton(true);
if (!_.compact(this.errors).length) {
data = this.setExtraData(data);
this.model.set(data);
this.model.save();
this.clearFormErrors();
} else {
this.renderErrors(this.defaultFormErrorsTitle, this.errors);
this.scrollToFormFeedback();
this.toggleDisableButton(false);
}
this.postFormSubmission();
},
/* Allows extended views to add custom
* code after form submission
*/
postFormSubmission: function() {
return true;
},
resetValidationVariables: function() {
return true;
},
clearFormErrors: function() {
var query = '.' + this.formErrorsJsHook;
this.clearFormFeedbackItems(query);
},
clearFormFeedbackItems: function(query) {
var $items = this.$formFeedback.find(query);
if ($items.length > 0) {
$items.remove();
}
},
/**
* If a form button is defined for this form, this will disable the button on
* submit, and re-enable the button if an error occurs.
*
* Args:
* disabled (boolean): If set to TRUE, disable the button.
*
*/
toggleDisableButton: function(disabled) {
if (this.$submitButton) {
this.$submitButton.attr('disabled', disabled);
}
},
scrollToFormFeedback: function() {
var self = this;
// Scroll to feedback container
$('html,body').animate({
scrollTop: this.$formFeedback.offset().top
}, 'slow', function() {
self.resetValidationVariables();
});
// Focus on the feedback container to ensure screen readers see the messages.
this.$formFeedback.focus();
},
validate: function($el) {
return EdxUtilsValidate.validate($el);
},
liveValidate: function($el, url, dataType, data, method, model) {
$.ajax({
url: url,
dataType: dataType,
data: data,
method: method,
success: function(response) {
model.trigger('validation', $el, response);
}
});
},
inLiveValidationFields: function($el) {
var i,
name = $el.attr('name') || false;
for (i = 0; i < this.liveValidationFields.length; ++i) {
if (this.liveValidationFields[i] === name) {
return true;
}
}
return false;
}
});
});
}).call(this, define || RequireJS.define);