Source: QfqForm.js

/**
                * @author Rafael Ostertag <rafael.ostertag@math.uzh.ch>
                */

                /* global $ */
                /* global EventEmitter */
                /* @depend QfqEvents.js */

                /**
                * Qfq Namespace
                *
                * @namespace QfqNS
                */
                var QfqNS = QfqNS || {};

                (function (n) {
                'use strict';

                // TODO: This object is getting too big. Start refactoring.
                /**
                * Represents a QFQ Form.
                *
                * @param formId {string} value of the form's id attribute
                * @param submitTo {string} url where data will be submitted to
                * @param deleteUrl {string} url to call upon record deletion
                * @param dataRefreshUrl {string} url where to fetch new element values from
                * @param fileUploadTo {string} url used for file uploads
                * @constructor
                *
                * @name QfqNS.QfqForm
                */
                n.QfqForm = function (formId, submitTo, deleteUrl, dataRefreshUrl, fileUploadTo, fileDeleteUrl) {
                this.formId = formId;
                this.submitTo = submitTo;
                this.deleteUrl = deleteUrl;
                this.dataRefreshUrl = dataRefreshUrl;
                this.fileUploadTo = fileUploadTo;
                this.fileDeleteUrl = fileDeleteUrl;

                this.form = new n.Form(this.formId);
                // This is required when displaying validation messages, in to activate the tab, which has validation
                issues
                this.bsTabs = null;
                this.lastButtonPress = null;
                this.eventEmitter = new EventEmitter();

                this.getSaveButton().addClass("disabled").attr("disabled", "disabled");

                this.form.on('form.changed', this.changeHandler.bind(this));
                this.form.on('form.reset', this.resetHandler.bind(this));
                this.form.on('form.submit.successful', this.submitSuccessDispatcher.bind(this));
                this.form.on('form.submit.failed', function (obj) {
                n.Helper.showAjaxError(null, obj.textStatus, obj.errorThrown);
                });

                this.getSaveButton().click(this.handleSaveClick.bind(this));
                this.getCloseButton().click(this.handleCloseClick.bind(this));
                this.getNewButton().click(this.handleNewClick.bind(this));
                this.getDeleteButton().click(this.handleDeleteClick.bind(this));

                this.setupFormUpdateHandler();
                this.setupEnterKeyHandler();

                this.fileUploader = new n.FileUpload('#' + this.formId, this.fileUploadTo);
                this.fileUploader.on('fileupload.started', this.startUploadHandler);
                this.fileUploader.on('fileupload.upload.successful', this.fileUploadSuccessHandler);

                this.fileUploader.on('fileupload.upload.failed',
                function (obj) {
                n.Helper.showAjaxError(null, obj.textStatus, obj.errorThrown);
                });
                this.fileUploader.on('fileupload.ended', this.endUploadHandler);

                this.fileDeleter = new n.FileDelete("#" + this.formId, this.fileDeleteUrl);
                this.fileDeleter.on('filedelete.delete.successful', this.fileDeleteSuccessHandler.bind(this));

                this.fileDeleter.on('filedelete.delete.failed',
                function (obj) {
                n.Helper.showAjaxError(null, obj.textStatus, obj.errorThrown);
                });

                var configurationData = this.readElementConfigurationData();

                this.applyElementConfiguration(configurationData);

                // Initialize jqxDateTimeInput elements.
                n.Helper.jqxDateTimeInput();
                // Initialize jqxComboBox elements.
                n.Helper.jqxComboBox();
                };

                n.QfqForm.prototype.on = n.EventEmitter.onMixin;

                /**
                * @private
                */
                n.QfqForm.prototype.setupEnterKeyHandler = function () {
                $("input").keyup(function (event) {
                if (event.which === 13) {
                if (this.form.formChanged) {
                this.lastButtonPress = "save&close";
                n.Log.debug("save&close click");
                this.submit();
                }
                event.preventDefault();
                }
                }.bind(this));
                };


                /**
                *
                * @private
                */
                n.QfqForm.prototype.readElementConfigurationData = function () {
                var $configuredElements = $("#" + this.formId + " [data-hidden],#" + this.formId + " [data-disabled],#"
                + this.formId + " [data-required]");

                var configurationArray = [];
                $configuredElements.each(function (index, element) {
                try {
                var $element = $(element);
                if (!element.hasAttribute("name")) {
                n.Log.warning("Element has configuration data, but no name. Skipping");
                return;
                }

                var configuration = {};
                configuration['form-element'] = $element.attr('name');

                var hiddenVal = $element.data('hidden');
                if (hiddenVal !== undefined) {
                configuration.hidden = n.Helper.stringToBool(hiddenVal);
                }

                var disabledVal = $element.data('disabled');
                if (disabledVal !== undefined) {
                configuration.disabled = n.Helper.stringToBool(disabledVal);
                }

                var requiredVal = $element.data("required");
                if (requiredVal !== undefined) {
                configuration.required = n.Helper.stringToBool(requiredVal);
                }

                configurationArray.push(configuration);
                } catch (e) {
                n.Log.error(e.message);
                }
                });

                return configurationArray;

                };

                /**
                * @public
                * @param bsTabs
                */
                n.QfqForm.prototype.setBsTabs = function (bsTabs) {
                this.bsTabs = bsTabs;
                };

                /**
                * @private
                */
                n.QfqForm.prototype.fileDeleteSuccessHandler = function (obj) {
                if (!obj.data.status) {
                throw Error("Response on file upload missing status");
                }

                if (obj.data.status === "error") {
                var alert = new n.Alert(obj.data.message, "error");
                alert.show();
                return;
                }

                var $button = $(obj.target);
                $button.prop("disabled", true);

                var $buttonParent = $button.parent();
                $buttonParent.addClass('hidden');

                var $inputFile = $buttonParent.siblings(':file');
                $inputFile.prop("disabled", false);
                $inputFile.removeClass('hidden');

                $inputFile.val("");

                this.form.markChanged();
                };

                /**
                * @private
                */
                n.QfqForm.prototype.fileUploadSuccessHandler = function (obj) {
                if (!obj.data.status) {
                throw Error("Response on file upload missing status");
                }

                if (obj.data.status === "error") {
                var alert = new n.Alert(obj.data.message, "error");
                alert.show();
                return;
                }

                var $fileInput = $(obj.target);
                $fileInput.prop("disabled", true);
                $fileInput.addClass("hidden");

                var $deleteContainer = $fileInput.siblings('div.uploaded-file');


                var fileNamesString = obj.target.files[0].name;
                var $fileNameSpan = $deleteContainer.find("span.uploaded-file-name");
                $fileNameSpan.empty().append(fileNamesString);

                var $deleteButton = $deleteContainer.find("button");
                $deleteButton.prop("disabled", false);

                $deleteContainer.removeClass("hidden");
                };

                /**
                *
                * @param $button
                * @param enabled {boolean}
                *
                * @private
                */
                n.QfqForm.prototype.setButtonEnabled = function ($button, enabled) {
                if (!$button) {
                n.Log.error("QfqForm#setButtonEnabled(): no button provided.");
                return;
                }
                if (!enabled) {
                $button.addClass("disabled");
                $button.attr("disabled", "disabled");
                } else {
                $button.removeClass("disabled");
                $button.removeAttr("disabled");
                }
                };

                n.QfqForm.prototype.setupFormUpdateHandler = function () {
                $('input[data-load],select[data-load]').on('change', this.formUpdateHandler.bind(this));
                };

                n.QfqForm.prototype.formUpdateHandler = function () {
                var that = this;
                $.post(this.dataRefreshUrl, this.form.serialize(), "json")
                .fail(n.Helper.showAjaxError)
                .done(function (data) {
                this.handleFormUpdate(data);
                }.bind(that));

                };

                n.QfqForm.prototype.handleFormUpdate = function (data) {
                if (!data.status) {
                throw new Error("Expected 'status' attribute to be present.");
                }

                if (data.status === "error") {
                var alert = new n.Alert("Error while updating form:<br>" + (data.message ? data.message : "No reason"
                +
                " given"), "error");
                alert.show();
                return;
                }

                if (data.status === "success") {
                if (!data['form-update']) {
                throw new Error("'form-update' attribute missing in form update data");
                }


                this.applyElementConfiguration(data['form-update']);
                return;
                }

                throw new Error("Unexpected status: '" + data.status + "'");
                };

                /**
                * @private
                */
                n.QfqForm.prototype.destroyFormAndSetText = function (text) {
                this.form = null;
                $('#' + this.formId).replaceWith($("<p>").append(text));
                this.eventEmitter.emitEvent('qfqform.destroyed', n.EventEmitter.makePayload(this, null));
                };

                /**
                * @private
                */
                n.QfqForm.prototype.handleSaveClick = function () {
                this.lastButtonPress = "save";
                n.Log.debug("save click");
                this.submit();
                };

                /**
                * @private
                */
                n.QfqForm.prototype.handleCloseClick = function () {
                this.lastButtonPress = "close";
                if (this.form.getFormChanged()) {
                var alert = new n.Alert({
                message: "You have unsaved changes. Do you want to save first?",
                type: "warning",
                modal: true,
                buttons: [
                {label: "Yes", eventName: "yes"},
                {label: "No", eventName: "no", focus: true},
                {label: "Cancel", eventName: "cancel"}
                ]
                });
                var that = this;
                alert.on('alert.yes', function () {
                that.submit();
                });
                alert.on('alert.no', function () {
                that.eventEmitter.emitEvent('qfqform.close-intentional', n.EventEmitter.makePayload(that, null));

                window.history.back();
                });
                alert.show();
                } else {
                window.history.back();
                }
                };

                n.QfqForm.prototype.submit = function () {
                if (this.form.validate() !== true) {

                this.form.$form.validator('validate');

                var element = this.form.getFirstNonValidElement();
                if (element.hasAttribute('name')) {
                var tabId = this.bsTabs.getContainingTabIdForFormControl(element.getAttribute('name'));
                if (tabId) {
                this.bsTabs.activateTab(tabId);
                }
                }

                var alert = new n.Alert("Form is incomplete.", "warning");
                alert.show();
                return;
                }
                // First, remove all validation states, in case a previous submit has set a validation state, thus we're
                not
                // stockpiling them.
                this.clearAllValidationStates();
                this.form.submitTo(this.submitTo);
                };

                /**
                * @private
                */
                n.QfqForm.prototype.handleNewClick = function (event) {
                event.preventDefault();

                this.lastButtonPress = "new";
                if (this.form.getFormChanged()) {
                var alert = new n.Alert({
                message: "You have unsaved changes. Do you want to save first?",
                type: "warning",
                modal: true,
                buttons:
                [
                {label: "Yes", eventName: "yes", focus: true},
                {label: "No", eventName: "no"},
                {label: "Cancel", eventName: "cancel"}
                ]
                });
                var that = this;
                alert.on('alert.no', function () {
                that.eventEmitter.emitEvent('qfqform.close-intentional', n.EventEmitter.makePayload(that, null));

                var anchorTarget = that.getNewButtonTarget();
                window.location = anchorTarget;
                });
                alert.on('alert.yes', function () {
                that.submit();
                });
                alert.show();
                } else {
                var anchorTarget = this.getNewButtonTarget();
                window.location = anchorTarget;
                }
                n.Log.debug("new click");
                };

                /**
                * @private
                */
                n.QfqForm.prototype.handleDeleteClick = function () {
                this.lastButtonPress = "delete";
                n.Log.debug("delete click");
                var alert = new n.Alert({
                message: "Do you really want to delete the record?",
                type: "warning",
                modal: true,
                buttons: [
                {label: "Yes", eventName: "ok"},
                {label: "No", eventName: "cancel", focus: true}
                ]
                });
                var that = this;
                alert.on('alert.ok', function () {
                $.post(that.deleteUrl)
                .done(that.ajaxDeleteSuccessDispatcher.bind(that))
                .fail(n.Helper.showAjaxError);
                });
                alert.show();
                };

                /**
                *
                * @param data
                * @param textStatus
                * @param jqXHR
                *
                * @private
                */
                n.QfqForm.prototype.ajaxDeleteSuccessDispatcher = function (data, textStatus, jqXHR) {
                if (!data.status) {
                throw new Error("No 'status' property 'data'");
                }

                switch (data.status) {
                case "error":
                this.handleLogicDeleteError(data);
                break;
                case "success":
                this.handleDeleteSuccess(data);
                break;
                default:
                throw new Error("Status '" + data.status + "' unknown.");
                }
                };

                /**
                *
                * @param data
                *
                * @private
                */
                n.QfqForm.prototype.handleDeleteSuccess = function (data) {
                this.setButtonEnabled(this.getCloseButton(), false);
                this.setButtonEnabled(this.getDeleteButton(), false);
                this.setButtonEnabled(this.getSaveButton(), false);
                this.setButtonEnabled(this.getNewButton(), false);
                this.destroyFormAndSetText("Record has been deleted!");

                if (!data.redirect || data.redirect === "client") {
                window.history.back();
                return;
                }

                if (data.redirect === "no") {
                var alert = new n.Alert("redirect=='no' not allowed", "error");
                alert.show();
                return;
                }

                if (data.redirect === "url" || data['redirect-url']) {
                window.location = data['redirect-url'];
                return;
                }
                };

                /**
                *
                * @param data
                *
                * @private
                */
                n.QfqForm.prototype.handleLogicDeleteError = function (data) {
                if (!data.message) {
                throw Error("Status is 'error' but required 'message' attribute is missing.");
                }
                var alert = new n.Alert(data.message, "error");
                alert.show();
                };

                /**
                *
                * @param form {n.QfqForm}
                *
                * @private
                */
                n.QfqForm.prototype.changeHandler = function (obj) {
                this.getSaveButton().removeClass("disabled");
                this.getSaveButton().addClass("alert-warning");
                this.getSaveButton().removeAttr("disabled");
                };

                /**
                *
                * @param form {n.QfqForm}
                *
                * @private
                */
                n.QfqForm.prototype.resetHandler = function (obj) {
                this.getSaveButton().removeClass("alert-warning");
                this.getSaveButton().addClass("disabled");
                this.getSaveButton().attr("disabled", "disabled");
                };

                /**
                *
                * @returns {jQuery|HTMLElement}
                *
                * @private
                */
                n.QfqForm.prototype.getSaveButton = function () {
                return $("#save-button");
                };

                /**
                *
                * @returns {jQuery|HTMLElement}
                *
                * @private
                */
                n.QfqForm.prototype.getCloseButton = function () {
                return $("#close-button");
                };

                /**
                *
                * @returns {jQuery|HTMLElement}
                *
                * @private
                */
                n.QfqForm.prototype.getDeleteButton = function () {
                return $("#delete-button");
                };

                /**
                *
                * @returns {jQuery|HTMLElement}
                *
                * @private
                */
                n.QfqForm.prototype.getNewButton = function () {
                return $("#form-new-button");
                };


                /**
                * @private
                */
                n.QfqForm.prototype.submitSuccessDispatcher = function (obj) {
                if (!obj.data.status) {
                throw new Error("No 'status' property in 'data'");
                }

                switch (obj.data.status) {
                case "error":
                this.handleLogicSubmitError(obj.target, obj.data);
                break;
                case "success":
                this.handleSubmitSuccess(obj.target, obj.data);
                break;
                default:
                throw new Error("Status '" + obj.data.status + "' unknown.");
                }

                };

                /**
                *
                * @param form
                * @param data
                *
                * @private
                */
                n.QfqForm.prototype.handleLogicSubmitError = function (form, data) {
                if (!data.message) {
                throw Error("Status is 'error' but required 'message' attribute is missing.");
                }
                var alert = new n.Alert(data.message, "error");
                alert.show();

                if (data["field-name"] && this.bsTabs) {
                var tabId = this.bsTabs.getContainingTabIdForFormControl(data["field-name"]);
                if (tabId) {
                this.bsTabs.activateTab(tabId);
                }

                this.setValidationState(data["field-name"], "error");
                this.setHelpBlockValidationMessage(data["field-name"], data["field-message"]);
                }
                };

                /**
                *
                * @param form
                * @param data
                *
                * @private
                */
                n.QfqForm.prototype.handleSubmitSuccess = function (form, data) {
                n.Log.debug('Reset form state');
                form.resetFormChanged();

                switch (this.lastButtonPress) {
                case 'save&close':
                window.history.back();
                break;
                case 'save':
                if (data.message) {
                var alert = new n.Alert(data.message);
                alert.timeout = 1500;
                alert.show();
                }

                // do we have to update the HTML Form?
                if (data['form-update']) {
                this.applyElementConfiguration(data['form-update']);
                }

                if (data.redirect === "url" || data['redirect-url']) {
                window.location = data['redirect-url'];
                return;
                }
                break;
                case 'close':
                if (!data.redirect || data.redirect === "no") {
                return;
                }

                if (data.redirect === "client") {
                window.history.back();
                return;
                }

                if (data.redirect === "url" || data['redirect-url']) {
                window.location = data['redirect-url'];
                return;
                }
                break;
                case 'new':
                var target = this.getNewButtonTarget();
                window.location = target;
                return;

                default:
                if (data.redirect === "client") {
                window.history.back();
                return;
                }


                if (data.redirect === "url" || data['redirect-url']) {
                window.location = data['redirect-url'];
                return;
                }
                break;
                }
                };

                n.QfqForm.prototype.getNewButtonTarget = function () {
                return $('#form-new-button').attr('href');
                };

                n.QfqForm.prototype.getFormGroupByControlName = function (formControlName) {
                var $formControl = $("[name='" + formControlName + "']");
                if ($formControl.length === 0) {
                n.Log.debug("QfqForm.setValidationState(): unable to find form control with name '" + formControlName +
                "'");
                return null;
                }

                var iterator = $formControl[0];
                while (iterator !== null) {
                var $iterator = $(iterator);
                if ($iterator.hasClass('form-group')) {
                return $iterator;
                }

                iterator = iterator.parentElement;
                }

                return null;
                };

                n.QfqForm.prototype.setValidationState = function (formControlName, state) {
                var $formGroup = this.getFormGroupByControlName(formControlName);
                if ($formGroup) {
                $formGroup.addClass("has-" + state);
                }
                };

                n.QfqForm.prototype.resetValidationState = function (formControlName) {
                var $formGroup = this.getFormGroupByControlName(formControlName);
                $formGroup.removeClass("has-warning");
                $formGroup.removeClass("has-error");
                $formGroup.removeClass("has-success");
                $formGroup.removeClass("has-danger");
                };


                n.QfqForm.prototype.clearAllValidationStates = function () {
                $('.has-warning,.has-error,.has-success,.has-danger').removeClass("has-warning has-error has-success" +
                " has-danger");
                $('[data-qfq=validation-message]').remove();
                };

                /**
                *
                * @param formControlName
                * @param text
                */
                n.QfqForm.prototype.setHelpBlockValidationMessage = function (formControlName, text) {
                /*
                * Why is this method here and not in FormGroup? Adding this particular method to FormGroup is easy,
                however
                * QfqForm.clearAllValidationStates() would not find its proper place in FormGroup, since FormGroup
                operates
                * on one element. We would end up having the responsibilities spread across several classes, which would
                be
                * confusing.
                */
                var $formGroup = this.getFormGroupByControlName(formControlName);
                if (!$formGroup) {
                return;
                }
                var $helpBlockColumn;
                var $formGroupSubDivs = $formGroup.find("div");
                if ($formGroupSubDivs.length < 3) {
                $helpBlockColumn = $("<div>").addClass("col-md-4");
                $formGroup.append($helpBlockColumn);
                } else {
                $helpBlockColumn = $($formGroupSubDivs[2]);
                }

                $helpBlockColumn.append(
                $("<p>")
                .addClass("help-block")
                .attr("data-qfq", "validation-message")
                .append(text)
                );
                };

                /**
                *
                * @param configuration {array} array of objects.
                */
                n.QfqForm.prototype.applyElementConfiguration = function (configuration) {
                var arrayLength = configuration.length;
                for (var i = 0; i < arrayLength; i++) {
                var configurationItem = configuration[i];
                var formElementName = configurationItem["form-element"];
                if (formElementName === undefined) {
                n.Log.error("configuration lacks 'form-element' attribute. Skipping.");
                continue;
                }
                try {
                var element = n.Element.getElement(formElementName);

                if (configurationItem.value !== undefined) {
                element.setValue(configurationItem.value);
                }

                if (configurationItem.readonly !== undefined) {
                // Readonly and disabled is the same in our domain
                element.setEnabled(!configurationItem.readonly);
                }

                if (configurationItem.disabled !== undefined) {
                // Readonly and disabled is the same in our domain
                element.setEnabled(!configurationItem.disabled);
                }

                if (configurationItem.hidden !== undefined) {
                element.setHidden(configurationItem.hidden);
                }

                if (configurationItem.required !== undefined) {
                element.setRequired(configuration.required);
                }
                } catch (e) {
                n.Log.error(e.message);
                }
                }
                };

                /**
                * @private
                * @param triggeredBy
                */
                n.QfqForm.prototype.startUploadHandler = function (obj) {
                $(obj.target).after(
                $('<i>').addClass('spinner')
                );
                };

                /**
                * @private
                * @param triggeredBy
                */
                n.QfqForm.prototype.endUploadHandler = function (obj) {
                var $siblings = $(obj.target).siblings();
                $siblings.filter("i").remove();
                };

                /**
                * Retrieve SIP as stored in hidden input field.
                *
                * @deprecated SIP should be passed via url or data attribute.
                * @returns {string} sip
                */
                n.QfqForm.prototype.getSip = function () {
                return $('#' + this.formId + ' input[name=s]').val();
                };

                /**
                * @public
                */
                n.QfqForm.prototype.isFormChanged = function () {
                return this.form.formChanged;
                };

                })(QfqNS);