PMA.UI Documentation by Pathomation

components/js/forms.js

import { Resources } from '../../resources/resources';
import { checkBrowserCompatibility } from '../../view/helpers';
import { Events, ApiMethods, parseJson, callApiMethod } from './components';

function arrayContains(array, value) {
    for (var i = 0; i < array.length; i++) {
        if (array[i] == value) {
            return true;
        }
    }

    return false;
}

export
    /**
     * Represents a component that provides both UI and programmatic interaction with PMA.core forms and data.
     * @memberof PMA.UI.Components
     * @alias Forms
     * @param  {Context} context
     * @param  {Object} options - Reserved for future use
     * @tutorial 06-forms
     */
    class Forms {
    constructor(context) {
        if (!checkBrowserCompatibility()) {
            return;
        }

        this.listeners = {};
        this.listeners[Events.FormSaved] = [];
        this.listeners[Events.FormEditClick] = [];
        this.context = context;
    }
    /**
     * Renders a form field in read-only form
     * @param  {Object} field - A PMA.core form field structure
     * @param  {Object} record - A PMA.core form field value structure
     * @param  {string} cssClass - Extra CSS class to add to the container element
     * @returns {string} The HTML output
     */
    renderReadOnlyField(field, record, cssClass) {
        var strVal = (record && record.FormValue ? record.FormValue : '').replace(/"/g, "&quot;");

        if (strVal.length !== 0 && record && field.FieldType != Forms.FieldType.CheckBox && field.FormList && field.FormList.FormListValues && !record.IsOtherValue && !record.IsBelowDetectableLimit) {
            strVal = field.FormList.FormListValues[strVal].Value;
        }

        if (record && record.IsBelowDetectableLimit) {
            strVal = Resources.translate("Below detectable limit");
        }

        var html = "";

        switch (field.FieldType) {
            case Forms.FieldType.Paragraph:
                html += '<p class="' + cssClass + '">' + strVal + '</p>';
                break;
            case Forms.FieldType.CheckBox:

                if (field.FormList && field.FormList.FormListValues && record && !record.IsOtherValue && strVal.length !== 0 && !record.IsBelowDetectableLimit) {
                    var chkValues = strVal.split("|");
                    html += '<ul class="' + cssClass + '">';

                    for (let k = 0; k < chkValues.length; k++) {
                        html += "<li>" + field.FormList.FormListValues[chkValues[k]].Value + "</li>";
                    }

                    html += '</ul>';
                }
                else {
                    html += '<div class="' + cssClass + '">' + strVal + '</div>';
                }

                break;
            case Forms.FieldType.Percentage:
                html += '<div class="' + cssClass + '">' + strVal + '%</div>';
                break;
            case Forms.FieldType.HyperLink:
                html += '<a class="link-field" title="' + field.Tooltip + '" href="' + field.Url + '" ' + (field.NewWindow === true ? ' target="_blank" ' : '') + '>' + field.Label + '</a>';
                break;
            case Forms.FieldType.Label:
                break;
            default:
                html += '<div class="' + cssClass + '">' + strVal + '</div>';
                break;
        }

        return html;
    }
    /**
     * Passes a form definition and optionally form data back to the caller
     * @callback Forms~getFormCallback
     * @param {Object} form - The requested form definition
     * @param {Object} [data] - The submitted form data
     */
    /**
     * A callback to filter the data retrieved from the server
     * @callback Forms~filterDataCallback
     * @param {Object} [data] - The retrieved form data
     * @returns {Object} The filtered data
     */
    /**
     * Loads a form definition and optionally the available submitted data
     * @param  {String} serverUrl
     * @param  {Number} formId
     * @param  {Object} [dataOptions] - Used when it is desired to load submitted data as well
     * @param  {string} dataOptions.path - The path to load form submitted data for
     * @param  {boolean} [dataOptions.currentUserOnly=false] - True to load only the current user's data. The user is determined by the server's URL and the authentication providers available in the context.
     * @param  {string | Forms~filterDataCallback} [dataOptions.dataFilter=""] - Optional parameter to filter data results with. If the parameter is a string it is considered a username and data from this user are kept. This is useful when a particular user's data is desired, but not the currently logged on one's. If a callback function is provided it should return the filtered data
     * @param  {Forms~getFormCallback} [success]
     * @param  {function} [failure]
     */
    getForm(serverUrl, formId, dataOptions, success, failure) {
        var _this = this;

        _this.context.getSession(serverUrl, function (sessionId) {
            callApiMethod({
                serverUrl: serverUrl,
                method: ApiMethods.GetForm,
                data: { sessionID: sessionId, id: formId },
                success: function (http) {
                    var response = parseJson(http.responseText);
                    if (response) {
                        if (dataOptions !== null && dataOptions !== undefined && typeof dataOptions === "object") {
                            loadFormData.call(_this, serverUrl, sessionId, response, dataOptions.path, dataOptions.currentUserOnly === true, dataOptions.dataFilter,
                                function (data) {
                                    response.FormData = data;
                                    if (typeof success === "function") {
                                        success.call(_this, response, data);
                                    }
                                }, failure);
                        }
                        else if (typeof success === "function") {
                            success.call(_this, response, null);
                        }
                    }
                    else if (typeof failure === "function") {
                        failure();
                    }
                },
                failure: failure
            });
        }, failure);
    }
    /**
     * Passes form definitions back to the caller
     * @callback Forms~getFormsCallback
     * @param {Object[]} forms - Form definition objects fetched from PMA.core
     */
    /**
     * Loads all the available form definitions from a PMA.core server
     * @param  {String} serverUrl - The PMA.core server URL
     * @param  {Forms~getFormsCallback} success - Called upon successful completion of the data request.
     * @param  {function} [failure]
     */
    getForms(serverUrl, success, failure) {
        var _this = this;
        _this.context.getSession(serverUrl, function (sessionId) {
            callApiMethod({
                serverUrl: serverUrl,
                method: ApiMethods.GetForms,
                data: { sessionID: sessionId },
                success: function (http) {
                    var response = parseJson(http.responseText);
                    if (response && typeof success === "function") {
                        success(response);
                    }
                    else if (typeof failure === "function") {
                        failure(response);
                    }
                },
                failure: failure
            });
        }, failure);
    }
    /**
     * Called while rendering each field. This function gives the opportunity to the caller to make modifications per element, or return false to prevent rendering of specific fields.
     * @callback Forms~renderFieldCallback
     * @param {Object} form - The definition of the form that is being rendered
     * @param {Object} field - The definition of the field that is being rendered
     * @param {Object} field.fieldGroupClass - The CSS class to assign to the fields group element
     * @param {Object} field.fieldContainerClass - The CSS class to assign to the field's container
     * @param {Object} [record] - The data that will be displayed by the field. Can be null if no data is available or loaded.
     * @returns {boolean} True to render this field, otherwise false
     */
    /**
     * Renders a form and optionally loads the available user submitted data
     * @param  {string} serverUrl
     * @param  {Number} formId
     * @param {string|HTMLElement} element - The element that hosts the form. It can be either a valid CSS selector or an HTMLElement instance.
     * @param  {Object} [dataOptions] - Supplied only when it is desired to load form submitted data as well
     * @param  {string} dataOptions.path - The path to load form submitted data for
     * @param  {boolean} [dataOptions.currentUserOnly=false] - True to load only the current user's data. The user is determined by the server's URL and the authentication providers available in the context.
     * @param  {string | Forms~filterDataCallback} [dataOptions.dataFilter=""] - Optional parameter to filter data results with. If the parameter is a string it is considered a username and data from this user are kept. This is useful when a particular user's data is desired, but not the currently logged on one's. If a callback function is provided it should return the filtered data
     * @param  {string} [dataOptions.btnContainerClass] - CSS class to assign the to element that contains the save and reset buttons
     * @param  {string} [dataOptions.btnResetClass] - CSS class to assign the reset button
     * @param  {string} [dataOptions.btnSaveClass] - CSS class to assign the save button
     * @param  {Forms~renderFieldCallback} [dataOptions.fieldCb] - Called when rendering each field.
     * @param  {string} [dataOptions.fieldContainerClass] - CSS class to assign the to element that contains field controls
     * @param  {string} [dataOptions.fieldValidationClass] - CSS class to assign to error labels that are displayed upon validating the form
     * @param  {string} [dataOptions.formClass] - CSS class to assign to the rendered form element
     * @param  {string} [dataOptions.inputClass] - CSS class to assign the to input elements
     * @param  {boolean} [dataOptions.readOnly=false] - True to render the form in read only mode
     * @param  {boolean} [dataOptions.editButton=false] - True to render an edit button next to the form's name. Only applicable in read only mode
     * @param  {string} [dataOptions.validationClass] - CSS class to assign to generic validation messages (e.g. "The form could not be saved")
     * @param  {function} [success]
     * @param  {function} [failure]
     */
    displayForm(serverUrl, formId, element, dataOptions, success, failure) {
        var _this = this;

        this.getForm(serverUrl, formId, dataOptions,
            function (form, data) {
                _this.serverUrl = serverUrl;

                if (form) {
                    _this.form = form;
                    _this.form.enabled = true;

                    if (form.ReadOnly === true) {
                        dataOptions.readOnly = true;
                    }
                }

                _this.path = dataOptions.path;
                _this.originalData = data;
                renderForm.call(_this, element, form, dataOptions, data, success, failure);
            },
            failure);
    }
    /**
     * Renders a form and optionally loads the available user submitted data without loading from the server
     * @param {string|HTMLElement} element - The element that hosts the form. It can be either a valid CSS selector or an HTMLElement instance.
     * @param {Object} form - The form definition object
     * @param  {Object} [dataOptions] - Supplied only when it is desired to load form submitted data as well
     * @param  {string} dataOptions.path - The path to load form submitted data for
     * @param  {boolean} [dataOptions.currentUserOnly=false] - True to load only the current user's data. The user is determined by the server's URL and the authentication providers available in the context.
     * @param  {string | Forms~filterDataCallback} [dataOptions.dataFilter=""] - Optional parameter to filter data results with. If the parameter is a string it is considered a username and data from this user are kept. This is useful when a particular user's data is desired, but not the currently logged on one's. If a callback function is provided it should return the filtered data
     * @param  {string} [dataOptions.btnContainerClass] - CSS class to assign the to element that contains the save and reset buttons
     * @param  {string} [dataOptions.btnResetClass] - CSS class to assign the reset button
     * @param  {string} [dataOptions.btnSaveClass] - CSS class to assign the save button
     * @param  {Forms~renderFieldCallback} [dataOptions.fieldCb] - Called when rendering each field.
     * @param  {string} [dataOptions.fieldContainerClass] - CSS class to assign the to element that contains field controls
     * @param  {string} [dataOptions.fieldValidationClass] - CSS class to assign to error labels that are displayed upon validating the form
     * @param  {string} [dataOptions.formClass] - CSS class to assign to the rendered form element
     * @param  {string} [dataOptions.inputClass] - CSS class to assign the to input elements
     * @param  {boolean} [dataOptions.readOnly=false] - True to render the form in read only mode
     * @param  {boolean} [dataOptions.editButton=false] - True to render an edit button next to the form's name. Only applicable in read only mode
     * @param  {string} [dataOptions.validationClass] - CSS class to assign to generic validation messages (e.g. "The form could not be saved")
     * @param {Object} [data] - The data to load to the form displayed
     * @param  {function} [success]
     * @param  {function} [failure]
     */
    renderForm(element, form, dataOptions, data, success, failure) {
        if (form) {
            this.form = form;
            this.form.enabled = true;

            if (form.ReadOnly === true) {
                dataOptions.readOnly = true;
            }
        }

        this.path = dataOptions.path;
        this.originalData = data;

        renderForm.call(this, element, form, dataOptions, data, success, failure);
    }
    /**
     * Saves the currently rendered form to PMA.core
     * @param  {function} [success]
     * @param  {function} [failure]
     * @fires PMA.UI.Components.Events.FormSaved
     */
    saveForm(success, failure) {
        var _this = this;
        _this.setEnabled(false);
        $("#" + _this.form.ClientID + " button[type='submit']").html(Resources.translate("Saving..."));
        submitForm.call(this, function (args) {
            $("#" + _this.form.ClientID + " button[type='submit']").html(Resources.translate("Save"));
            _this.setEnabled(true);
            _this.fireEvent(Events.FormSaved, args);
            if (typeof success === "function") {
                success(args);
            }
        },
            function (args) {
                $("#" + _this.form.ClientID + " button[type='submit']").html(Resources.translate("Save"));
                _this.setEnabled(true);
                _this.fireEvent(Events.FormSaved, args);
                if (typeof failure === "function") {
                    failure(args);
                }
            });
    }
    /**
     * Enables or disables the currently rendered form
     * @param  {boolean} enabled
     */
    setEnabled(enabled) {
        if (this.form) {
            this.form.enabled = !!enabled;
            $("#" + this.form.ClientID + " > fieldset").prop("disabled", !this.form.enabled);
        }
        else {
            console.error("No form");
        }
    }
    /**
     * Gets the state of the currently rendered form
     * @return {boolean}
     */
    getEnabled() {
        if (this.form) {
            return this.form.enabled;
        }
        else {
            console.error("No form");
        }
    }
    /**
     * Attaches an event listener
     * @param {PMA.UI.Components.Events} eventName - The name of the event to listen to
     * @param {function} callback - The function to call when the event occurs
     */
    listen(eventName, callback) {
        // if (!this.listeners.hasOwnProperty(eventName)) {
        if (!Object.prototype.hasOwnProperty.call(this.listeners, eventName)) {
            console.error(eventName + " is not a valid event");
        }

        this.listeners[eventName].push(callback);
    }
    // fires an event
    fireEvent(eventName, eventArgs) {
        // if (!this.listeners.hasOwnProperty(eventName)) {
        if (!Object.prototype.hasOwnProperty.call(this.listeners, eventName)) {
            console.error(eventName + " does not exist");
            return;
        }

        for (var i = 0, max = this.listeners[eventName].length; i < max; i++) {
            this.listeners[eventName][i].call(this, eventArgs);
        }
    }
    /**
     * Checks if the currently loaded form has any changes that have not been saved yet.
     * @return {boolean}
     */
    hasChanges() {
        if (!this.form) {
            return false;
        }

        return this.hasChangesProperty;
    }
    /**
     * Resets the currently loaded form to it's initial state
     */
    reset() {
        if (!this.form) {
            return false;
        }

        $("#" + this.form.ClientID + " [type='reset']").click();
    }
}

/**
 * Available form field types
 * @readonly
 * @enum {number}
 */
Forms.FieldType = {
    /**
     * Simple text (input)
     */
    Text: 0,

    /**
     * Simple paragraph (label)
     */
    Paragraph: 1,

    /**
     * Dropdown
     */
    ListBox: 2,

    /**
     * Checkbox list
     */
    CheckBox: 3,

    /**
     * Radio button list
     */
    RadioButton: 4,

    /**
     * Integer input
     */
    Integer: 5,

    /**
     * Decimal input
     */
    Double: 6,

    /**
     * Datetime input
     */
    DateTime: 7,

    /**
     * Percentage input
     */
    Percentage: 8,

    /**
     * Section separator - label
     */
    Label: 9,

    /**
     * HyperLink
     */
    HyperLink: 10
};

function getCheckboxesValue(el) {
    var value = "";
    el.each(function () {
        if (value.length > 0) {
            value += "|";
        }

        value += this.value;
    });

    return value;
}

function createPostData(sessionId) {
    var values = [];
    var form = this.form;

    for (var i = 0; i < this.form.FormFields.length; i++) {
        var field = form.FormFields[i];
        var fieldId = getFieldId(this.form, i);

        var value = null,
            below = false,
            other = false,
            el, otherElVal;
        switch (field.FieldType) {
            case Forms.FieldType.Text:
            case Forms.FieldType.Paragraph:
            case Forms.FieldType.DateTime:
                el = $("#" + fieldId);
                if (el.val() !== "") {
                    value = el.val();
                }

                break;
            case Forms.FieldType.Integer:
            case Forms.FieldType.Double:
            case Forms.FieldType.Percentage:
                el = $("#" + fieldId);
                if (el.val() !== "") {
                    value = el.val();
                }
                else if (field.AllowBelowDetectableLimit === true && $("#" + fieldId + "_below:checked").length === 1) {
                    below = true;
                }

                break;
            case Forms.FieldType.ListBox:
                el = $("#" + fieldId);
                if (el.val() !== "") {
                    value = el.val();
                }
                else if (field.AllowOther && $("#" + fieldId + "_other").val() !== "") {
                    value = $("#" + fieldId + "_other").val();
                    other = true;
                }

                break;
            case Forms.FieldType.CheckBox:
                otherElVal = $("#" + fieldId + "_other").val();
                el = $("input[name='" + fieldId + "']:checked");
                if (el.length !== 0) {
                    value = getCheckboxesValue(el);
                }
                else if (field.AllowOther === true && otherElVal !== "") {
                    value = otherElVal;
                    other = true;
                }

                break;
            case Forms.FieldType.RadioButton:
                otherElVal = $("#" + fieldId + "_other").val();
                el = $("input[name='" + fieldId + "']:not([id$='_other_indicator']):checked");

                if (el.length !== 0) {
                    value = el.val();
                }
                else if (field.AllowOther === true && otherElVal !== "") {
                    value = otherElVal;
                    other = true;
                }

                break;
        }

        values.push({
            FieldId: field.FieldID,
            FormValue: value,
            IsBelowDetectableLimit: below,
            IsOtherValue: other
        });
    }

    return {
        sessionID: sessionId,
        pathOrUid: this.path,
        formId: this.form.FormID,
        fieldValues: values
    };
}

function submitForm(success, failure) {
    if (!validateForm.call(this)) {
        if (typeof failure === "function") {
            failure({ success: false, message: Resources.translate("Form validation error") });
        }

        return;
    }

    var _this = this;
    _this.context.getSession(_this.serverUrl, function (sessionId) {
        var postData = createPostData.call(_this, sessionId);

        callApiMethod({
            serverUrl: _this.serverUrl,
            method: ApiMethods.SaveFormData,
            httpMethod: "POST",
            contentType: 'application/json',
            data: postData,
            success: function () {
                _this.hasChangesProperty = false;

                if (typeof success === "function") {
                    success({ success: true, message: '' });
                }
            },
            failure: function (http) {
                var errorMsg = Resources.translate("Save failed. Check the form values and try again.");
                if (http && http.responseText) {
                    var errorObj = JSON.parse(http.responseText);
                    if (errorObj && errorObj.Message) {
                        errorMsg = errorObj.Message;
                    }
                }

                $("#" + _this.form.ClientID + "_validation").html(errorMsg);
                if (typeof failure === "function") {
                    failure({ success: false, message: errorMsg });
                }
            }
        });
    }, function (args) {
        if (typeof failure === "function") {
            failure({ success: false, message: ((args && args.Message) ? args.Message : Resources.translate("Authentication error")) });
        }
    });
}

function clearValidationErrors() {
    if (!this.form) {
        return;
    }

    $("#" + this.form.ClientID + " [id$='_validation']").html("");
    $("#" + this.form.ClientID + " *").removeClass("invalid");
}

function validateForm() {
    if (!this.form) {
        return false;
    }

    clearValidationErrors.call(this);

    var form = this.form;
    $("#" + form.ClientID + "_validation").html("");

    var el, isValid;

    for (var i = 0; i < this.form.FormFields.length; i++) {
        var field = form.FormFields[i];
        var fieldId = getFieldId(this.form, i);

        var validationSpan = $("#" + fieldId + "_validation");
        validationSpan.html("");

        switch (field.FieldType) {
            case Forms.FieldType.Text:
            case Forms.FieldType.Paragraph:
            case Forms.FieldType.DateTime:
                el = $("#" + fieldId);

                if (field.Required === true && el.val() === "") {
                    el.addClass("invalid");
                    el.focus();
                    validationSpan.html(Resources.translate("Please enter a value"));

                    return false;
                }

                el.removeClass("invalid");
                break;
            case Forms.FieldType.Integer:
            case Forms.FieldType.Double:
            case Forms.FieldType.Percentage:

                el = $("#" + fieldId);
                if (field.Required === true && el.val() === "") {
                    isValid = false;
                    if (field.AllowBelowDetectableLimit === true && $("#" + fieldId + "_below:checked").length === 1) {
                        isValid = true;
                    }

                    if (!isValid) {
                        el.addClass("invalid");
                        el.focus();
                        validationSpan.html(Resources.translate("Please enter a value"));

                        return false;
                    }
                }

                var numVal = el.val();
                if (isNaN(numVal)) {
                    el.addClass("invalid");
                    el.focus();
                    validationSpan.html(Resources.translate("Please enter a valid value"));

                    return false;
                }

                if (typeof numVal !== "number") {
                    numVal = parseFloat(numVal);
                }

                if (typeof field.LowerBound === "number" && numVal < field.LowerBound) {
                    el.addClass("invalid");
                    el.focus();
                    validationSpan.html(Resources.translate("Please enter a value larger than or equal to {LowerBound}", { LowerBound: field.LowerBound }));

                    return false;
                }

                if (typeof field.UpperBound === "number" && numVal > field.UpperBound) {
                    el.addClass("invalid");
                    el.focus();
                    validationSpan.html(Resources.translate("Please enter a value less than or equal to {UpperBound}", { UpperBound: field.UpperBound }));

                    return false;
                }

                el.removeClass("invalid");
                break;
            case Forms.FieldType.ListBox:
                el = $("#" + fieldId);
                if (field.Required === true && el.val() === "") {
                    isValid = false;

                    if (field.AllowOther && $("#" + fieldId + "_other").val() !== "") {
                        isValid = true;
                    }

                    if (!isValid) {
                        el.addClass("invalid");
                        el.focus();
                        validationSpan.html(Resources.translate("Please select a value"));

                        return false;
                    }
                }

                el.removeClass("invalid");
                break;
            case Forms.FieldType.CheckBox:
            case Forms.FieldType.RadioButton:
                // if the field is required and not option is select apart from the "other value" indicator
                if (field.Required === true && $("input[name='" + fieldId + "']:not([id$='_other_indicator']):checked").length === 0) {
                    if (field.AllowOther !== true || $("#" + fieldId + "_other").val() === "") {
                        $("input[name='" + fieldId + "']").first().focus();
                        validationSpan.html(Resources.translate("Please select a value"));
                        return false;
                    }
                }

                break;
            case Forms.FieldType.Label:
            case Forms.FieldType.HyperLink:
                break;
            default:
                console.error("Unknown field type " + field.FieldType);
                return false;
        }
    }

    return true;
}

function bindClearFieldValueWhenBelowCheckedEvent(field, fieldId) {
    $("#" + fieldId + "_below").change(function () {
        if (this.checked) {
            $("#" + fieldId).val("");
        }
    });
}

function bindUncheckBelowEvent(field, fieldId) {
    $("#" + fieldId).change(function () {
        if ($(this).val() !== "") {
            $("#" + fieldId + "_below").prop("checked", false);
        }
    });
}

function bindClearFieldValueWhenOtherChangedEvent(field, fieldId) {
    $("#" + fieldId + "_other").change(function () {
        if ($(this).val() !== "") {
            $("#" + fieldId).val("");
            $("input[name='" + fieldId + "'").prop("checked", false);
            $("#" + fieldId + "_other_indicator").prop("checked", true);
        }
    });
}

function bindClearOtherWhenValueSelectedEvent(field, fieldId) {
    $("input[name='" + fieldId + "'], #" + fieldId).change(function () {
        if (this.checked || (this.tagName === "SELECT" && this.value !== "")) {
            $("#" + fieldId + "_other").val("");
        }
    });
}

function bindFormElementEvents() {
    var _this = this;
    $("#" + this.form.ClientID).submit(function (evt) {
        evt.preventDefault();
        _this.saveForm();
    }).change(function () {
        _this.hasChangesProperty = true;
    });

    $("#" + this.form.ClientID + " legend .edit-button").click(function (evt) {
        evt.preventDefault();
        _this.fireEvent(Events.FormEditClick);
    });

    $("#" + this.form.ClientID + " [type='reset']").click(function (evt) {
        clearValidationErrors.call(_this);
        _this.hasChangesProperty = false;
        $("#" + _this.form.ClientID + " select").val('');
        $("#" + _this.form.ClientID + " input[type='text']").val('');
        $("#" + _this.form.ClientID + " input[type='number']").val('');
        $("#" + _this.form.ClientID + " textarea").val('');
        $("#" + _this.form.ClientID + " input[type='checkbox'], #" + _this.form.ClientID + " input[type='radio']").prop('checked', false);
        evt.preventDefault();
    });

    for (var i = 0; i < this.form.FormFields.length; i++) {
        var fieldId = getFieldId(this.form, i);

        var field = this.form.FormFields[i];
        if (field.AllowBelowDetectableLimit) {
            bindClearFieldValueWhenBelowCheckedEvent.call(_this, field, fieldId);
            bindUncheckBelowEvent.call(_this, field, fieldId);
        }

        if (field.AllowOther) {
            bindClearFieldValueWhenOtherChangedEvent.call(_this, field, fieldId);
            bindClearOtherWhenValueSelectedEvent.call(_this, field, fieldId);
            //bindUncheckBelowEvent.call(_this, field, fieldId);
        }
    }
}

function getFieldId(form, fieldIndex) {
    return form.ClientID + "_field_" + form.FormFields[fieldIndex].FieldID;
}

function getDataRecord(fieldId) {
    if (!this.originalData) {
        return;
    }

    for (var i = 0; i < this.originalData.length; i++) {
        var dataSet = this.originalData[i];
        if (!dataSet.FieldValues || !dataSet.FieldValues.length) {
            continue;
        }

        for (var j = 0; j < dataSet.FieldValues.length; j++) {
            var f = dataSet.FieldValues[j];
            if (f.FieldId === fieldId) {
                return f;
            }
        }
    }

    return null;
}

function renderField(form, fieldIndex, dataOptions) {
    var field = form.FormFields[fieldIndex];
    var list;

    field.Tooltip = field.Tooltip || "";

    var fieldId = getFieldId(form, fieldIndex);
    var record = getDataRecord.call(this, field.FieldID);

    if (!record) {
        record = {
            IsOtherValue: false,
            IsBelowDetectableLimit: false,
            FormValue: null
        };
    }

    if (typeof dataOptions.fieldCb === "function") {
        if (dataOptions.fieldCb(form, field, record) === false) {
            return '';
        }
    }

    var fieldGroupClass = '';

    if (field.ExtraStyle) {
        fieldGroupClass += " " + field.ExtraStyle + " ";
    }

    if (field.fieldGroupClass) {
        fieldGroupClass += " " + field.fieldGroupClass + " ";
    }

    var strVal = record && record.FormValue ? record.FormValue : '';

    var html = '<div class="form-group ' + (field.Required === true ? "required" : "") + " " + fieldGroupClass + '" title="' + field.Tooltip + '">';
    if (field.FieldType !== Forms.FieldType.HyperLink) {
        html += '<label for="' + fieldId + '">' + field.Label + '</label>';
    }

    html += "<div class='field-container " + (dataOptions.fieldContainerClass ? dataOptions.fieldContainerClass : "") + " " + (field.fieldContainerClass ? field.fieldContainerClass : "") + " '>";

    var requiredStr = "";
    if (field.Required === true) {
        requiredStr = ' required="required" ';
    }

    var inputClassStr = '';
    if (dataOptions.inputClass) {
        inputClassStr += " " + dataOptions.inputClass + " ";
    }

    if (field.fieldClass) {
        inputClassStr += " " + field.fieldClass + " ";
    }

    if (field.FieldType === Forms.FieldType.DateTime) {
        console.warn("PMA.UI.Components.Forms: No proper date time field support yet.");
    }

    if (dataOptions.readOnly === true) {
        html += this.renderReadOnlyField(field, record, inputClassStr);
    }
    else {
        switch (field.FieldType) {
            case Forms.FieldType.Text:
                html += '<input id="' + fieldId + '" name="' + fieldId + '" value="' + strVal + '" type="text" placeholder="' + field.Tooltip + '" ' + requiredStr + ' class="' + inputClassStr + '" />';
                break;
            case Forms.FieldType.DateTime: // no proper date time support yet
                html += '<input id="' + fieldId + '" name="' + fieldId + '" value="' + strVal + '" type="text" placeholder="yyyy-MM-dd HH:mm:ss" ' + requiredStr + ' class="' + inputClassStr + '" />';
                break;
            case Forms.FieldType.Paragraph:
                html += '<textarea id="' + fieldId + '" name="' + fieldId + '" placeholder="' + field.Tooltip + '"' + requiredStr + ' class="' + inputClassStr + '">' + strVal + '</textarea>';
                break;
            case Forms.FieldType.ListBox:
                html += '<select id="' + fieldId + '" name="' + fieldId + '" ' + ' class="' + inputClassStr + '">';

                if (field.FormList && field.FormList.FormListValues) {
                    html += '<option value="" ' + (strVal.length === 0 ? 'selected="selected"' : "") + '>' + field.Tooltip + '</option>';

                    list = field.FormList.FormListValues;
                    for (let k = 0; k < list.length; k++) {
                        html += '<option value="' + list[k].ValueID + '" ' + (strVal.length !== 0 && list[k].ValueID == strVal ? 'selected="selected"' : "") + '>' + list[k].Value + '</option>';
                    }
                }
                html += '</select>';

                break;
            case Forms.FieldType.CheckBox:
                html += '<ul class="checkboxlist">';

                var strValues = strVal.split("|");

                if (field.FormList && field.FormList.FormListValues) {
                    list = field.FormList.FormListValues;
                    for (let k = 0; k < list.length; k++) {
                        html += '<li>';
                        html += '<input type="checkbox" class="' + inputClassStr + '" id="' + fieldId + '_' + list[k].ValueID + '" name="' + fieldId + '" value="' + list[k].ValueID + '" ' + (arrayContains(strValues, "" + list[k].ValueID) ? 'checked="checked"' : "") + '/>';
                        html += '<label for="' + fieldId + '_' + list[k].ValueID + '">' + list[k].Value + '</label>';
                        html += "</li>";
                    }
                }

                html += '</ul>';
                break;
            case Forms.FieldType.RadioButton:
                html += '<ul class="radiobuttonlist">';

                if (field.FormList && field.FormList.FormListValues) {
                    list = field.FormList.FormListValues;

                    /*
                    if (field.Required === false) {
                        html += '<li>';
                        html += '<input type="radio" id="' + fieldId + '_novalue" name="' + fieldId + '" value="" />';
                        html += '<label for="' + fieldId + '_novalue">' + Resources.translate("(No selection)") + '</label>';
                        html += "</li>";
                    }
                    */

                    for (let k = 0; k < list.length; k++) {
                        html += '<li>';
                        html += '<input type="radio" class="' + inputClassStr + '" id="' + fieldId + '_' + list[k].ValueID + '" name="' + fieldId + '" value="' + list[k].ValueID + '" ' + (strVal !== "" && list[k].ValueID == strVal ? 'checked="checked"' : "") + ' />';
                        html += '<label for="' + fieldId + '_' + list[k].ValueID + '">' + list[k].Value + '</label>';
                        html += "</li>";
                    }

                    if (field.AllowOther === true) {
                        html += '<li>';
                        html += '<input type="radio" class="' + inputClassStr + '" id="' + fieldId + '_other_indicator" name="' + fieldId + '" ' + (record && record.IsOtherValue ? 'checked="checked"' : "") + ' value="other" />';
                        html += '<label for="' + fieldId + '_other_indicator">' + Resources.translate("Other value") + '</label>';
                        html += "</li>";
                    }
                }

                html += '</ul>';
                break;
            case Forms.FieldType.Integer:
                var step = 1;
                if (field.Interval) {
                    step = field.Interval | 0;
                }

                html += '<input id="' + fieldId + '" name="' + fieldId + '"' + requiredStr + ' ' + (field.LowerBound !== null ? 'min="' + field.LowerBound + '"' : "") + ' ' + (field.UpperBound !== null ? 'max="' + field.UpperBound + '"' : "") + ' type="number" step="' + step + '" value="' + strVal + '"  placeholder="' + field.Tooltip + '" class="' + inputClassStr + '"/>';
                break;
            case Forms.FieldType.Double:
                html += '<input id="' + fieldId + '" name="' + fieldId + '"' + requiredStr + ' ' + (field.LowerBound !== null ? 'min="' + field.LowerBound + '"' : "") + ' ' + (field.UpperBound !== null ? 'max="' + field.UpperBound + '"' : "") + ' type="number" step="' + (field.Interval ? field.Interval : 'any') + '" value="' + strVal + '" placeholder="' + field.Tooltip + '" class="' + inputClassStr + '" />';
                break;
            case Forms.FieldType.Percentage:
                var minv = 0,
                    maxv = 100;
                if (field.LowerBound !== null && (field.LowerBound | 0) >= 0) {
                    minv = field.LowerBound | 0;
                }

                if (field.UpperBound !== null && (field.UpperBound | 0) <= 0) {
                    maxv = field.UpperBound | 0;
                }

                html += '<input id="' + fieldId + '" name="' + fieldId + '"' + requiredStr + ' min="' + minv + '" max="' + maxv + '" type="number" step="' + (field.Interval ? field.Interval : 'any') + '" value="' + strVal + '" placeholder="' + field.Tooltip + '" class="' + inputClassStr + '" />';
                break;
            case Forms.FieldType.Label:
                ////html += '<div class="labelfield" id="' + fieldId + '">' + field.Label + '</div>';
                break;
            case Forms.FieldType.HyperLink:
                html += '<a class="link-field" id="' + fieldId + '" title="' + field.Tooltip + '" href="' + field.Url + '" ' + (field.NewWindow === true ? ' target="_blank" ' : '') + '>' + field.Label + '</a>';
                break;
        }

        if (field.AllowBelowDetectableLimit === true) {
            html += '<div class="below-container">';
            html += '<input id="' + fieldId + '_below" name="' + fieldId + '_below" type="checkbox"  ' + (record && record.IsBelowDetectableLimit === true ? 'checked="checked"' : "") + ' value="below" />';
            html += '<label for="' + fieldId + '_below">' + Resources.translate("Below detectable limit") + '</label>';
            html += '<div>';
        }

        if (field.AllowOther === true) {
            html += '<input id="' + fieldId + '_other" class="other ' + (dataOptions.inputClass ? dataOptions.inputClass : "") + '" name="' + fieldId + '_other" type="text" value="' + (strVal !== "" && record && record.IsOtherValue === true ? strVal : "") + '" placeholder="' + Resources.translate("Other value") + '" />';
        }
    }

    html += '<span id="' + fieldId + '_validation" class="field-validation ' + (dataOptions.fieldValidationClass ? dataOptions.fieldValidationClass : "") + '"></span>';

    if (field.additionalHtml) {
        html += field.additionalHtml;
    }

    html += "</div></div>";

    return html;
}

function renderForm(element, form, dataOptions, data, success, failure) {
    var _this = this;
    _this.form.ClientID = "frm" + ((Math.random() * 10000000) | 0);
    _this.hasChangesProperty = false;

    var html = "<form class='pma-ui-form " + (dataOptions.formClass ? dataOptions.formClass : "") + "' name='" + form.ClientID + "' id='" + form.ClientID + "' autocomplete='off' novalidate><fieldset>";

    if (form.FormName) {
        html += "<legend class='form-name'>" + form.FormName + (dataOptions.editButton === true && dataOptions.readOnly === true ? "<span class=\"edit-button\">" + Resources.translate("[Edit]") + "</span>" : "") + "</legend>";
    }

    /*            
    if (form.Description) {
        html += "<div class='form-description'>" + form.Description + "</div>";
    }
    */

    if (form.Instructions) {
        html += "<div class='form-instructions'>" + form.Instructions + "</div>";
    }

    form.FormFields.sort(function (a, b) {
        return a.DisplayOrder - b.DisplayOrder;
    });

    for (var i = 0; i < form.FormFields.length; i++) {
        html += renderField.call(_this, form, i, dataOptions);
    }

    html += "<div id='" + form.ClientID + "_validation' class='form-validation " + (dataOptions.validationClass ? dataOptions.validationClass : "") + "'></div>";

    if (dataOptions.readOnly !== true) {
        html += "<div class='input-container " + (dataOptions.btnContainerClass ? dataOptions.btnContainerClass : "") + "'>";
        html += "<button type='submit' value='" + Resources.translate("Save") + "' class='" + (dataOptions.btnSaveClass ? dataOptions.btnSaveClass : "") + "' >" + Resources.translate("Save") + "</button>";
        html += "<button type='reset' value='" + Resources.translate("Reset") + "' class='" + (dataOptions.btnResetClass ? dataOptions.btnResetClass : "") + "' >" + Resources.translate("Reset") + "</button>";
    }

    html += "</div>";
    html += "</fieldset></form>";

    $(element).html(html);
    bindFormElementEvents.call(_this);
    if (typeof success === "function") {
        success.call(_this, _this.form, null);
    }
}

function loadFormData(serverUrl, sessionId, form, path, currentUserOnly, dataFilter, success, failure) {
    var _this = this;
    callApiMethod({
        serverUrl: serverUrl,
        method: ApiMethods.GetFormData,
        data: { sessionID: sessionId, pathOrUid: path, formId: form.FormID, currentUserOnly: currentUserOnly },
        success: function (http) {
            var response = parseJson(http.responseText);
            if (typeof success === "function") {
                var data = null;
                if (typeof dataFilter === "string") {
                    for (var i = 0; i < response.length; i++) {
                        if (response[i].Login == dataFilter) {
                            data = [response[i]];
                            break;
                        }
                    }
                }
                else if (typeof dataFilter === "function") {
                    data = dataFilter.call(_this, response);
                }
                else {
                    data = response;
                }

                success.call(_this, data);
            }
        },
        failure: failure
    });
}