define(['../Utils/Events/events', '../Utils/XHR/ajax', '../Utils/Templating/hogan', '../Utils/Patterns/when', '../Utils/DOM/getEl', '../Utils/CSS/getAppliedStyle', '../Utils/CSS/addClass', '../Utils/CSS/removeClass', '../Utils/CSS/hasClass', '../Utils/DOM/getOffset', '../Utils/Datepicker/kalendae'], function (event, ajax, hogan, when, getElement, getAppliedStyle, addClass, removeClass, hasClass, getOffset, Kalendae) {
/*
* Code Structure:
* - Variables
* - Functions
* - check_amount
* - check_popup_close
* - load_calendar
* - get_costs
* - generate_html
* - calculate
* - process_application
* - update_amount
* - show_popup
* - Initialisation
*/
var amount = getElement('js-amount');
var amount_updated = getElement('js-amount-update');
var max = +amount.getAttribute('data-max');
var specify_amount = getElement('js-specifyamount');
var paydate = getElement('js-choosepaydate');
var popup = getElement('js-calendarpopup');
var button_continue = getElement('js-appcontinue');
var details_container = getElement('js-details');
var details_summary = getElement('js-details-summary');
var borrowing = getElement('js-initialamount');
var limit_reached = false;
var calendar_container = document.createElement('div');
var days_to_pay = 1;
var calendar, template, template_content;
function check_amount (isPopup) {
var input = (isPopup) ? amount_updated : amount;
// because the content of the summary box is pulled in from a template file we have
// to store a reference to the element in the current execution context
// e.g. we can't store in a variable prior to this function executing because
// the template is re-loaded every time a new calculation is made
var apply_now = getElement('js-applynow');
/*
* if statement checks following aspects:
* make sure value entered isn't the same as the placeholder value
* make sure user has entered a number
* make sure a value of some form has been entered
*/
if (input.value === input.getAttribute('placeholder') ||
!/^\d+$/.test(input.value) ||
input.value.length === 0) {
input.value = 0;
input.focus();
// only handle messages if the popup isn't showing
if (!isPopup) {
if (limit_reached) {
specify_amount.innerHTML = specify_amount.getAttribute('data-noamount');
limit_reached = false;
}
removeClass(specify_amount, 'hide');
}
// if we can access the #js-applynow element then hide it now there is an error
if (apply_now) {
addClass(apply_now, 'hide');
}
}
/*
* because DOM values are of type String we use unary operator to convert to integer
* we then check to see if the user has requested a value lower than 1
* if they have then we reset it to to zero and update the message to let them know
*/
else if (+input.value < 1) {
input.value = 0;
// only handle messages if the popup isn't showing
if (!isPopup) {
if (limit_reached) {
specify_amount.innerHTML = specify_amount.getAttribute('data-noamount');
limit_reached = false;
}
removeClass(specify_amount, 'hide');
}
}
/*
* because DOM values are of type String we use unary operator to convert to integer
* we then check to see if the user has requested a value greater than the max
* if they have then we reset it to the maximum allowed and update the message to let them know
*/
else if (+input.value > max) {
input.value = max;
// only handle messages if the popup isn't showing
if (!isPopup) {
specify_amount.innerHTML = 'Requested amount limit reached';
limit_reached = true;
removeClass(specify_amount, 'hide');
}
// in the popup we aren't displaying a message to the user when they go over the max allowed value
// so in this instance we'll return true so our calling environment (#js-amount-update 'keyup' listener) can start calculating costs
else {
return true;
}
}
/*
* if all other conditions aren't met then we're OK to display the calendar to the user
* and to hide any error message previously shown to the user
*/
else {
addClass(specify_amount, 'hide');
return true; // amount has validated successfully
}
return false;
}
function check_popup_close (e) {
/*
* we want to close the popup when the user clicks on the area where the close button appears.
* the close button is added via CSS' :after pseudo-element and so isn't actually accessible to JavaScript
* so we need to detect the area of the popup where it appears.
* to do this we need to first locate the popup within the page and then calculate where the close button sits relative to the popup
*
* the CSS looks like this:
* height: 30px;
* width: 30px;
* right: -13px;
* top: -10px;
*
* this means the close button has 17px over the popup on the x axis and 20px over the popup on the y axis
*
* we first have to get references to the pseudo-element's styles which is easy enough for browsers that support getComputedStyle
* but for Internet Explorer 8 this becomes a bit of a chore as we have to loop through all rules of specific stylesheet.
* but we make sure we cache this processing work because there is no point in doing it every time the 'checkPopupClose' function is called.
*/
var cache_styles = (function(){
var cache, len, found;
return function(){
if (!cache) {
cache = {};
// IE9+ and all versions of Firefox/Chrome/Safari
if (window.getComputedStyle) {
cache.style = window.getComputedStyle(popup, ':after');
cache.right = parseInt(cache.style.right); // -13
cache.top = parseInt(cache.style.top); // -10
cache.height = parseInt(cache.style.height); // 30
cache.width = parseInt(cache.style.width); // 30
}
// IE8 - needs to loop through stylesheet looking for the relevant Rule to pick up the :after styles from
else {
len = document.styleSheets[1].rules.length;
while (len--) {
if (document.styleSheets[1].rules[len].selectorText === '.application-popup:after') {
found = document.styleSheets[1].rules[len];
break;
}
}
cache.styles = found.style.cssText.toLowerCase();
cache.right = parseInt(cache.styles.match(/right: ?([^;]+)/)[1]); // -13
cache.top = parseInt(cache.styles.match(/top: ?([^;]+)/)[1]); // -10
cache.height = parseInt(cache.styles.match(/height: ?([^;]+)/)[1]); // 30
cache.width = parseInt(cache.styles.match(/width: ?([^;]+)/)[1]); // 30
}
// currently the values are negative integers so we need to check for that and return positive integer if need be
cache.right = (cache.right < 0) ? Math.abs(cache.right) : cache.right; // 13
cache.top = (cache.top < 0) ? Math.abs(cache.top) : cache.top; // 10
}
return cache;
};
}());
var pseudo = cache_styles();
/*
* pageX/Y is position relative to Window
* clientX/Y is for Internet Explorer which doesn't recognise pageX/Y
*
* BUT although clientX/Y is similar it has one small caveat!
* the value changes depending on whether the user has scrolled the window
* which means we need to add on top of clientY any scroll amount (if any)
*/
var x = e.pageX || e.originalEvent.clientX;
var y = e.pageY || (document.documentElement.scrollTop + e.originalEvent.clientY);
var popup_width = popup.clientWidth;
var popup_height = popup.clientHeight;
var popup_offset = getOffset(popup);
var popup_x = popup_offset.left;
var popup_y = popup_offset.top;
var popup_xrange = (popup_x + popup_width) + (pseudo.width - pseudo.right); // 30 - 13 = 17
var popup_yrange = popup_y - pseudo.top;
var popup_xclose = (popup_x + popup_width) - pseudo.right;
var popup_yclose = popup_y + (pseudo.height - pseudo.top); // 30 - 10 = 20
var within_xrange = x <= popup_xrange && x >= popup_xclose;
var within_yrange = y <= popup_yclose && y >= popup_yrange;
if (within_xrange && within_yrange) {
addClass(popup, 'hide');
}
}
function load_calendar(){
// the following variables are used for calculating the difference between
// today's date and the selected date to pay back the loan
var curent_date = new Date();
var current_day = curent_date.getDate();
var current_month = curent_date.getMonth();
var current_year = curent_date.getFullYear();
var today;
// we correct current_month to include a zero prefix if the number is less than 10
current_month = (current_month < 10) ? ('0' + current_month) : current_month;
// construct a date for today which is used for calculating diff
today = new Date(current_year, current_month, current_day);
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: {
'change': function(){
var selected_date = this.getSelected();
var temp_integer_month;
var one_day;
var payback_date;
days_to_pay = selected_date.split('-');
// 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.abs((today.getTime() - payback_date.getTime()) / (one_day));
// update the <input> #js-choosepaydate (currently sitting behind the popup) to display the date selected by the user
paydate.value = selected_date;
// call function which will pull in the relevant template and populate with relevant costs
calculate();
}
}
});
}
function get_costs (amount) {
var dfd = when.defer();
ajax({
url: 'Assets/PHP/calculator.php',
method: 'POST',
data: 'amount=' + amount + '&days=' + days_to_pay,
onSuccess: function (data) {
dfd.resolve(data);
}
});
return dfd.promise;
}
function generate_html(){
details_container.innerHTML = template_content;
details_summary.innerHTML = template_content;
// this is the 'apply now' button outside of the popup
display_applynow();
}
function calculate(){
/*
* wait for async functions to finish before inserting HTML
*/
function process(){
when(get_costs(amount_updated.value), function (data) {
template_content = template.render(JSON.parse(data));
generate_html();
// once the HTML is generated then we're safe to set-up an event listener for the 'continue' button (within the popup)
event.add(button_continue, 'click', process_application);
});
}
// 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 (template) {
process();
} else {
ajax({
url: 'Assets/Templates/Application-Calculator.tpl',
data: 'html',
onSuccess: function (tmp) {
template = hogan.compile(tmp);
process();
}
});
}
}
function display_applynow(){
// because the content of the summary box is pulled in from a template file we have
// to store a reference to the element in the current execution context
// e.g. we can't store in a variable prior to this function executing because
// the template is re-loaded every time a new calculation is made
var apply_now = getElement('js-applynow');
removeClass(apply_now, 'hide');
}
function process_application(){
// hide the popup and show the summary of costs
addClass(popup, 'hide');
removeClass(details_summary, 'hide');
// once the popup is closed we want to show the 'apply now' button within the details summary box
display_applynow();
}
function update_amount (e) {
// we pass through 'true' to the 'check_amount' function so it knows to apply
// specific code branches based on the <input> #js-amount-update within the popup
if (check_amount(true)) {
amount.value = amount_updated.value; // ensure the <input> outside the popup is updated to reflect the new value inside the popup
calculate();
} else {
// Prevent the user from closing the popup if the amount is invalid
event.remove(button_continue, 'click', process_application);
}
}
function show_popup(){
removeClass(popup, 'hide');
event.add(popup, 'click', check_popup_close);
event.add(amount_updated, 'keyup', update_amount);
// pull in amount entered into the main <input> #js-amount
amount_updated.value = amount.value;
// if no template available then we know this is the first time opening the popup
// and we do a quick and dirty 'innerHTML' of the amount to be borrowed
if (!template) {
borrowing.innerHTML = '£' + amount.value;
}
// no point in loading a new instance of the calendar every time the popup is displayed
if (!calendar) {
load_calendar();
}
}
paydate.onfocus = function(){
// validate the amount entered by the user
if (check_amount()) {
show_popup();
}
};
amount.onblur = function(){
// if the user has already opened the popup and selected some values and clicked to continue
// then we know the #js-details-summary element will be visible and so we need to call
// the function which gets costs/interest fees etc and update the values
if (!hasClass(details_summary, 'hide')) {
// validate the amount entered by the user
if (check_amount()) {
amount_updated.value = amount.value; // ensure the <input> inside the popup is updated to reflect the new value outside the popup
calculate();
}
}
};
popup.insertBefore(calendar_container, details_container.parentNode); // insert Kalendae (calendar) widget into the popup dialog
});