﻿(function ($) {
    $.fn.betterForm = function (options) {
        // check that every element passed in is a form
        this.each(function () {
            if (this.nodeName !== "FORM") {
                throw new Error("BetterForm only works on forms");
            }
        });

        var settings = {
            dateFormat: "yy-m-d", // The date format to use (y - year (2 digit), yy - year (4 digit), m - month, d - date)
            timeFormat: "H:i:s.n", // The time format to use (h - hour in 12 hour format, H - hour in 24 hour format, i - minutes, s - seconds, n - milliseconds, a - am/pm indicator
            placeholderClass: "betterform_placeholder", // CSS class used for placeholders
            errorMessageClass: "betterform_error", // CSS class to use for validation error messages
            invalidFieldClass: "betterform_invalid", // CSS class to use for invalid fields after validation
            // Error messages
            requiredError: "This field is required",
            invalidDateError: "Please enter a valid date"
        };
        if (options) {
            $.extend(settings, options);
        }

        // Creates a span with the error message
        var createMessage = function (message) {
            var message = '<span class="' + settings.errorMessageClass + '">' + message + '</span>';
            return message;
        };

        // Check if field has a value filled in or if checkbox is checked or if any of radio buttons in a set are checked
        var hasValue = function (field) {
            var placeholder = field.attr("placeholder");
            if (placeholder == null) {
                placeholder = "";
            }
            var good = false;
            switch (field.attr("type")) {
                case "checkbox":
                    good = field.attr("checked");
                    break;
                case "radio":
                    field.parents("form").find("input[name=" + field.attr("name") + "]").each(function () {
                        if (this.checked) {
                            good = true;
                            return false; // stop the foreach
                        }
                    });
                    break;
                default:
                    good = field.val().length > 0 && field.val() != placeholder;
                    break;
            }
            return good;
        };

        // Returns the field after which we put the error message. Usually it's the field getting checked, unless it's a checkbox or radio button
        // In case of checkbox, it's the surrounding label, or the label right after the checkbox, or the checkbox itself
        // In case of radio button, it's the surrounding label, or the label right after, or the radio button which is the last of the set
        var getMessageField = function (field) {
            var messageField = field;
            switch (field.attr("type")) {
                case "checkbox":
                    if (field.parent("label").length > 0) {
                        messageField = field.parent();
                    }
                    else if (field.next("label").length > 0) {
                        messageField = field.next();
                    }
                    break;
                case "radio":
                    var f = field.parents("form").find("input[name=" + field.attr("name") + "]:last");
                    if (f.parent("label").length > 0) {
                        messageField = f.parent();
                    }
                    else if (f.next("label").length > 0) {
                        messageField = f.next();
                    }
                    else {
                        messageField = f;
                    }
                    break;
            }
            return messageField;
        };

        // Marks field as valid, removes the invalid class and the error message. Marks all radio buttons in the set as valid unless forValidation is true
        var markFieldValid = function (field, forValidation) {
            var fields = field;
            if (!forValidation && field.attr("type") == "radio") {
                fields = field.parents("form").find("input[name=" + field.attr("name") + "]");
            }
            $(fields).each(function () {
                $this = $(this);
                var messageField = getMessageField($this);
                messageField.next("." + settings.errorMessageClass).remove();
                $this.removeClass(settings.invalidFieldClass);
            });
        };

        // Marks field as invalid, adds invalid class, and puts an error message unless it's already there
        var markFieldInvalid = function (field, message) {
            field.addClass(settings.invalidFieldClass);
            var messageField = getMessageField(field);

            if (messageField.next("." + settings.errorMessageClass).length == 0) {
                messageField.after(createMessage(message));
            }
        };

        var isNumeric = function (input) {
            return (input - 0) == input && input.length > 0;
        };

        var validateDate = function (field) {

            var dateStringPosition = 0;
            var dateString = field.val();
            var year = "", month = "", date = "";
            for (var i = 0; i < settings.dateFormat.length; i++) {
                switch (settings.dateFormat.charAt(i)) {
                    case "y":
                        var int = dateString.substr(dateStringPosition, 2);
                        if (!isNumeric(int)) {
                            return false;
                        }
                        year += int;
                        dateStringPosition += 2;
                        break;
                    case "m":
                        var int = dateString.substr(dateStringPosition, 2);
                        if (!isNumeric(int)) {
                            int = dateString.charAt(dateStringPosition);
                            if (!isNumeric(int)) {
                                return false;
                            }
                            dateStringPosition++;
                        }
                        else {
                            dateStringPosition += 2;
                        }
                        month += int;
                        break;
                    case "d":
                        var int = dateString.substr(dateStringPosition, 2);
                        if (!isNumeric(int)) {
                            int = dateString.charAt(dateStringPosition);
                            if (!isNumeric(int)) {
                                return false;
                            }
                            dateStringPosition++;
                        }
                        else {
                            dateStringPosition += 2;
                        }
                        date += int;
                        break;
                    default:
                        var char = settings.dateFormat.charAt(i);
                        if (dateString.charAt(dateStringPosition) != char) {
                            return false;
                        }
                        dateStringPosition++;
                        break;
                }
            }
            if (dateString.length > dateStringPosition) {
                return false;
            }

            var d = new Date();
            d.setFullYear(year, month - 1, date);
            return year == d.getFullYear() && month == d.getMonth() + 1 && date == d.getDate();
        };

        // Validates a field
        var validateField = function (field) {
            markFieldValid(field, true);
            if (field.attr("required") != null) {
                if (!hasValue(field)) {
                    markFieldInvalid(field, settings.requiredError);
                    return false;
                }
            }
            switch (field.attr("type")) {
                case "date":
                    if (!validateDate(field)) {
                        markFieldInvalid(field, settings.invalidDateError);
                        return false;
                    }
                    break;
            }
            return true;
        };

        // Sets placeholders on fields
        var setPlaceholder = function (field) {
            var placeholder = field.attr("placeholder");
            if (placeholder != null) {
                if (placeholder.length > 0 && field.val().length == 0) {
                    field.val(placeholder);
                    field.addClass(settings.placeholderClass);
                }
                field.focus(function () {
                    var $this = $(this);
                    if ($this.val() === placeholder) {
                        $this.val("");
                        $this.removeClass(settings.placeholderClass);
                    }
                });
                field.blur(function () {
                    var $this = $(this);
                    if ($this.val().length === 0) {
                        $this.val(placeholder);
                        $this.addClass(settings.placeholderClass);
                    }
                });
            }
        };

        return this.each(function () {
            var $this = $(this);
            // Placeholder
            $this.find("input[type!=password], textarea").each(function () {
                setPlaceholder($(this));
            });

            // Validation
            if (!$this.attr("novalidate")) {
                $this.attr("novalidate", true);
                $this.find("input, textarea, select").each(function () {
                    var $this = $(this);
                    var origValue = "";
                    $this.focus(function () {
                        origValue = $this.val();
                    });
                    // If value changes by typing something, or on change event or by clicking if clicking cause value change (spinboxes, etc)
                    $this.keyup(function () {
                        if ($this.val() != origValue) {
                            markFieldValid($this);
                        }
                    });
                    $this.change(function () {
                        markFieldValid($this);
                    });
                    $this.mouseup(function () {
                        if ($this.val() != origValue) {
                            markFieldValid($this);
                        }
                    });
                });
                // On submit of the form, validate all fields
                $this.bind("submit", function (event) {
                    var invalidFields = 0;
                    $this.find("input, textarea, select").each(function () {
                        var $this = $(this);
                        if (!validateField($this)) {
                            invalidFields++;
                        }
                    });
                    return invalidFields === 0;
                });
            }
        });
    };
})(jQuery);
