define(['edx-ui-toolkit/js/utils/html-utils', 'js/views/baseview', 'underscore', 'jquery', 'gettext', 'common/js/components/views/feedback_notification', 'common/js/components/views/feedback_alert', 'js/views/baseview', 'jquery.smoothScroll'], function(HtmlUtils, BaseView, _, $, gettext, NotificationView, AlertView) { var ValidatingView = BaseView.extend({ // Intended as an abstract class which catches validation errors on the model and // decorates the fields. Needs wiring per class, but this initialization shows how // either have your init call this one or copy the contents initialize: function() { this.listenTo(this.model, 'invalid', this.handleValidationError); this.selectorToField = _.invert(this.fieldToSelectorMap); }, errorTemplate: HtmlUtils.template('<span class="message-error"><%- message %></span>'), save_title: gettext("You've made some changes"), save_message: gettext('Your changes will not take effect until you save your progress.'), error_title: gettext("You've made some changes, but there are some errors"), error_message: gettext('Please address the errors on this page first, and then save your progress.'), events: { 'change input': 'clearValidationErrors', 'change textarea': 'clearValidationErrors' }, fieldToSelectorMap: { // Your subclass must populate this w/ all of the model keys and dom selectors // which may be the subjects of validation errors }, _cacheValidationErrors: [], handleValidationError: function(model, error) { this.clearValidationErrors(); // error is object w/ fields and error strings for (var field in error) { var ele = this.$el.find('#' + this.fieldToSelectorMap[field]); this._cacheValidationErrors.push(ele); this.getInputElements(ele).addClass('error'); HtmlUtils.append($(ele).parent(), this.errorTemplate({message: error[field]})); } $('.wrapper-notification-warning').addClass('wrapper-notification-warning-w-errors'); $('.action-save').addClass('is-disabled'); // TODO: (pfogg) should this text fade in/out on change? $('#notification-warning-title').text(this.error_title); $('#notification-warning-description').text(this.error_message); }, clearValidationErrors: function() { // error is object w/ fields and error strings while (this._cacheValidationErrors.length > 0) { var ele = this._cacheValidationErrors.pop(); this.getInputElements(ele).removeClass('error'); $(ele).nextAll('.message-error').remove(); } $('.wrapper-notification-warning').removeClass('wrapper-notification-warning-w-errors'); $('.action-save').removeClass('is-disabled'); $('#notification-warning-title').text(this.save_title); $('#notification-warning-description').text(this.save_message); }, setField: function(event) { // Set model field and return the new value. this.clearValidationErrors(); var field = this.selectorToField[event.currentTarget.id]; var newVal = ''; if (event.currentTarget.type == 'checkbox') { newVal = $(event.currentTarget).is(':checked').toString(); } else { newVal = $(event.currentTarget).val(); } this.model.set(field, newVal); this.model.isValid(); return newVal; }, // these should perhaps go into a superclass but lack of event hash inheritance demotivates me inputFocus: function(event) { $("label[for='" + event.currentTarget.id + "']").addClass('is-focused'); }, inputUnfocus: function(event) { $("label[for='" + event.currentTarget.id + "']").removeClass('is-focused'); }, getInputElements: function(ele) { var inputElements = 'input, textarea'; if ($(ele).is(inputElements)) { return $(ele); } else { // put error on the contained inputs return $(ele).find(inputElements); } }, showNotificationBar: function(message, primaryClick, secondaryClick) { // Show a notification with message. primaryClick is called on // pressing the save button, and secondaryClick (if it's // passed, which it may not be) will be called on // cancel. Takes care of hiding the notification bar at the // appropriate times. if (this.notificationBarShowing) { return; } // If we've already saved something, hide the alert. if (this.saved) { this.saved.hide(); } var self = this; this.confirmation = new NotificationView.Warning({ title: this.save_title, message: message, actions: { primary: { 'text': gettext('Save Changes'), 'class': 'action-save', 'click': function() { primaryClick(); self.confirmation.hide(); self.notificationBarShowing = false; } }, secondary: [{ 'text': gettext('Cancel'), 'class': 'action-cancel', 'click': function() { if (secondaryClick) { secondaryClick(); } self.model.clear({silent: true}); self.confirmation.hide(); self.notificationBarShowing = false; } }] }}); this.notificationBarShowing = true; this.confirmation.show(); // Make sure the bar is in the right state this.model.isValid(); }, showSavedBar: function(title, message) { var defaultTitle = gettext('Your changes have been saved.'); this.saved = new AlertView.Confirmation({ title: title || defaultTitle, message: message, closeIcon: false }); this.saved.show(); $.smoothScroll({ offset: 0, easing: 'swing', speed: 1000 }); }, saveView: function() { var self = this; this.model.save( {}, { success: function() { self.showSavedBar(); self.render(); }, silent: true } ); } }); return ValidatingView; }); // end define()