/**
* @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);