Source: form.js

/* global app, Lang */

'use strict';

define('form', ['jquery'], function($) {
    /**
     * Class FormInput, represents any input in a form
     *
     * @class  FormInput
     * @param {Object} field The input parameters
     * @param {Form} form The form the input is asssociated with
     **/
    var FormInput = function(field, form) {
        this.form = form;
        for (var key in field) {
            if (field.hasOwnProperty(key)) {
                this[key] = field[key];
            }
        }

        this.node = $('[id=\'' + this.id + '\']');

        if (this.type === 'submit') {
            this.node.click(function() {
                // Ask for confirmation
                if (this.name === 'delete' && !confirm(Lang.get('form.confirm-delete'))) {
                    // The user finally doesn't want to delete the record
                    return false;
                }

                // The user confirmed
                this.form.setObjectAction(this.name);

                return true;
            }.bind(this));
        }
    };


    /**
     * Get or set the value of the field
     *
     * @memberOf FormInput
     * @param {string} value If this variable is set, it will be set to the input
     * @returns {string} The input value or the value that has been set
     */
    FormInput.prototype.val = function(value) {
        if (value === undefined) {
            // Get the input value
            switch (this.type) {
                case 'checkbox' :
                    return this.node.prop('checked');

                case 'radio' :
                    return this.node.find(':checked').val();

                case 'html' :
                    return this.node.html();

                default :
                    return this.node.val();
            }
        }
        else {
            switch (this.type) {
                case 'checkbox' :
                    this.node.prop('checked', value);
                    break;

                case 'radio' :
                    this.node.find('[value="' + value + '"]').prop('checked', true);
                    break;

                case 'html' :
                    this.node.html(value);
                    break;

                default :
                    this.node.val(value);
                    break;
            }
        }

        return value;
    };


    /**
     * Get a property data of the field
     *
     * @memberOf FormInput
     * @param {string} prop - the property to get the data value
     * @returns {styring} The value of the property
     */
    FormInput.prototype.data = function(prop) {
        return this.node.data(prop);
    };


    /**
     * Check the value of the field is valid
     *
     * @memberOf FormInput
     * @returns {bool} True if the field is valid, false else
     */
    FormInput.prototype.isValid = function() {
        // If the field is required, the field can't be empty
        if (this.required) {
            var emptyValue = this.emptyValue || '';

            if (this.val() === emptyValue) {
                this.addError(Lang.get('form.required-field'));
                return false;
            }
        }

        // If the field has a specific pattern, test the value with this pattern
        if (this.pattern) {
            var regex = new RegExp(this.pattern.substr(1, -1));

            if (this.val() && !regex.test(this.val())) {
                this.addError(Lang.exists('form.' + this.type + '-format') ?
                    Lang.get('form.' + this.type + '-format') :
                    Lang.get('form.field-format')
                );
                return false;
            }
        }

        if (this.minimum) {
            if (this.val() && this.val() < this.minimum) {
                this.addError(Lang.get('form.number-minimum', {value: this.minimum}));
                return false;
            }
        }

        if (this.maximum) {
            if (this.val() && this.val() > this.maximum) {
                this.addError(Lang.get('form.number-maximum', {value: this.maximum}));
                return false;
            }
        }

        // If the field has to be compared with another one, compare the two values
        if (this.compare) {
            if (this.val() !== this.form.inputs[this.compare].val()) {
                this.addError(Lang.get('form.' + this.type + '-comparison'));
                return false;
            }
        }

        return true;
    };


    /**
     * Display an error on the input
     *
     * @memberOf FormInput
     * @param {string} text The error message to set to the input
     */
    FormInput.prototype.addError = function(text) {
        if (this.errorAt) {
            this.form.inputs[this.errorAt].addError(text);
        }
        else {
            this.node.addClass('error').after('<span class="input-error-message">' + text + '</span>');
        }
    };

    /**
     * Remove the errors on the input
     *
     * @memberOf FormInput
     */
    FormInput.prototype.removeError = function() {
        this.node.removeClass('error').next('.input-error-message').remove();
    };



    /**
     * This class is used to validate and submit forms client side.
     * forms are accessible to window by app.formrs[id]
     *
     * @class Form
     * @param {string} id - the id of the form
     * @param {Object} fields - The list of all fields in the form
     */
    var Form = function(id, fields) {
        this.id = id;
        this.node = $('[id=\'' + this.id + '\']');
        this.upload = this.node.hasClass('upload-form');
        this.action = this.node.attr('action');
        this.method = this.node.attr('method').toLowerCase();
        this.inputs = {};

        for (var name in fields) {
            if (fields.hasOwnProperty(name)) {
                this.inputs[name] = new FormInput(fields[name], this);
            }
        }

        // Listen for form submission
        this.node.submit(function() {
            this.submit();

            return false;
        }.bind(this));

        // Listen for form change
        this.onchange = null;
        this.node.change(function(event) {
            if (this.onchange) {
                this.onchange.call(this, event);
            }
        }.bind(this));
    };



    /**
     * Check the dat of the form
     *
     * @memberOf Form
     * @returns {bool} - true if the form data is correct, false else
     */
    Form.prototype.isValid = function() {
        var valid = true;

        this.removeErrors();
        for (var name in this.inputs) {
            if (!this.inputs[name].isValid()) {
                valid = false;
            }
        }

        return valid;
    };


    /**
     * Remove all the form errors
     *
     * @memberOf Form
     */
    Form.prototype.removeErrors = function() {
        this.node.find('.form-result-message').removeClass('alert alert-danger').text('');
        for (var name in this.inputs) {
            if (this.inputs.hasOwnProperty(name)) {
                this.inputs[name].removeError();
            }
        }
    };


    /**
     * Display an error message to the form
     *
     * @memberOf Form
     * @param  {string} text The message to display
     */
    Form.prototype.displayErrorMessage = function(text) {
        this.node.find('.form-result-message')
            .addClass('alert alert-danger')
            .html('<i class=\'icon icon-exclamation-circle\'></i>  ' + text);
    };


    /**
     * Display the errors on the form inputs
     *
     * @memberOf Form
     * @param  {Object} errors The errors to display, where keys are inputs names, and values the error messages
     */
    Form.prototype.displayErrors = function(errors) {
        if (typeof errors === 'object' && !(errors instanceof Array)) {
            for (var id in errors) {
                if (errors.hasOwnProperty(id)) {
                    this.inputs[id].addError(errors[id]);
                }
            }
        }
    };


    /**
     * Set the object action of the form. The object action can be "register" or "delete",
     * and represents the action that will be performed server side
     *
     * @memberOf Form
     * @param {string} action - The action value to set
     */
    Form.prototype.setObjectAction = function(action) {
        if (action.toLowerCase() === 'delete') {
            this.method = action;
        }
    };


    /**
     * Submit the form
     *
     * @returns {boolean} False
     * @memberOf Form
     */
    Form.prototype.submit = function() {
        // Remove all Errors on this form
        this.removeErrors();

        if (this.objectAction === 'delete' || this.isValid()) {
            app.loading.start();

            // Send an Ajax request to submit the form
            var data;

            if (this.method === 'get') {
                data = $(this.node).serlialize();
            }
            else {
                data = new FormData(this.node.get(0));
            }

            var options = {
                xhr : app.xhr,
                url : this.action,
                type : this.method,
                dataType : 'json',
                data : data,
                processData : false,
                contentType : false
            };

            $.ajax(options)

            .done(function(results) {
                // treat the response
                if (results.message) {
                    app.notify('success', results.message);
                }

                // Trigger a form_success event to the form
                if (this.onsuccess) {
                    this.onsuccess(results.data);
                }
            }.bind(this))

            .fail(function(xhr) {
                if (!xhr.responseJSON) {
                    // The returned result is not a JSON
                    this.displayErrorMessage(xhr.responseText);
                }
                else {
                    var response = xhr.responseJSON;

                    switch (xhr.status) {
                        case 412 :
                            // The form has not been checked correctly
                            this.displayErrorMessage(response.message);
                            this.displayErrors(response.errors);
                            break;

                        case 424 :
                            // An error occured in the form treatment
                            this.displayErrorMessage(response.message);
                            break;

                        default :
                            this.displayErrorMessage(Lang.get('main.technical-error'));
                            break;
                    }

                    if (this.onerror) {
                        this.onerror(response.data);
                    }
                }
            }.bind(this))

            .always(function() {
                app.loading.stop();
            });
        }
        else {
            this.displayErrorMessage(Lang.get('form.error-fill'));
        }

        return false;
    };


    /**
     * Reset the form values
     *
     * @memberOf Form
     */
    Form.prototype.reset = function() {
        this.node.get(0).reset();
    };


    /**
     * Get the form data as Object
     *
     * @memberOf Form
     * @returns {Object} The object containing the form inputs data
     */
    Form.prototype.valueOf = function() {
        var result = {};

        for (var name in this.inputs) {
            if (this.inputs.hasOwnProperty(name)) {
                var item = this.inputs[name],
                    matches = (/^(.+?)((?:\[(.*?)\])+)$/).exec(name);

                if (matches !== null) {
                    var params = matches[2];

                    if (!result[matches[1]]) {
                        result[matches[1]] = {};
                    }

                    var tmp = result[matches[1]],
                        m;

                    do {
                        m = (/^(\[(.*?)\])(\[(.*?)\])?/).exec(params);

                        if (m !== null) {
                            if (m[3]) {
                                if (!tmp[m[2]]) {
                                    tmp[m[2]] = m[4] ? {} : [];
                                }
                                tmp = tmp[m[2]];
                                params = m[3];
                            }
                            else if (tmp instanceof Array) {
                                tmp.push(item.val());
                            }
                            else {
                                tmp[m[2]] = item.val();
                            }
                        }
                    } while (m && m[3]);
                }
                else {
                    result[name] = item.val();
                }
            }
        }

        return result;
    };


    /**
     * Display the content of the form
     *
     * @memberOf Form
     * @returns {string} The JSON representing the form inputs data
     */
    Form.prototype.toString = function() {
        return JSON.stringify(this.valueOf());
    };

    return Form;
});