/** * @author Rafael Ostertag <rafael.ostertag@math.uzh.ch> */ /* global $ */ /* global EventEmitter */ /* @depend QfqEvents.js */ 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 */ 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.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); this.fileDeleter.on('filedelete.delete.failed', function (obj) { n.Helper.showAjaxError(null, obj.textStatus, obj.errorThrown); }); }; n.QfqForm.prototype.on = n.EventEmitter.onMixin; /** * @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(""); }; /** * @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"); // 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(); }; /** * @private */ n.QfqForm.prototype.handleCloseClick = function () { this.lastButtonPress = "close"; 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(); }); alert.on('alert.ok', function () { 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 alert = new n.Alert("Form is incomplete.", "warning"); alert.show(); return; } 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("You have unsaved changes. Do you want to close?", "warning", "yesnosave"); var that = this; alert.on('alert.save', function () { that.submit(); }); alert.on('alert.ok', function () { var anchorTarget = that.getNewButtonTarget(); window.location = anchorTarget; }); 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("Do you really want to delete the record?", "warning", "yesno"); 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': 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"); }; 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 */ 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) { element.setReadOnly(configurationItem.readonly); } if (configurationItem.disabled !== undefined) { element.setEnabled(!configurationItem.disabled); } } 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. * * @returns {string} sip */ n.QfqForm.prototype.getSip = function () { return $('#' + this.formId + ' input[name=s]').val(); }; })(QfqNS);