define(['../Utils/Templating/hogan', '../Utils/Datepicker/kalendae', '../Models/LoanApplication', '../Utils/DOM/getEl', 'Backbone'], function (hogan, Kalendae, LoanApplication, getElement) {
return Backbone.View.extend({
model: new LoanApplication(),
initialize: function(){
// Store other elements that will be interacted with.
// Any element that will potentially utilise jQuery we pre-wrap in a single jQuery instance.
this.promocode = getElement('js-promocode');
this.amount = getElement('js-amount');
this.error_amount = $('#js-amounterror');
this.error_amount_popup = $('#js-amounterror-popup');
this.popup = $('#js-loanpopup');
this.popup_amount = getElement('js-popupamount');
this.loan_details = getElement('js-loandetails');
this.apply_now = $('#js-applynow');
this.trigger_popup = getElement('js-pickdate'); // this is the form input that triggers the popup to be shown
// We use this to tell whether the calendar widget has already been rendered,
// as there is no point re-rendering it every time the popup is closed then opened again.
this.is_calendar_rendered = false;
// Details that will be passed around in different methods
this.days_to_pay = null;
this.amount_to_borrow = null;
this.selected_date = null;
// This holds the template file we'll compile with data pulled from server
this.template = null;
this.template_content = null;
// The Model triggers custom events when certain actions happen which the View should ideally handle
this.model.on('item:invalid', this.process_errors, this);
this.model.on('amount:changed', this.validation_success, this);
// There is one instance where we need the Model to have access to the View (so we can set a shared property)
this.model.view = this;
// Used within 'remove_error' method
this.allow_app_process = false;
// Used to determine if the application can proceed
this.valid_date = false;
// Used to determine if the validation passed
this.validation_pass = false;
},
// The containing element
el: getElement('js-loanapplication'),
// Selectors are scoped to the parent element
events: {
'focus #js-pickdate': 'validate_amount',
'click #js-calendarclose': 'close_popup',
'click #js-applynow': 'validate_amount',
'keyup #js-popupamount': 'validate_amount'
},
validate_amount: function (e) {
/*
First thing we need to do is to lose focus on the #js-pickdate input element
Otherwise, if the user has the popup open and then decides to view a different screen and comes back.
Returning back causes the input to gain focus again.
*/
this.trigger_popup.blur();
/*
We validate a different field depending on whether the popup is open (the popup has its own copy of the application fields)
Note: Model's "set" method calls Backbone validation by default (see Model for validation rules)
*/
// If popup is hidden
if (this.popup.hasClass('hide')) {
this.model.set({
amount: this.amount.value,
event_type: e.type
});
}
// If popup is visible
else {
if (e.type === 'keyup') {
/*
We don't want to validate the amount if the user is just pressing the shift key or the left/right/up/down arrow keys:
Shift = 16 | Left = 37 | Up = 38 | Right = 39 | Down = 40
*/
if (!_.contains([16, 37, 38, 39, 40], e.keyCode)) {
this.model.set({
amount: this.popup_amount.value,
event_type: e.type // passing through the event type means we can prevent the 'remove_error' method from processing the application
});
}
}
else {
this.model.set({
amount: this.popup_amount.value,
event_type: e.type
});
}
}
},
process_errors: function (item) {
// Hide the 'apply' button if it's already viewable
this.apply_now.addClass('hide');
// Check what field was invalid and display corresponding error message
switch (item.field) {
case 'amount':
// We display the error message in a different place depending on whether the popup is open
// If the popup is NOT visible
if (this.popup.hasClass('hide')) {
this.error_amount.removeClass('invisible');
} else {
this.error_amount_popup.removeClass('invisible');
}
break;
}
},
validation_success: function(){
this.validation_pass = true;
this.check_apply_display();
},
check_apply_display: function(){
// If there is a valid date then we can show the 'apply' button
if (this.valid_date) {
this.apply_now.removeClass('hide');
}
// Now we hide any errors
this.remove_error();
},
// TODO: refactor this function - surely the inner if statement logic can be abstracted into a separate function?
remove_error: function(){
// We remove the error message from different places depending on whether the popup is open
// If the popup is NOT visible
if (this.popup.hasClass('hide')) {
this.error_amount.addClass('invisible');
this.display_calendar();
} else {
this.error_amount_popup.addClass('invisible');
// Only process the application if the user has explictly clicked on the 'apply' button and their data has been validate
if (this.allow_app_process && this.valid_date) {
window.location = 'create-account.php?amount=' + this.popup_amount.value + '&paymentdate=' + this.selected_date;
} else {
// Noticed issue with keyup event constantly firing, so safer just to force the input to lose focus
this.popup_amount.blur();
}
}
},
display_calendar: function(){
/*
We only load the calendar on screens large enough to display it
And we make sure to only render it once by check "is_calendar_rendered" is false
UPDATE: Since moving this script into an 'advanced' file and having a seperate 'basic' version - the clientWidth check is redundant here.
Although it probably isn't much of a performance hit to have that extra check in place still.
*/
if (document.documentElement.clientWidth >= 585 && !this.is_calendar_rendered) {
this.render_calendar();
}
// Pass through the value into this new popup view
this.popup_amount.value = this.amount.value;
// Make the popup visible
this.popup.removeClass('hide');
},
render_calendar: function(){
this.is_calendar_rendered = true;
// Context of "this" gets lost within the Kalendae script
var self = this;
// the following variables are used for calculating the difference between
// today's date and the selected date to pay back the loan
var calendar_container = getElement('js-calendar');
var calendar;
calendar = new Kalendae({
// element to attach the calendar to
attachTo: calendar_container,
// blackout days after 45 days from current date
blackout: function (date) {
return Kalendae.moment().yearDay() + 45 < date.yearDay(); // yearDay() is an extension Kalendae adds to moment.js to calculate the total number of days since epoch.
},
// how many characters from the week day name to display (e.g. we've gone with 3 = Mon, Tue, Wed, Thu, Fri, Sat, Sun)
columnHeaderLength: 3,
// restricts date selectability to past or future ('future' blacks out all days previous to current date)
direction: 'future',
// only allows selection of one day
mode: 'single',
// determines the number of months to display
months: 2,
// determines when the week should start (Sunday = 0 [default] or Monday = 1 etc)
weekStart: 1,
// causes the <input> to update to the selected date
subscribe: {
'date-clicked': function(){
// This event is fire before the selection is changed
if (!self.validation_pass) {
return false;
}
},
'change': function(){
// We don't want to let the user select a date if the amount is invalid
// allow_app_process is being used to determine if the app can move to stage 1 of application
// but it's set every time validation is carried out so we can use it here
if (self.validation_pass) {
// We know a date was selected so we store that information
self.valid_date = true;
/*
There could be an instance where the user enters a valid amount *before* the popup is shown,
and then selects a date from the date picker, but that wont trigger a change event on the amount
and so the model doesn't validate the amount and doesn't then cause the 'apply' button to appear.
This means we need to check here (once a date is picked) if the user should see the 'apply' button.
We do this by setting the attribute value again (thus causing the validation to be triggered)
*/
self.model.set({
amount: self.popup_amount.value
});
// The following code works out the number of days selected to pay back the loan
var selected_date = this.getSelected();
var temp_integer_month;
var one_day;
var payback_date;
var days_to_pay;
days_to_pay = selected_date.split('-');
// Make sure the date is in the correct order
self.selected_date = days_to_pay[2] + '-' + days_to_pay[1] + '-' + days_to_pay[0];
// the date is returned as non-zero index format, so put it back to be zero-indexed
temp_integer_month = parseInt(days_to_pay[1], 10);
days_to_pay[1] = '0' + --temp_integer_month;
one_day = 24*60*60*1000; // hours * minutes * seconds * milliseconds
payback_date = new Date(days_to_pay[0], days_to_pay[1], days_to_pay[2]);
days_to_pay = Math.ceil(Math.abs(((new Date()).getTime() - payback_date.getTime()) / (one_day)));
// Keep reference to number of days to pay
self.days_to_pay = days_to_pay;
// call function which will pull in the relevant template and populate with relevant costs
self.calculate();
}
}
}
});
},
close_popup: function(){
this.amount.value = ''; // We reset the value so the Model's "change" event can be fired (which is what we rely upon to trigger the popup)
this.trigger_popup.value = ''; // was finding Firefox would randomly put the 'amount' value into this input, can't see why though?
this.loan_details.innerHTML = '<dt></dt><dd></dd>'; // if the user re-opens the popup then we don't want the old details to be there still
this.allow_app_process = false;
this.valid_date = false;
this.validation_pass = false;
this.apply_now.addClass('hide');
this.popup.addClass('hide');
},
calculate: function(){
// Noticed issue with keyup event constantly firing, so safer just to force the input to lose focus
this.popup_amount.blur();
// Context of "this" gets lost within the jQuery methods
var self = this;
// Wait for async functions to finish before inserting HTML
function process(){
$.when(self.get_costs(self.popup_amount.value)).then(function (data) {
// Take the JSON data and compile it into the template file
self.template_content = self.template.render(JSON.parse(data));
// Generate the HTML code
self.generate_html();
});
}
// The following code prevents calling the server to load/compile the same template code every time the button is pressed.
// Instead we retrieve the template and compile it once
if (self.template) {
process();
} else {
$.ajax({
url: 'Assets/Templates/Application-Calculator.txt',
dataType: 'html',
success: function (tmp) {
self.template = hogan.compile(tmp);
process();
}
});
}
},
get_costs: function (amount) {
var dfd = $.Deferred();
$.ajax({
url: 'Assets/PHP/calculator.php',
type: 'POST',
data: 'amount=' + amount + '&days=' + this.days_to_pay,
success: function (data) {
dfd.resolve(data);
}
});
return dfd.promise();
},
generate_html: function(){
// Insert the template content into the DOM
this.loan_details.innerHTML = this.template_content;
}
});
});