define(["../Templating/hogan", "../XHR/ajax", "../Array/map", "../DOM/getEl"], function (hogan, ajax, map, getElement) {
function validate (formname) {
var template,
doc = document,
module = getElement("js-alreadymember"),
insert_errors = getElement("js-formerrors"),
pin_error = getElement("js-incorrectpin"),
errors = [],
form = document.forms[formname],
fields = form.elements,
formats = {
number: function (field) {
if (/^\D+/.test(field.value) || !field.value.length) {
errors.push(field);
} else if (field.name === "pin") {
// We call script which tells us whether the data added by the user is correct or not
ajax({
url: "Assets/PHP/pin.php",
async: false, // first time ever I've needed to use a 'synchronous' AJAX call
method: "POST",
data: "pin=" + field.value + "", // we're posting the user's entered pin to the server
onSuccess: function (data) {
// 'data' is returned as a String which causes issues with coercion (as a string of any length, even if it contains zero, is still a truthy value)
// So we use the + operator to convert the string into a number
if (+data) {
pin_error.className = "hide";
} else {
errors.push(field);
pin_error.className = "";
}
}
});
}
},
date: function (field) {
/*
The regex allows the following formats:
00/00/0000
00/00/00
0/0/00
*/
if (!/^\d{1,2}\/\d{1,2}\/\d{2,4}$/.test(field.value) || !field.value.length) {
errors.push(field);
}
},
email: function (field) {
if (field.value.indexOf("@") === -1 || !field.value.length) {
errors.push(field);
}
},
mobile: function (field) {
/*
The regex allows the following formats:
+44 07000000000
07000000000
+4407000000000
So there is an optional +000 country code at the start (wrapped in a non-capturing group)
We then allow for an optional space after the optional country code
Finally we allow for 11 digits (no spaces)
*/
if (!/^(?:\+\d{1,3})?\s?\d{11}$/.test(field.value) || !field.value.length) {
errors.push(field);
}
},
password: function (field) {
/*
The regex makes sure there is at least 8 alpha-numerical characters
And that at least one of those values is a number
And that at least one of those values is a text character
We use a positive lookahead (which checks to see if a sub pattern matches a specific position)
The lookahead checks for any character (zero or more times) is followed by a digit (e.g. making sure there is at least one digit)
The lookahead then checks for any character (zero or more times) is followed by a text character (e.g. making sure there is at least one text character)
Finally after the two lookaheads we have the standard regex which makes sure there is at least 8 alpha-numerical characters
*/
if (!/^(?=.*\d)(?=.*[a-z])\w{8,}/i.test(field.value)) {
errors.push(field);
}
},
income: function (field) {
if (!/^[\d,.]+/.test(field.value) || !field.value.length) {
errors.push(field);
}
},
accountnumber: function (field) {
if (!/^\d{8}$/.test(field.value)) {
errors.push(field);
}
},
cardnumber: function (field) {
function luhn (cardnumber) {
// Build an array with the digits in the card number
var getdigits = /\d/g;
var digits = [];
while (match = getdigits.exec(cardnumber)) {
digits.push(parseInt(match[0], 10));
}
// Run the Luhn algorithm on the array
var sum = 0;
var alt = false;
for (var i = digits.length - 1; i >= 0; i--) {
// On every other number in the card sequence...
if (alt) {
digits[i] *= 2; // multiple the number by itself
// If the number is now over 9 then we'll minus 9 from the number
if (digits[i] > 9) {
digits[i] -= 9;
}
}
// Add this digit onto the current total sum
sum += digits[i];
// Alternate
alt = !alt;
}
// The individual card numbers (after the above algorithm is applied and then when added together)
// should be possible to divide by 10 with zero left over
if (sum % 10 == 0) {
return true;
} else {
return false;
}
}
/*
The following regex was actually borrowed from The Regular Expression Cookbook (co-written by the regex legend @stevenlevithan)
*/
if (!/^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|6(?:011|5[0-9][0-9])[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\d{3})\d{11})$/g.test(field.value)) {
errors.push(field);
} else {
// If the card number appears to be valid we then run the Luhn test
if (!luhn(field.value)) {
errors.push(field);
}
}
},
shortdate: function (field) {
/*
The regex allows the following formats:
00/0000
0/0000
00/00
0/00
*/
if (!/^\d{1,2}\/\d{2,4}$/.test(field.value) || !field.value.length) {
errors.push(field);
}
},
year: function (field) {
// The regex forces the year to be a 4 digit number
if (!/^\d{4}$/.test(field.value) || !field.value.length) {
errors.push(field);
}
},
checkbox: function (field) {
if (!field.checked) {
errors.push(field);
}
},
confirm: function (field, index) {
if (field.value.length && field.value !== fields[index].value) {
errors.push(field);
}
},
is_expiry_invalid: function (field) {
var split = field.value.split("/"),
month = split[0],
year = split[1],
paydate = new Date(window.mp_paymentdate[1], window.mp_paymentdate[0]-1, window.mp_paymentdate[2]),
expirydate = new Date("20"+year,month-1, window.mp_paymentdate[2]); // month is zero indexed
// We construct a date object based on the user's final pay date and their card's expiry date
// We then convert both dates into a number so we can compare the values
if (+expirydate <= +paydate) {
errors.push(field);
}
}
};
function display_errors(){
// First we'll remove any messages that are already on the page
var original_list = getElement("js-errorlist");
if (original_list) {
original_list.parentNode.removeChild(original_list);
}
var content,
// We need to make sure that all mandatory fields have a custom attribute "data-errormsg"
// Which will display the relevant message necessary if there is a problem
err = map(errors.reverse(), function (item, index, array) {
return {
// I don't like the idea of having to touch the DOM every time I need to get an error message but
// this seemed like the best way to temporarily access that data without using expando properties
// which can cause memory leaks in Internet Explorer
error: item.getAttribute("data-errormsg")
}
});
function generate_html(){
var frag = doc.createDocumentFragment(),
div = doc.createElement("div");
div.id = "js-errorlist";
// Insert 'confirmation' header into page above the Flash file
div.innerHTML = content;
frag.appendChild(div);
insert_errors.parentNode.insertBefore(frag, insert_errors);
window.location.hash = "js-errorlist";
}
// The following code prevents calling the server every time the form is submitted.
// Instead we retrieve the template and compile it once
if (template) {
content = template.render({
errors: err
});
generate_html();
} else {
ajax({
url: "Assets/Templates/FormErrors.tpl",
data: "html",
onSuccess: function (data) {
template = hogan.compile(data);
content = template.render({
errors: err
});
generate_html();
}
});
}
}
form.onsubmit = function(){
var len = fields.length,
field,
classname,
i;
errors = [];
// If the "Are you already a member" module still visible then hide it.
// No point in still showing it when we know they are trying to register
// Also it makes it less confusing for the user if we need to display an error message box,
// otherwise there would be two red boxes next to each other and the errors could be lost
// This element only appears on the Stage 1 page so we need to check that it exists and if it does then we check it's class attribute value
if (module && module.className.indexOf(" hide") === -1) {
module.className += " hide";
}
while(len--) {
field = fields[len];
classname = field.className;
i = classname.indexOf("mandatory");
if (i >= 0) {
// For the validation script to work we need to ensure a specific format is used in the HTML.
// The main principle is to make sure the last class on an input is either 'mandatory' or 'mandatory-xxxx'
// e.g. 'mandatory-number', 'mandatory-dob', 'mandatory-email', 'mandatory-mobile', 'mandatory-password'
if (classname.split("mandatory-")[1]) {
// If there is a second item then we know there is a specific type to validate against
formats[classname.split("mandatory-")[1]](field);
} else {
// Otherwise we validate by standard method
// e.g. if the length is zero (meaning the field is empty) then zero will coerce to false
// so we return the opposite of that using the ! operator (so the first part of the following condition is met)
// and if the length is greater than zero then the regex checks to see if the content isn't just empty spaces
if (/^\s+$/.test(field.value) || !field.value.length) {
errors.push(field);
}
}
}
if (formname === "createaccount-stage1" && field.name === "confirmemail") {
// If the email and confirm email values aren't the same then store as error
formats.confirm(field, len-1);
}
if (formname === "createaccount-stage2" && field.name === "password2") {
// If the password and confirm password values aren't the same then store as error
formats.confirm(field, len-1);
}
if (formname === "createaccount-stage2" && field.name === "question2") {
// If the 2nd question selected is the same as the 1st question field then store as error (we don't want user selecting same question twice)
if (field.value.length && field.value === fields[len-2].value) {
errors.push(field);
}
}
if (formname === "createaccount-stage4" && field.name === "expirydate") {
if (field.value.length) {
formats.is_expiry_invalid(field)
}
}
}
// Make sure there are no errors stored
// If errors.length is zero then that means there are no errors
// Zero will coerce to false and so we return the opposite of false using the ! operator
if (!errors.length) {
return true;
}
// We return false to prevent the form from submitting
// And we display the errors we found
else {
display_errors();
return false;
}
}
}
return validate;
});