<html><head><meta name="color-scheme" content="light dark"></head><body><pre style="word-wrap: break-word; white-space: pre-wrap;">/**
 * Backbone Views for making easy AJAX POST submissions.
 *
 * @author znanja, inc
 */

define([
	'jquery', 'underscore', 'backbone', 'main', 'response',
	'jquery.iframe-post-form'
],
function($, _, Backbone, Common, resp)
{
	var Views = {};

	/**
	 * Display the error modal for the response of a failed request.
	 *
	 * Additional information may be shown based on the status code of the
	 * request.
	 *
	 * Options:
	 * model -- response object
	 */
	Views.ErrorModalView = Backbone.View.extend({
		el: '#modal-error',

		render: function()
		{
			this.$('.error').addClass('hide');
			this.$('.error-' + this.model.status).removeClass('hide');

			this.$el.modal();
		}
	});


	/**
	 * Abstract View for all AJAX-based Views.
	 *
	 * The following classes are used by this View:
	 *
	 * - ajax-loading: Given to elements while the AJAX request is in progress.
	 *
	 * - ajax-disabled: Given to form fields that have been disabled by this module
	 *   while an AJAX request is in progress.
	 *
	 * Messages produced by responseHandler() (i.e., having class "response") will
	 * be removed upon submission.
	 */
	Views.AjaxView = Backbone.View.extend({
		/**
		 * Prepare view for submission and return a Promise that is resolved
		 * once preparations are completed or rejected if a submission is in
		 * progress.
		 *
		 * @return {Promise}
		 */
		preSubmit: function()
		{
			// The "ajax-loading" class is applied to indicate submission is in
			// progress and to prevent duplicate submissions
			this.trigger('request:submit');

			if (!this.$el.hasClass('ajax-loading'))
			{
				this.$el.addClass('ajax-loading');
				this.disableButtons();
				return this.clearMessages();
			}
			var promise = $.Deferred();
			promise.reject();
			return promise;
		},

		/**
		 * Restore View after a response has been received.
		 */
		postResponse: function(response)
		{
			this.$el.removeClass('ajax-loading');
			this.enableButtons();
			this.trigger('response', response);

			if (response.success)
			{
				this.trigger('response:success', response);
			}
			else
			{
				this.trigger('response:error', response);
			}
		},

		/**
		 * Disable all buttons to prevent duplicate submissions.
		 */
		disableButtons: function()
		{
			// Use "ajax-disabled" so the View can distinguish between what it
			// disabled and buttons that were already disabled
			$('button:not([disabled])', this.el)
				.attr('disabled', true)
				.addClass('ajax-disabled');
		},

		/**
		 * Enable all buttons previously disabled by this View.
		 */
		enableButtons: function()
		{
			$('button.ajax-disabled', this.el)
				.removeAttr('disabled')
				.removeClass('ajax-disabled');
		},

		/**
		 * Clear old response messages and return a Promise that is resolved
		 * once all animations have finished.
		 *
		 * @return {Promise}
		 */
		clearMessages: function()
		{
			return $.when(
				$('.help-block.response', this.el)
					.fadeOut()
					.promise().done(function()
					{
						$(this).parents('.control-group')
							.removeClass('error warning success info');
						$(this).remove();
					}),
				$('.alert.response', this.el)
					.slideUp()
					.promise().done(function()
					{
						$(this).remove();
					})
			);
		}
	});

	/**
	 * View that accepts a Promise, disables a form for the duration of the
	 * Deferred, and then processes the response from the Deferred.
	 *
	 * This is a little more generic than a normal form view, because it
	 * requires a Promise to be passed rather than listening for form submission
	 * and using the Promise for an AJAX request.
	 */
	Views.AjaxDeferView = Views.AjaxView.extend({
		/**
		 * Disable the form, wait for the given Promise to be resolved or
		 * rejected, then handle the response returned from the Promise.
		 *
		 * @param {Promise} promise  Promise to monitor
		 */
		await: function(promise)
		{
			var self = this;

			this.preSubmit().done(function()
			{
				promise.always(function(response)
				{
					resp.responseHandler(response, self.$el)
						.done(function(response)
						{
							self.postResponse(response);
						});
				});
			});
		}
	});

	/**
	 * View for saving a model, that is "bound" to a form or very closely
	 * related enough to have it makes sense to save the model when the form
	 * is submitted.
	 *
	 * Options:
	 * model -- the model associated with this form
	 * models -- an array of models to associate with this forum
	 */
	Views.AjaxModelFormView = Views.AjaxView.extend({
		patch: false,
		events: {'submit': 'submitForm'},

		initialize: function(options)
		{
			if (options.models !== undefined)
			{
				if (!_.isArray(options.models))
				{
					throw new Error(
						'options.models must be an array of models'
					);
				}
				else
				{
					this.models = options.models;
				}
			}
			else
			{
				this.models = [options.model];
			}

			this.destroy = options.destroy;
		},

		/**
		 * Either saves or destroys the given model based on the view's options.
		 *
		 * @param {Model} model  Model to change
		 * @return {Promise}
		 */
		updateModel: function(model)
		{
			if (this.destroy)
			{
				return model.destroy();
			}
			else
			{
				return model.save(model.attributes, {patch: self.patch});
			}
		},

		submitForm: function(event)
		{
			var self = this;

			this.preSubmit().done(function()
			{
				// Save our model (or models)
				var deferreds = _.map(self.models, function(model)
				{
					return self.updateModel(model);
				});

				$.when.apply(this, deferreds)
					.done(function(response)
					{
						if (!_.isArray(response))
						{
							collected_response = _.extend(
								{success: true, messages: []},
								response
							);
						}
						else
						{
							collected_response = {
								success: _.reduce(response, function(memo, item)
								{
									return true; //memo | item.success;
								}, false),
								messages: _.reduce(response, function(memo, item)
								{
									memo.push.apply(memo, item.messages);
									return memo;
								}, [])
							};
						}

						resp
							.responseHandler(collected_response, self.$el)
							.done(function(collected_response)
							{
								self.postResponse(collected_response);
							});
					})
					.fail(function(response)
					{
						resp.responseHandler(response, self.$el)
							.done(function(response)
						{
							self.postResponse(response);
						});
					}
				);
			});

			return false;
		}
	});

	/**
	 * View for submitting a form via AJAX.
	 *
	 * Submit a form via AJAX and include all fields serialized in the body of the
	 * POST request.
	 *
	 * Simply instantiate this View using a &lt;form&gt; and AJAX is used to submit POST
	 * requests.  Override serialize() to customize the data submitted.  By default,
	 * it serializes the form using jQuery.
	 *
	 * Options:
	 * method -- HTTP method to use (uses method attribute by default)
	 */
	Views.AjaxFormView = Views.AjaxView.extend({
		events: {'submit': 'submitForm'},

		initialize: function(options)
		{
			this.method = options.method;
			this.action = options.action;
		},

		/**
		 * Handle form submission.
		 */
		submitForm: function(event)
		{
			var self = this;
			this.setupForm();

			this.preSubmit().done(function()
			{
				$.ajax(
						self.action,
						{
							data: self.serialize(),
							type: self.method
						}
				).always(function(response)
				{
					resp.responseHandler(response, self.$el).done(function(response)
					{
						self.postResponse(response);
					});
				});
			});

			return false;
		},

		setupForm: function()
		{
			this.method = this.method || this.$el.attr('method');
			this.action = this.action || this.$el.attr('action');
		},

		/**
		 * Return serialized form data.
		 *
		 * @return {string}
		 */
		serialize: function()
		{
			return this.$el.serialize();
		}
	});


	/**
	 * Partially generic View for managing payment forms.
	 *
	 * This View uses the validation in Stripe.JS to validate the information
	 * real-time to the user. This means, we show them the card type, make sure
	 * all the fields *look* OK. This View doesn't generate a token from Stripe,
	 * since we could (should) extend it base on the use Payment Processor
	 * (since Organizations can define their own, sort of, for the Storefront.
	 *
	 * Simply instantiate this View using a &lt;form&gt;, and the View will do the
	 * calidation for it.
	 */
	Views.AjaxPaymentFormView = Views.AjaxFormView.extend({
		events: {
			'submit': 'submitForm',
			'keyup input.payment.card-number': 'checkCard',
			'keyup input.payment.card-exp_month, input.payment.card-exp_year': 'checkExpiry',
			'keyup input.payment.card-cvc': 'checkCVC',
			'change input.payment.card-number': 'checkCard',
			'change input.payment.card-exp_month, input.payment.card-exp_year': 'checkExpiry',
			'change input.payment.card-cvc': 'checkCVC'
		},

		checkCard: function(event)
		{
			var control = this.$(event.currentTarget).parents('div.form-group');

			var number = this.$(event.currentTarget).val();
			var valid = Stripe.validateCardNumber(number);
			var type = 'credit-card';

			this._validate(control, valid).always(function()
			{
				type = Stripe.cardType(number).replace(/[^a-zA-Z0-9_\-]/, '').toLowerCase();
				var icon = control.find('label.control-label i');

				/** Let Stripe.JS figure out the Card type */
				var type_field = $('&lt;input&gt;', {
					type: 'hidden',
					name: 'card-type',
					value: type
				});

				var existing_type_field = control.find('input[name=card-type]');
				if(existing_type_field.length)
				{
					existing_type_field.val(type);
				} else
				{
					control.append(type_field);
				}

				icon.removeClass();
				$(icon).addClass('fa fa-' + type);
			});

		},

		checkExpiry: function(event)
		{
			var control = this.$(event.currentTarget).parents('div.form-group');

			var month = control.find('input.payment.card-exp_month').val();
			var year = control.find('input.payment.card-exp_year').val();

			var valid = Stripe.validateExpiry(month, year);

			this._validate(control, valid);
		},

		checkCVC: function(event)
		{
			var control = this.$(event.currentTarget).parents('div.form-group');

			var cvc = this.$(event.currentTarget).val();
			var valid = Stripe.validateCVC(cvc);

			this._validate(control, valid);
		},

		_validate: function(control, valid)
		{
			var defer = $.Deferred();

			if(typeof error === 'undefined')
			{
				error = false;
			}

			if(valid)
			{
				control.addClass('has-success');
				control.removeClass('has-error');
				defer.resolve();
			} else
			{
				control.addClass('has-error');
				control.removeClass('has-success');
				defer.reject();
			}

			return defer;
		}
	});

	/**
	 * View for submitting paymnt form via. AJAX to create a Stripe.JS token
	 *
	 * Submit a form via. AJAX and include all fields serialized in the body of
	 * the POST request, including an additional stripe_token field, with the
	 * chargeable token.
	 *
	 * Simply instantiate this View using a &lt;form&gt; and AJAX is used to create a
	 * token and POST request for the form.
	 *
	 * Options:
	 * fields -- Fields to be included in the POST request, along with the
	 *           Stripe token.
	 */
	Views.StripePaymentFormView = Views.AjaxPaymentFormView.extend({
		initialize: function(options)
		{
			Views.AjaxPaymentFormView.prototype.initialize.apply(this,
																 arguments);
			this.fields = options.fields || {};
		},

		submitForm: function(event)
		{
			var self = this;
			this.setupForm();

			this.preSubmit().done(function()
			{
				// Create and post our token (unless something happened)
				Stripe.createToken(self.$el, function(status, response)
				{
					// Stripe gave us an error
					if(response.error)
					{
						field = response.error.param.split('_')[0];
						resp.responseHandler({
							messages: [{
								'text': response.error.message,
								'for': 'card-' + field,
								'type': 'error'
							}],
							success: false
						}, self.$el).done(function(response)
						{
							self.postResponse(response);
						});
					} else
					{
						// Post our Stripe Token to our account
						$.post(self.action, self.serialize(response))
						 .always(function(response)
						{
							resp.responseHandler(response, self.$el)
								.done(function(response)
								{
									self.postResponse(response);
								});
						});
					}
				});
			});

			return false;
		},

		serialize: function(stripe_response)
		{
			var data = {
				stripe_token: stripe_response.id // Our Stripe.JS token
			};

			_.each(this.fields, function(keyname)
			{
				data[keyname] = $('[name=' + keyname + ']').val();
			});

			return $.param(data, true);
		}
	});

	/**
	 * View for submitting a file upload form via AJAX.
	 *
	 * Simply instantiate this View using a &lt;form&gt; and XHR2 functionality is used to
	 * submit POST requests via AJAX.  If the browser doesn't support XHR2, it
	 * falls back to using iframes.
	 *
	 * Since Internet Explorer sucks, don't change any of the event handling on the
	 * submit event.  Instead, you can extend submitForm and return false to cancel
	 * any form submission.  Otherwise, you need to handle IE separately from every
	 * other browser.
	 */
	Views.AjaxUploadFormView = Views.AjaxView.extend({
		events: {
			// When we submit the dialog, check to see if we need to show the
			// upload/conversion progress bar. We only need to show it when we
			// uploaded a file, and didn't just change the file.
			'submit': function(e)
			{
				var self = this;
				var file = $(e.currentTarget).find('input[type=file]').val();

				if(file.length !== 0)
				{
					// Bootstrap 3 'hide' class fix.
					self.$el.find('.uploading').hide();
					self.$el.find('.uploading').removeClass('hide');
					self.$el.find('.uploading').slideDown();
				}
			}
		},

		initialize: function(options)
		{
			this.supportXHR2 = Boolean(window.FormData);

			this.uploadingBar = this.$el.find('.uploading');
			this.message = this.uploadingBar.find('.message');
			this.progress = this.uploadingBar.find('.progress');
			this.options = options;

			if (!this.supportXHR2)
			{
				var view = this;
				this.$el.iframePostForm({
					json: true,
					post: function()
					{
						view.preSubmit();
						return view.submitForm();
					},
					complete: $.proxy(this.callback, this)
				});
			}
			else
			{
				this.$el.on('submit', $.proxy(this.submitForm, this));
			}
		},

		/**
		 * Handle form submission.
		 *
		 * Requires browser support for XMLHttpRequest 2, which is supported
		 * everywhere except for everyone's favourite browser, Internet Explorer.
		 *
		 * Older browsers will just submit the form without any AJAX.
		 */
		submitForm: function(event, formData)
		{
			this.formData = formData;
			if (this.supportXHR2)
			{
				// With XHR2, we can submit the form over AJAX.  Without XHR2, let
				// the default submit handler deal with it, but use iframe-post-form
				// to keep the "AJAX-y" effect
				var view = this;

				this.preSubmit().done(function()
				{
					if (typeof view.formData === 'undefined')
					{
						formData = new FormData(view.el);
					}
					else
					{
						formData = view.formData;
					}

					$.ajax({
						url: view.$el.attr('action'),
						type: 'POST',
						xhr: function()
						{
							var my_xhr = $.ajaxSettings.xhr();
							if(my_xhr.upload)
							{
								my_xhr.upload.addEventListener('progress', function(e)
								{
									view.trigger('progress', e);
								});
							}
							return my_xhr;
						},
						data: formData,
						cache: false,
						contentType: false,
						processData: false
					}).always($.proxy(view.callback, view));
				});


				return false;
			}
		},

		/**
		 * Update the progress bar when the AjaxUploadFormView recieves a
		 * progress event from the xhr object. Depending on the speed of the
		 * user, we could get a bunch of these, or a few.
		 *
		 * We draw the progress by changing the width of the .progress &gt; .bar
		 * as we get progress form the xhr object
		 *
		 * @param {e} - xhr upload progress event
		 */
		updateProgress: function(e, scale, message)
		{
			var completed;
			message = message === undefined ? 'Uploading...' : message;
			scale = scale || 100;

			if(typeof(e) === 'number')
			{
				completed = e;
			} else
			{
				completed = e.position * scale / e.total;
			}

			var bar = this.progress.find('.progress-bar');

			this.setMessage(message);
			bar.css('width', completed + "%");
		},

		resetProgress: function()
		{
			this.progress.find('.progress-bar').css('width', '0%');
			this.uploadingBar.hide();
		},

		setMessage: function(message)
		{
			this.message.text(message);
		},

		/**
		 * Callback after a response has been received.
		 */
		callback: function(response)
		{
			var view = this;
			resp.responseHandler(response, this.$el).done(function(response)
			{
				view.postResponse(response);
			});
		}
	});


	/**
	 * A generic view for triggering some request when a button is clicked and
	 * then handling the response.
	 *
	 * The view element must be a button.
	 *
	 * This view must be extended to implement submit() which must return
	 * a Promise that is resolved with a response object.
	 *
	 * Options:
	 * messageEl -- selector for an .overlay-message to show on success
	 *
	 * Events:
	 * button:click -- triggered when button is clicked
	 * button:success -- triggered when successful response is received
	 * button:error -- triggered when failed response is received
	 */
	Views.ButtonView = Backbone.View.extend({
		events: {'click': 'onClick'},

		initialize: function(options)
		{
			options = options || {};
			this.messageEl = this.messageEl || options.messageEl;
		},

		/**
		 * Event handler called when the button is clicked.
		 *
		 * This disables the button while the Promise returned by submit() is
		 * unresolved.
		 */
		onClick: function()
		{
			var self = this;

			this.$el.attr('disabled', true);
			this.trigger('button:click');

			this.submit()
				.always(function()
				{
					self.$el.attr('disabled', false);
				})
				.done(function(response)
				{
					self.trigger('button:success', response);
					self.onSuccess(response);
				})
				.fail(function(response)
				{
					self.trigger('button:error', response);
					self.onError(response);
				});
		},

		/**
		 * Event handler called when a successful response is received.
		 *
		 * If this view was configured to show a message, the mesasge will be
		 * shown.
		 */
		onSuccess: function()
		{
			if (this.messageEl)
			{
				new Common.OverlayView({el: this.messageEl}).render();
			}
		},

		/**
		 * Event handler called when a failed response is received.
		 *
		 * This displays an error message.
		 */
		onError: function(response)
		{
			new Views.ErrorModalView({model: response}).render();
		}
	});


	/**
	 * View for submitting an AJAX POST request via clicking a button.
	 *
	 * Clicking a button submits a POST request to a URI.  There might be several
	 * buttons under a common parent element.
	 *
	 * Wrap an element around all the buttons involved and instantiate this View
	 * using that element.  Each button should have a "data-post" attribute, which
	 * is the URI used to submit the POST request.
	 *
	 * To customize the data submitted with the request, override serialize().  By
	 * default, the POST request is empty.
	 */
	Views.AjaxButtonView = Views.AjaxView.extend({
		events: {
			'click button[data-post]': 'submitButton',
			'click button[data-delete]': 'deleteButton',
			'click button[data-put]': 'putButton',
			'click button[data-options]': 'optionsButton',
			'click button[data-get]': 'getButton',
			'click button[data-head]': 'headButton'
		},

		// Handle RESTful AJAX calls from buttons within the view

		/**
		 * Handle triggering a POST request via button submission.
		 */
		submitButton: function(event)
		{
			var self = this;
			var button = this.$(event.currentTarget);

			this.preSubmit().done(function()
			{
				$.post(button.data('post'), self.serializeButton(button))
					.always(function(response)
					{
						resp.responseHandler(response, self.$el, button)
							.done(function(response)
							{
								self.postResponse(response);
							});
					});
			});

			return false;
		},

		/**
		 * Handle triggering a DELETE request via button submission
		 */
		deleteButton: function(event)
		{
			return this._send_button('delete', event);
		},

		/**
		 * Handle triggering a PUT request via button submission
		 */
		putButton: function(event)
		{
			return this._send_button('put', event);
		},

		optionsButton: function(event)
		{
			return this._send_button('options', event);
		},

		getButton: function(event)
		{
			return this._send_button('get', event);
		},

		headButton: function(event)
		{
			return this._send_button('head', event);
		},

		_send_button: function(type, event, name)
		{
			name = name === undefined ? type : name;
			var self = this;
			var button = this.$(event.currentTarget);

			this.preSubmit().done(function()
			{
				$.ajax({
					type: type,
					url: button.data(name)
				}).always(function(response)
				{
					resp.responseHandler(response, self.$el, button)
						.done(function(response)
						{
							self.postResponse(response);
						});
				});
			});
		},

		/**
		 * Returns serialized data for the POST request triggered via button
		 * submission.
		 *
		 * @return {string}
		 */
		serializeButton: function(button)
		{
			return '{}';
		}
	});

	/**
	 * Auto-instantiation of Views.
	 *
	 * For forms, use &lt;form class="ajax"&gt; or &lt;form class="ajax-upload"&gt;.
	 *
	 * For buttons, use &lt;section class="ajax"&gt; as the common parent element of the
	 * buttons.
	 */
	$(function()
	{
		$('form.ajax').each(function()
		{
			if (!$(this).data('view'))
			{
				$(this).data('view', new Views.AjaxFormView({el: $(this)}));
			}
		});

		$('section.ajax').each(function()
		{
			if (!$(this).data('view'))
			{
				$(this).data('view', new Views.AjaxButtonView({el: $(this)}));
			}
		});

		$('form.ajax-upload').each(function()
		{
			if (!$(this).data('view'))
			{
				$(this).data('view', new Views.AjaxUploadFormView({el: $(this)}));
			}
		});

		$('form.payment').each(function()
		{
			if (!$(this).data('view'))
			{
				$(this).data('view', new Views.StripePaymentFormView({
					el: $(this)
				}));
			}
		});
	});

	return Views;
});
</pre></body></html>