« Back to Index

Chunky script

View original Gist on GitHub

application.js

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 = '&pound;' + 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
    
});