Skip to content
Snippets Groups Projects
QfqForm.js 20.4 KiB
Newer Older
/**
 * @author Rafael Ostertag <rafael.ostertag@math.uzh.ch>
 */

/* global $ */
/* @depend QfqEvents.js */
var QfqNS = QfqNS || {};

(function (n) {
    'use strict';

Rafael Ostertag's avatar
Rafael Ostertag committed
    // TODO: This object is getting too big. Start refactoring.
Rafael Ostertag's avatar
Rafael Ostertag committed
    /**
     * 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
     */
    n.QfqForm = function (formId, submitTo, deleteUrl, dataRefreshUrl, fileUploadTo, fileDeleteUrl) {
        this.formId = formId;
Rafael Ostertag's avatar
Rafael Ostertag committed
        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
Rafael Ostertag's avatar
Rafael Ostertag committed
        this.bsTabs = null;
Carsten  Rose's avatar
Carsten Rose committed
        this.lastButtonPress = null;
Rafael Ostertag's avatar
Rafael Ostertag committed
        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);
        });
Rafael Ostertag's avatar
Rafael Ostertag committed

        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.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);
Rafael Ostertag's avatar
Rafael Ostertag committed
            });
        this.fileUploader.on('fileupload.ended', this.endUploadHandler);
        this.fileDeleter = new n.FileDelete("#" + this.formId, this.fileDeleteUrl);
        this.fileDeleter.on('filedelete.delete.successful', this.fileDeleteSuccessHandler);

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

Rafael Ostertag's avatar
Rafael Ostertag committed
    /**
     * @public
     * @param bsTabs
     */
Rafael Ostertag's avatar
Rafael Ostertag committed
    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("");
    };

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

        if (obj.data.status === "error") {
            var alert = new n.Alert(obj.data.message, "error");
Rafael Ostertag's avatar
Rafael Ostertag committed
            alert.show();
            return;
Rafael Ostertag's avatar
Rafael Ostertag committed
        }

        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");
Rafael Ostertag's avatar
Rafael Ostertag committed
    };

    /**
     *
     * @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 () {
Carsten  Rose's avatar
Carsten Rose committed
        $('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)
    n.QfqForm.prototype.handleFormUpdate = function (data) {
        if (!data.status) {
            throw new Error("Expected 'status' attribute to be present.");
        }

        if (data.status === "error") {
Rafael Ostertag's avatar
Rafael Ostertag committed
            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));
Rafael Ostertag's avatar
Rafael Ostertag committed
    /**
     * @private
     */
    n.QfqForm.prototype.handleSaveClick = function () {
Carsten  Rose's avatar
Carsten Rose committed
        this.lastButtonPress = "save";
Rafael Ostertag's avatar
Rafael Ostertag committed
        n.Log.debug("save click");
Rafael Ostertag's avatar
Rafael Ostertag committed
        // First, remove all validation states, in case a previous submit has set a validation state, thus we're not
        // stockpiling them.
        this.clearAllValidationStates();
        this.submit();
Rafael Ostertag's avatar
Rafael Ostertag committed
    };

    /**
     * @private
     */
    n.QfqForm.prototype.handleCloseClick = function () {
Carsten  Rose's avatar
Carsten Rose committed
        this.lastButtonPress = "close";
Rafael Ostertag's avatar
Rafael Ostertag committed
        if (this.form.getFormChanged()) {
Rafael Ostertag's avatar
Rafael Ostertag committed
            var alert = new n.Alert("You have unsaved changes. Do you want to close?", "warning", "yesnosave");
Rafael Ostertag's avatar
Rafael Ostertag committed
            var that = this;
                that.submit();
Rafael Ostertag's avatar
Rafael Ostertag committed
            });
Rafael Ostertag's avatar
Rafael Ostertag committed
                window.history.back();
            });
            alert.show();
        } else {
            window.history.back();
        }
    };

    n.QfqForm.prototype.submit = function () {
            var alert = new n.Alert("Form is incomplete.", "warning");
            alert.show();
            return;
        }
        this.form.submitTo(this.submitTo);
    };

Rafael Ostertag's avatar
Rafael Ostertag committed
    /**
     * @private
     */
    n.QfqForm.prototype.handleNewClick = function (event) {
        event.preventDefault();

Carsten  Rose's avatar
Carsten Rose committed
        this.lastButtonPress = "new";
        if (this.form.getFormChanged()) {
            var alert = new n.Alert("You have unsaved changes. Do you want to close?", "warning", "yesnosave");
            var that = this;
            alert.on('alert.save', function () {
                that.submit();
                var anchorTarget = that.getNewButtonTarget();
                window.location = anchorTarget;
            });
            alert.show();
        } else {
            var anchorTarget = this.getNewButtonTarget();
            window.location = anchorTarget;
Rafael Ostertag's avatar
Rafael Ostertag committed
        n.Log.debug("new click");
Rafael Ostertag's avatar
Rafael Ostertag committed
    };

    /**
     * @private
     */
    n.QfqForm.prototype.handleDeleteClick = function () {
Carsten  Rose's avatar
Carsten Rose committed
        this.lastButtonPress = "delete";
Rafael Ostertag's avatar
Rafael Ostertag committed
        n.Log.debug("delete click");
        var alert = new n.Alert("Do you really want to delete the record?", "warning", "yesno");
            $.post(that.deleteUrl)
                .done(that.ajaxDeleteSuccessDispatcher.bind(that))
                .fail(n.Helper.showAjaxError);
    /**
     *
     * @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") {
Rafael Ostertag's avatar
Rafael Ostertag committed
            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.");
        }
Rafael Ostertag's avatar
Rafael Ostertag committed
        var alert = new n.Alert(data.message, "error");
Rafael Ostertag's avatar
Rafael Ostertag committed
     * @param form {n.QfqForm}
     *
     * @private
     */
    n.QfqForm.prototype.changeHandler = function (obj) {
        this.getSaveButton().removeClass("disabled");
        this.getSaveButton().addClass("alert-warning");
        this.getSaveButton().removeAttr("disabled");
    };

    /**
     *
Rafael Ostertag's avatar
Rafael Ostertag committed
     * @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 () {
    /**
     * @private
     */
    n.QfqForm.prototype.submitSuccessDispatcher = function (obj) {
        if (!obj.data.status) {
Rafael Ostertag's avatar
Rafael Ostertag committed
            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.");
Rafael Ostertag's avatar
Rafael Ostertag committed
    /**
     *
     * @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.");
        }
Rafael Ostertag's avatar
Rafael Ostertag committed
        var alert = new n.Alert(data.message, "error");
        alert.show();
Rafael Ostertag's avatar
Rafael Ostertag committed

        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) {
Rafael Ostertag's avatar
Rafael Ostertag committed
        n.Log.debug('Reset form state');
Rafael Ostertag's avatar
Rafael Ostertag committed
        form.resetFormChanged();

Rafael Ostertag's avatar
Rafael Ostertag committed
                if (data.message) {
                    var alert = new n.Alert(data.message);
                    alert.timeout = 1500;
                    alert.show();
                }
Rafael Ostertag's avatar
Rafael Ostertag committed
                // do we have to update the HTML Form?
                if (data['form-update']) {
                    this.applyElementConfiguration(data['form-update']);
Rafael Ostertag's avatar
Rafael Ostertag committed
                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();
Rafael Ostertag's avatar
Rafael Ostertag committed
                window.location = target;
            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');
    };

Rafael Ostertag's avatar
Rafael Ostertag committed
    n.QfqForm.prototype.getFormGroupByControlName = function (formControlName) {
Carsten  Rose's avatar
Carsten Rose committed
        var $formControl = $("[name='" + formControlName + "']");
Rafael Ostertag's avatar
Rafael Ostertag committed
        if ($formControl.length === 0) {
Rafael Ostertag's avatar
Rafael Ostertag committed
            n.Log.debug("QfqForm.setValidationState(): unable to find form control with name '" + formControlName + "'");
Rafael Ostertag's avatar
Rafael Ostertag committed
            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");
    };

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

    /**
     *
     * @param formControlName
     * @param text
     */
Rafael Ostertag's avatar
Rafael Ostertag committed
    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.
         */
Rafael Ostertag's avatar
Rafael Ostertag committed
        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) {
Rafael Ostertag's avatar
Rafael Ostertag committed
                n.Log.error("configuration lacks 'form-element' attribute. Skipping.");
                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) {
                    element.setEnabled(!configurationItem.disabled);
                if (configurationItem.hidden !== undefined) {
                    element.setHidden(configurationItem.hidden);
Rafael Ostertag's avatar
Rafael Ostertag committed
                n.Log.error(e.message);
    n.QfqForm.prototype.startUploadHandler = function (obj) {
        $(obj.target).after(
    n.QfqForm.prototype.endUploadHandler = function (obj) {
        var $siblings = $(obj.target).siblings();
    /**
     * Retrieve SIP as stored in hidden input field.
     *
     * @returns {string} sip
     */
    n.QfqForm.prototype.getSip = function () {
        return $('#' + this.formId + ' input[name=s]').val();
    };

})(QfqNS);