/*
 * sol.common.forms.Utils.initializeForm function will be executed when the page is loaded as long this script file is included.
 */
window.$initFrame = window.initFrame;
window.initFrame = function () {
  window.$initFrame && window.$initFrame();
  window.setTimeout(function () {
    sol.common.forms.Utils.initializeForm();
  }, 100);
};

/**
 * Helper functions for ELOwf forms
 *
 * @author MW, ELO Digital Office GmbH
 * @version 1.03.006
 *
 * @requires sol.common.ObjectUtils
 */

sol.define("sol.common.forms.Utils", {

  singleton: true,

  unitSuffix: "_UNIT",

  wfMapSavedFlag: "WF_MAP_FORM_SAVED",

  roundingFunctions: {

    CHF: function (decimal) {
      var roundedDecimal;

      roundedDecimal = decimal.times(20).toDecimalPlaces(0).dividedBy(20);

      return roundedDecimal;
    }
  },

  /**
   * Hides the tab bar if only one tab was added to the form.
   */
  hideTabBarIfOnlyOneTab: function () {
    var me = this,
        i, tabs, firstTabContainer = document.querySelector(".tabs");
    if (firstTabContainer) {
      tabs = firstTabContainer.querySelectorAll(".tabs > ul > li");
      if (tabs.length <= 1) {
        document.getElementById("elotable").className += " hideTabBar";
      } else {
        for (i in tabs) {
          tabs[i].onclick = me.scrollToTop;
        }
      }
    }
  },

  /**
   * Reset form layout if page content changes.
   */
  resetFormLayout: function () {
    var d = document.getElementById("elo_wf_form");
    if (d) {
      d.classList.add("hidden");
      setTimeout(function () {
        document.getElementById("elo_wf_form").classList.remove("hidden");
        sol.common.forms.Utils.updateHeaderHeight();
        sol.common.forms.Utils.updateFooterHeight();
      }, 10);
    }
  },

  /**
   * Scroll to top of the page.
   */
  scrollToTop: function () {
    document.body.scrollTop = 0;
    sol.common.forms.Utils.resetFormLayout();
  },

  /**
   * Set main header if class is set.
   */
  setMainHeader: function () {
    if (document.querySelector(".mainheader")) {
      var headTables = document.querySelectorAll("#elotable .edittable"),
          i, contentContainer, topNotification;

      if (headTables.length >= 1) {
        for (i in headTables) {
          if (headTables[i].querySelector(".mainheader")) {

            // Set identifier for main header
            headTables[i].className += " mainheadertable";

            // Set identifier for content container
            contentContainer = headTables[i].nextElementSibling;
            if (contentContainer) {
              contentContainer.className += " maincontent";
            }

            // Hide header or calculate top margin
            if (document.querySelector("body.version-gt-20 .mainheader.hide-header-gt-20")) {
              document.body.className += " hideMainHeader";
            } else if (contentContainer) {
              contentContainer.style.marginTop = (headTables[i].offsetHeight || 0) + "px";
            }

            // Check for top notification
            topNotification = document.querySelector("#topnotification");
            if (topNotification) {
              document.body.className += " disabledform";
              if (!topNotification.textContent.length) {
                document.body.className += " hideNotificationHeader";
              }
            }

            break;
          }
        }
      }
    }
  },

  /**
   * Set footer settings and calculate height.
   */
  setMainFooter: function () {
    if (document.querySelector("#elotable .sysarea")) {
      var me = this;

      // Calculate footer margin
      me.updateFooterHeight();
    }
  },

  /**
   * Update top margin because of main header height.
   */
  updateHeaderHeight: function () {
    var hideHeader = document.querySelector(".hideMainHeader"),
        mainheader = document.querySelector(".mainheadertable"),
        maincontent = document.querySelector(".maincontent");

    if (!hideHeader && mainheader && maincontent) {
      maincontent.style.marginTop = (mainheader.offsetHeight || 0) + "px";
    }
  },

  /**
   * Update bottom margin because of footer height.
   */
  updateFooterHeight: function () {
    var sysarea = document.querySelector("#elotable .sysarea"),
        footercontent = sysarea.querySelector("#FlowNext");

    if (sysarea && footercontent) {
      sysarea.style.marginTop = ((footercontent.offsetHeight || 0) + 17) + "px";
    }
  },

  /**
   * Mapping list for supported background colors in mainheader table
   */
  headerColors: {
    white: "whiteheader",
    light: "lightheader",
    info: "infoheader",
    success: "successheader",
    warning: "warningheader",
    danger: "dangerheader"
  },

  /**
   * Set background color in mainheader table
   */
  setHeaderColor: function (color) {
    var i, me = this;
    if (color && color in me.headerColors) {
      for (i in me.headerColors) {
        document.body.classList.remove(me.headerColors[i]);
      }
      document.body.classList.add(me.headerColors[color]);
    }
  },

  /**
   * Clear background color in mainheader table
   */
  clearHeaderColor: function () {
    var i, me = this;
    for (i in me.headerColors) {
      document.body.classList.remove(me.headerColors[i]);
    }
  },

  /**
   * Show single header description line
   */
  showHeaderDescription: function (identifier, color) {
    var me = this,
        message;

    if (message = document.getElementById(identifier)) {
      message.parentElement.classList.remove("hidden");
    } else if (message = document.querySelector("." + identifier)) {
      message.classList.remove("hidden");
    } else {
      return false;
    }

    me.setHeaderColor(color);

    if (document.querySelector(".hide-header-gt-20")) {
      document.querySelector(".hide-header-gt-20").classList.add("show-temporary");
      document.querySelector(".hide-header-gt-20").classList.remove("hide-header-gt-20");
      if (!document.body.classList.contains("actionMode")) {
        document.body.classList.remove("hideMainHeader");
      }
      me.updateHeaderHeight();
    }
  },

  /**
   * Hide specific header description line
   */
  hideHeaderDescription: function (identifier) {
    var me = this,
        message;

    if (message = document.getElementById(identifier)) {
      message.parentElement.classList.add("hidden");
    } else if (message = document.querySelector("." + identifier)) {
      message.classList.add("hidden");
    } else {
      return false;
    }

    me.clearHeaderColor();

    if (document.querySelector(".show-temporary")) {
      document.querySelector(".show-temporary").classList.add("hide-header-gt-20");
      document.querySelector(".show-temporary").classList.remove("show-temporary");
      if (!document.body.classList.contains("actionMode")) {
        document.body.classList.add("hideMainHeader");
      }
      me.updateHeaderHeight();
    }
  },

  /**
   * Set full-width flag if class is set.
   */
  setFullWidth: function () {
    var me = this, i, formElements, formTable;

    // Add class .elo-full-width to all relevant edittables
    formElements = document.querySelectorAll("#elotable .edittable .full-width");
    if (formElements.length >= 1) {
      for (i in formElements) {
        formTable = me.getTable(formElements[i]);
        if (formTable) {
          formTable.classList.add("elo-full-width");
        }
      }
    }
  },

  /**
   * Set responsive flag if class is set.
   */
  setResponsive: function () {
    var me = this,
        i, viewport, formElements, formElement;

    if (document.querySelector(".responsive")) {

      // Set mobile viewport meta info
      viewport = document.querySelector("meta[name=viewport]");
      if (viewport) {
        viewport.setAttribute("content", "width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0");
      } else {
        viewport = document.createElement("meta");
        viewport.name = "viewport";
        viewport.content = "width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0";
        document.getElementsByTagName("head")[0].appendChild(viewport);
      }

      // Add class .elo-responsive to all relevant edittables
      formElements = document.querySelectorAll("#elotable .edittable");
      if (formElements.length >= 1) {
        for (i in formElements) {
          formElement = formElements[i];
          if (formElement.tagName && formElement.querySelector(".responsive")) {
            formElement.className += " elo-responsive";
          }
        }
      }

      // Add class .formlistheader to all relevant tr elements
      formElements = document.querySelectorAll("#elotable tr td.formlistheader:first-child");
      if (formElements.length >= 1) {
        for (i in formElements) {
          formElement = formElements[i];
          if (formElement.tagName) {
            formElement.parentNode.className += " formlistheader";
          }
        }
      }

      // Reset layout when resizing the browser window
      window.onresize = me.resetFormLayout;
    }
  },

  /**
   * Register table navigation
   */
  registerTableNavigation: function () {
    var me = this;
    document.addEventListener("keydown", me.navigateTable.bind(me), false);
  },

  keyCodeArrowLeft: 37,
  keyCodeArrowUp: 38,
  keyCodeArrowRight: 39,
  keyCodeArrowDown: 40,

  navigateTable: function (event) {
    var me = this,
        isArrowKey, field, fieldName, baseFieldName, fieldNameIndex, nextField,
        dynKwlCmp = document.getElementById("keywordlist");

    isArrowKey = me.isArrowKey(event.keyCode);
    if (!isArrowKey ||
      (dynKwlCmp && dynKwlCmp.offsetLeft >= 0)) {
      // disable if dynKwl is opened
      return;
    }

    field = event.target;
    fieldName = field.name;
    fieldNameIndex = me.getFieldNameIndex(fieldName);
    if (fieldNameIndex == "") {
      return;
    }

    switch (event.keyCode) {
      case me.keyCodeArrowLeft:
        if ((typeof field.selectionStart != "number") || (field.selectionStart == 0)) {
          nextField = me.getNextWriteableHorizontalField(field, -1);
        }
        break;

      case me.keyCodeArrowUp:
        baseFieldName = me.getFieldNamePrefix(fieldName);
        nextField = me.getNextWriteableVerticalField(baseFieldName, fieldNameIndex, -1);
        break;

      case me.keyCodeArrowRight:
        if ((typeof field.selectionStart != "number") || (field.selectionStart == field.value.length)) {
          nextField = me.getNextWriteableHorizontalField(field, 1);
        }
        break;

      case me.keyCodeArrowDown:
        baseFieldName = me.getFieldNamePrefix(fieldName);
        nextField = me.getNextWriteableVerticalField(baseFieldName, fieldNameIndex, 1);
        break;

      default:
        break;
    }

    if (nextField) {
      nextField.focus();
      if (typeof nextField.selectionStart == "number") {
        nextField.selectionStart = nextField.value.length;
      }
      event.preventDefault();
    }
  },

  /**
   * @private
   * Returns the next writeable horizontal field
   * @param {HTMLElement} field Field
   * @param {HTMLElement} columnCount Column count
   * @return {HTMLElement} Next horizontal input field
   */
  getNextWriteableHorizontalField: function (field, columnCount) {
    var me = this;

    do {
      field = me.getNextHorizontalField(field, columnCount);
      if (!field) {
        return;
      }
    } while (field.readOnly);

    return field;
  },

  /**
   * @private
   * Returns the next horizontal field
   * @param {HTMLElement} field Field
   * @param {HTMLElement} columnCount Column count
   * @return {HTMLElement} Next horizontal input field
   */
  getNextHorizontalField: function (field, columnCount) {
    var me = this,
        fieldName, nextField, nodeList, i, nextNodeIndex;

    fieldName = field.name;
    nodeList = me.getInputsInRow(field);
    for (i = 0; i < nodeList.length; i++) {
      field = nodeList[i];
      if (field.name == fieldName) {
        nextNodeIndex = i + columnCount;
        if ((nextNodeIndex > -1) && (nextNodeIndex < nodeList.length)) {
          nextField = nodeList[nextNodeIndex];
          return nextField;
        }
      }
    }
  },

  /**
   * @private
   * Return the next writeable vertical input field
   * @param {String} baseFieldName Base field name
   * @param {String} fieldNameIndex Field name index
   * @param {String} rowCount Row count
   * @return {HTMLElement} Next vertical input field
   */
  getNextWriteableVerticalField: function (baseFieldName, fieldNameIndex, rowCount) {
    var me = this,
        field;

    do {
      field = me.getNextVerticalField(baseFieldName, fieldNameIndex, rowCount);
      if (!field) {
        return;
      }
      if (rowCount < 0) {
        rowCount--;
      } else {
        rowCount++;
      }
    } while (field.readOnly);

    return field;
  },

  /**
   * @private
   * Return the next vertical input field
   * @param {String} baseFieldName Base field name
   * @param {String} fieldNameIndex Field name index
   * @param {String} rowCount Row count
   * @return {HTMLElement} Next vertical input field
   */
  getNextVerticalField: function (baseFieldName, fieldNameIndex, rowCount) {
    var nextIndex, nextFieldName, nextField;
    nextIndex = fieldNameIndex + rowCount;
    if (nextIndex < 0) {
      return;
    }

    nextFieldName = baseFieldName + nextIndex;
    nextField = $var(nextFieldName);

    return nextField;
  },

  /**
   * Checks whether the pressed key is an arrow key
   * @param {Number} keyCode Key code;
   * @return {Boolean}
   */
  isArrowKey: function (keyCode) {
    var me = this,
        arrowKeyCodes, isArrowKey;

    arrowKeyCodes = [me.keyCodeArrowLeft, me.keyCodeArrowUp, me.keyCodeArrowRight, me.keyCodeArrowDown];
    isArrowKey = (arrowKeyCodes.indexOf(keyCode) > -1);
    return isArrowKey;
  },

  /**
   * Returns the parent table
   * @param {HTMLElement} element Element
   * @return {HTMLElement} Table
   */
  getParentTable: function (element) {
    element = element.parentNode;

    while (element) {
      if (element.id == "elotable") {
        return element;
      }
      element = element.parentNode;
    }
  },

  /**
   * Set CSS Classes .
   */
  setActionMode: function () {
    var mode = "",
        i, query, vars, pair;

    query = window.location.search.substring(1);
    vars = query.split("&");
    if (vars) {
      for (i = 0; i < vars.length; i++) {
        if (vars[i]) {
          pair = vars[i].split("=");
          if (pair[0] == "mode") {
            mode = pair[1];
            break;
          }
        }
      }
    }
    if (mode == "launchpad") {
      document.body.className += " actionMode hideMainHeader";
    }
  },

  /**
   * Initializing actions after loading the form.
   */
  initializeForm: function () {
    var me = this,
        $saveClicked,
        $onDynListItemSelected,
        $sendDirty;

    if (me.formIsInitialized === true) {
      return;
    }

    me.formIsInitialized = true;

    me.hideTabBarIfOnlyOneTab();
    me.setActionMode();
    me.setMainHeader();
    me.setMainFooter();
    me.setFullWidth();
    me.setResponsive();
    me.registerTableNavigation();

    $saveClicked = window.saveClicked;
    window.saveClicked = function () {
      $update(me.wfMapSavedFlag, "1", true);
      return $saveClicked();
    };

    if (window.onDynListItemSelected) {
      $onDynListItemSelected = window.onDynListItemSelected;
      window.onDynListItemSelected = function (item, name) {
        me.dynListItemSelected = true;
        $onDynListItemSelected.apply(this, arguments);
      };
    }

    $sendDirty = elo.elowf.CommUtils.sendDirty;
    elo.elowf.CommUtils.sendDirty = function () {
      me.pageIsDirty = true;
      $sendDirty.apply(this, arguments);
    };

  },

  /**
   * Initializes an index server connection and sets both elo.IX and elo.CONST.
   *
   * By default the relative url is used. If this url is not accesible for some reason the internal absolute url is tried instead.
   *
   * @param {Function} successCallback
   * @param {Function} failureCallback
   * @param {boolean} absoluteUrl defines wheather to use an absolute url or not. Defaults to false.
   */
  initializeIxSession: function (successCallback, failureCallback, absoluteUrl) {
    var me = this,
        ixUrl, ixLib, ixUtils, ixUtilsUrl, ixServiceUrl,
        onload = function () {
          elo = elo || {};
          try {
            ixServiceUrl = ixUrl + "/ix";
            console.info("establishing IX connection: " + ixServiceUrl);
            elo.connFact = new de.elo.ix.client.IXConnFactory(ixServiceUrl, "ELO WF", "Business Solution");
            elo.connFact.sessOpts = elo.connFact.sessOpts || {};
            elo.connFact.sessOpts["ix.translateTerms"] = "true";
            elo.connFact.sessOpts["ix.startDocMaskWorkflows"] = "true";
            elo.connFact.connProps = elo.connFact.connProps || {};
            elo.connFact.connProps[elo.connFact.NB_OF_REVERSE_CNNS] = 0;

            elo.IX = elo.connFact.createFromTicket(ELO_PARAMS.ELO_TICKET);
            elo.IX.getLoginResult().clientInfo.language = me.getFormLanguage();
            elo.CONST = elo.IX.getCONST();

            elo.data = elo.data || {};
            elo.data.user = {};
            elo.data.user.id = elo.IX.getUserId();
            elo.data.user.name = elo.IX.getUserName();
          } catch (ex) {
            me.initializedIxSession = false;
            alert("error initializing ix connection: " + (ex && ex.msg ? ex.msg : ex));
            return;
          }

          if (successCallback) {
            successCallback();
          }
        };

    // define IX Url
    ixUrl = ELO_PARAMS.ELOIX_PATH.substring(0, ELO_PARAMS.ELOIX_PATH.lastIndexOf("/"));
    if (!absoluteUrl) {
      ixUrl = ixUrl.substring(ixUrl.indexOf("/ix"));
    }

    if (me.initializedIxSession && elo && elo.IX) {
      if (successCallback) {
        successCallback();
      }
      return;
    } else if (me.initializedIxSession && (typeof de !== "undefined") && de.elo && de.elo.ix && de.elo.ix.client) {
      onload();
    } else if (me.initializedIxSession) {
      window.setTimeout(function () {
        me.initializeIxSession(successCallback);
      }, 50);
      return;
    }

    me.initializedIxSession = true;


    ixLib = document.createElement("script");
    ixLib.type = "text/javascript";
    ixUtilsUrl = ixUrl + "/EloixClient-min.js";

    console.info("loading ix utils: " + ixUtilsUrl);
    ixLib.src = ixUtilsUrl;
    ixLib.onerror = function (event) {
      var src, message;
      src = (event && event.srcElement && event.srcElement.src) ? event.srcElement.src : "";
      message = "Error while loading IX library: " + src;
      if (!absoluteUrl) {
        console.info("Initializing session with absolute url failed. Using relative url instad.");
        me.initializedIxSession = false;
        me.initializeIxSession(successCallback, failureCallback, true);
      } else if (failureCallback) {
        failureCallback(message);
      } else {
        eloAlert(message, "Error");
      }
    };
    ixLib.onload = onload;

    if (!document.getElementById("sol_common_IxUtils")) {
      ixUtils = document.createElement("script");
      ixUtils.id = "sol_common_IxUtils";
      ixUtils.type = "text/javascript";
      ixUtils.src = "lib_sol.common.IxUtils";
      document.body.appendChild(ixUtils);
    }
    document.body.appendChild(ixLib);
  },

  /**
   * Registers an element for update.
   *
   * To do so, it calls the rule defined by `ruleName` with the wf objects objId (as `param1`).
   *
   * @since 1.05.000 this can be called without a `ruleName` to register the element for direct update (without using the job queue). The common_monitoring package hast to be installed.
   *
   * Update will only be registered, if the page has changed.
   * This only applies if the form was initialized using {@link #initializeForm}.
   * If not, the update will always be executed.
   *
   * @param {String} ruleName
   */
  registerUpdate: function (ruleName) {
    var me = this,
        req, url, rfName, rfParams, rfErrorMsg;

    if ((me.formIsInitialized !== true) || (me.pageIsDirty === true) || (me.dynListItemSelected === true)) {
      if (ELO_PARAMS.WF_MAP_COMMON_SKIP_UPDATE_REGISTRATION === "true") {
        console.info("Skip update registration: 'WF_MAP_COMMON_SKIP_UPDATE_REGISTRATION' is set to 'true'");
        return;
      }
      if (me.checkVersion(ELOWF_VERSION, "10.01.000")) {
        if (ruleName) {
          rfName = "RF_sol_common_service_ExecuteAsAction";
          rfParams = {
            action: ruleName,
            objId: ELO_PARAMS.ELO_OBJID,
            config: {}
          };
          rfErrorMsg = "Failed to call ELOas rule '" + ruleName + "' via '" + rfName + "'";
        } else {
          rfName = "RF_sol_common_monitoring_function_RegisterUpdate";
          rfParams = {
            objId: ELO_PARAMS.ELO_OBJID,
            registerFlowId: ELO_PARAMS.ELO_FLOWID,
            registerNodeId: ELO_PARAMS.ELO_NODEID
          };
          rfErrorMsg = "Failed to register update using '" + rfName + "'";
        }

        me.callRegisteredFunction(
          rfName,
          rfParams,
          function (succObj) {
            console.info("Registered update for '" + rfParams.objId + "'");
          },
          function (errObj) {
            console.error(rfErrorMsg + " : " + JSON.stringify(errObj));
          }
        );
      } else {
        req = new XMLHttpRequest();
        url = me.getAsBaseUrl() + "?cmd=get&name=" + ruleName + "&param1=" + ELO_PARAMS.ELO_OBJID + "&ticket=" + ELO_PARAMS.ELO_TICKET;
        req.open("GET", url, true);
        req.onreadystatechange = function () {
          if (req.readyState !== 4) {
            return;
          }
          if (req.status !== 200) {
            console.error("Failed to call ELOas rule '" + ruleName + "' via XMLHttpRequest: statusCode=" + req.status);
          }
        };
        req.send(null);
      }
    }
  },

  /**
   * Returns the ELOas base URL
   * @return {String} ELOas base URL
   */
  getAsBaseUrl: function () {
    var me = this,
        asUrl, formLocation, asLocation, asProxyUrl;

    asUrl = ELO_PARAMS.ELOAS_PATH;
    formLocation = window.location;

    if (!formLocation.port) {
      asLocation = me.getLocation(asUrl);
      if (asLocation.port) {
        asProxyUrl = formLocation.protocol + "//" + formLocation.host + asLocation.path;
        return asProxyUrl;
      }
    }

    return asUrl;
  },

  /**
   * Gets the location info from a given URL
   * @param {String} url URL
   * @return {Object} urlParts
   * @return {String} location.url Complete URL
   * @return {String} location.protocol Protocol
   * @return {String} location.hostName Host name
   * @return {String} location.port Port
   * @return {String} location.path Path
   */
  getLocation: function (url) {
    var location,
        parts, hostParts;

    location = {
      url: url
    };

    if (url) {
      parts = url.split("/");
      if ((parts.length > 2) && (parts[0].toLowerCase().indexOf("http") == 0) && !parts[1]) {
        location.protocol = parts[0];
        hostParts = parts[2].split(":");
        location.hostName = hostParts[0];
        if (hostParts.length > 1) {
          location.port = hostParts[1];
        }
        if (parts.length > 3) {
          parts.shift();
          parts.shift();
          parts.shift();
          location.path = "/" + parts.join("/");
        }
      }
    }

    return location;
  },

  /**
   * Returns the language of the form
   * @returns {String} Language
   */
  getFormLanguage: function () {
    return document.querySelector("input[name=lang]").getAttribute("value");
  },

  /**
   * Check wether mandatory fields exist
   * @param {Array} fieldNames
   */
  checkMandatoryFields: function (fieldNames) {
    var me = this,
        missingFields = [],
        i, fieldName, content;

    if (!fieldNames || (fieldNames.length == 0)) {
      return;
    }

    for (i = 0; i < fieldNames.length; i++) {
      fieldName = fieldNames[i];
      if (!me.fieldExists(fieldName)) {
        missingFields.push(fieldName);
      }
    }

    if (missingFields.length > 0) {
      content = "Missing fields:\r\n" + missingFields.join("\r\n");
      eloAlert(content, "Missing fields");
    }
  },

  /**
   * Checks wether a field exists
   * @param {String} fieldName
   * @return {Boolean}
   */
  fieldExists: function (fieldName) {
    if (!fieldName) {
      throw "Field name is empty";
    }
    return !!$var(fieldName);
  },

  /**
   * Disables the validation if cancel button is pressed
   *
   * ### Example:
   *     function nextClicked(id) {
   *       sol.common.forms.Utils.disableCancelButtonValidation(id, ["sol.common.wf.node.cancel"]);
   *       return true;
   *     }
   *
   * @param {String} successorNodeId Node ID of the successor node
   * @param {Array} cancelButtonTranslationKeys Cancel button translation key
   * @param {Object} params Parameters
   * @param {String} [params.mode=REMOVE_TABLE] Mode: `REMOVE_VALIDATION_ATTRIBUTES` deletes only the validation attributes
   * @return {Boolean}
   */
  disableCancelButtonValidation: function (successorNodeId, cancelButtonTranslationKeys, params) {
    var me = this,
        i, nextButtonIndex, pressedNextButtonTranslationKey, cancelButtonTranslationKey;

    params = params || {};

    if (!successorNodeId) {
      console.warn("Successor node ID is empty");
    }

    if (!cancelButtonTranslationKeys) {
      console.warn("Cancel button translation keys are empty");
      return;
    }

    if (!sol.common.ObjectUtils.isArray(cancelButtonTranslationKeys)) {
      console.warn("Cancel button translation keys must be an array");
      return;
    }

    nextButtonIndex = me.getNextButtonIndex(successorNodeId);
    if (!nextButtonIndex) {
      console.warn("Next button not found");
      return;
    }

    pressedNextButtonTranslationKey = ELO_PARAMS["KEY_NEXT_" + nextButtonIndex];

    for (i = 0; i < cancelButtonTranslationKeys.length; i++) {
      cancelButtonTranslationKey = cancelButtonTranslationKeys[i];
      if (pressedNextButtonTranslationKey == cancelButtonTranslationKey) {
        if (params && params.mode && (params.mode.indexOf("REMOVE_VALIDATION_ATT") == 0)) {
          me.removeValidationAttributes();
        } else {
          me.removeTables();
        }
        me.removeHiddenCheckboxes();
        me.setSuccessor(successorNodeId);
        return true;
      }
    }
    return false;
  },

  /**
   * @private
   * Deletes the validation attributes
   */
  removeValidationAttributes: function () {
    var inputs, i, input, eloverify;

    inputs = document.querySelectorAll("input, textarea");

    for (i = 0; i < inputs.length; i++) {
      input = inputs[i];
      eloverify = input.getAttribute("eloverify");
      if (eloverify) {
        eloverify = eloverify.replace("notemptyforward");
        eloverify = eloverify.replace(/min\:\d+/, "");
        eloverify = eloverify.replace(/max\:\d+/, "");
        input.setAttribute("eloverify", eloverify);
      }
    }
  },

  /**
   * Remove all tables within the form to avoid validation
   */
  removeTables: function () {
    var tables, table, i;
    tables = document.querySelectorAll("#elotable table");
    for (i = 0; i < tables.length; i++) {
      table = tables[i];
      table.parentNode.removeChild(table);
    }
  },

  /**
   * Remove all checkboxes within the form to avoid validation
   */
  removeHiddenCheckboxes: function () {
    var checkboxes, checkbox, i;
    checkboxes = document.querySelectorAll("input[name^='HIDDEN_'][value='CHECKBOX']");
    for (i = 0; i < checkboxes.length; i++) {
      checkbox = checkboxes[i];
      checkbox.parentNode.removeChild(checkbox);
    }
  },


  /**
   * Set successor node
   * @param {String} successorNodeId Successor node id
   */
  setSuccessor: function (successorNodeId) {
    var nextNodeElem;
    nextNodeElem = document.querySelector("input[name='NEXTNODE']");
    if (nextNodeElem) {
      nextNodeElem.setAttribute("value", successorNodeId);
    }
  },

  /**
   * Returns the translation key of the next button by a given nodeId
   * @param {String} nodeId
   * @return {String} Translation key
   */
  getNextButtonTranslationKey: function (nodeId) {
    var me = this,
        nextButtonIndex, nextButtonTranslationKey;

    nextButtonIndex = me.getNextButtonIndex(nodeId);
    if (!nextButtonIndex) {
      console.warn("Next button not found");
      return;
    }

    nextButtonTranslationKey = ELO_PARAMS["KEY_NEXT_" + nextButtonIndex];

    return nextButtonTranslationKey;
  },

  /**
   * Returns the index of the next button
   * @param {String} successorNodeId Node ID of the successor node
   * @return {Number} Index of the next button
   */
  getNextButtonIndex: function (successorNodeId) {
    var nextProp, nextPropParts, i, nodeId,
        maxButtonId = 100;
    for (i = 1; i < maxButtonId; i++) {
      nextProp = ELO_PARAMS["NEXT_" + i];
      if (!nextProp) {
        break;
      }
      nextPropParts = nextProp.split("\t");
      if (nextPropParts && (nextPropParts.length > 0)) {
        nodeId = nextPropParts[0].trim();
        if (nodeId == successorNodeId) {
          return i;
        }
      }
    }
  },

  /**
   * generates a user image url relative to the current form.
   * @param {String} username ELO Username
   * @param {String} userid ELO User id
   * @return {String}
   */
  getUserImageUrl: function (username, userid) {
    return "../social/api/feed/img/users/" + userid + "/" + username + "/" + new Date().getTime();
  },

  /**
   * Renders images for a given user set. Internally calls applyUserImage on each entry of the map.
   * If imageTargetField is not set, userNameMap will be used as imageTargetField.
   * @param {String} userNameMap Name of the map field that contains the username
   * @param {String} userIdMap Name of the field that contains the user id
   * @param {String} imageTargetField Name of the field in which to show the userimage
   */
  applyUserImages: function (userNameMap, userIdMap, imageTargetField) {
    var i = 1;
    imageTargetField = imageTargetField || userNameMap;

    while ($var(userNameMap + i)) {
      this.applyUserImage(userNameMap + i, userIdMap + i, imageTargetField + i);
      i += 1;
    }
  },

  /**
   * Renders a user image
   * If imageTargetField is not set, userNameMap will be used as imageTargetField.
   * @param {String} userNameMap Name of the map field that contains the username
   * @param {String} userIdMap Name of the field that contains the user id
   * @param {String} imageTargetField Name of the field in which to show the userimage
   */
  applyUserImage: function (userNameMap, userIdMap, imageTargetField) {
    var img, cont;
    imageTargetField = imageTargetField || userNameMap;

    if ($val(userNameMap) && $val(userIdMap)) {
      img = img = sol.common.forms.Utils.getUserImageUrl($val(userNameMap), $val(userIdMap));
    } else {
      img = "";
    }

    cont = document.querySelector('[name="' + imageTargetField + '"]');
    if (cont) {
      cont = cont.parentElement;
      cont.style.backgroundImage = img ? "url('" + img + "')" : "";
    }
  },

  /**
   * Returns the surrounding table
   * @param {HTMLElement} element HTML element
   * @return {HTMLElement}
   */
  getTable: function (element) {
    var me = this;
    if (!element) {
      throw "Element is empty";
    }
    return me.getParentByTagName(element, "TABLE");
  },

  /**
   * Returns the surrounding row
   * @param {HTMLElement} element HTML element
   * @return {HTMLElement}
   */
  getRow: function (element) {
    var me = this;
    if (!element) {
      throw "Element is empty";
    }
    return me.getParentByTagName(element, "TR");
  },

  /**
   * Returns the surrounding cell
   * @param {HTMLElement} element HTML element
   * @return {HTMLElement}
   */
  getCell: function (element) {
    var me = this;
    if (!element) {
      throw "Element is empty";
    }
    return me.getParentByTagName(element, "TD");
  },

  /**
   * Returns the template
   * @param {HTMLElement} element HTML element
   * @return {HTMLElement}
   */
  getTemplate: function (element) {
    var me = this;
    if (!element) {
      throw "Element is empty";
    }
    return me.getParentByTagName(element, "tbody");
  },

  /**
   * Returns a parent element by a given tag name
   * e.g. to find the surrounding table of an element
   * @param {HTMLElement} element HTML element.
   * @param {String} tagName Tag name of the wanted element
   * @return {HTMLElement}
   */
  getParentByTagName: function (element, tagName) {
    if (!element) {
      throw "Start element is empty";
    }
    if (!tagName) {
      throw "Tag name is empty";
    }
    do {
      element = element.parentElement;
      if (!element) {
        return;
      }
      if (element.nodeName.toLowerCase() == tagName.toLowerCase()) {
        return element;
      }
    } while (element);
  },

  /**
   * Returns the "JS add line" button
   * @param {String|HTMLElement} field Field
   * @return {HTMLElement}
   */
  getJsAddLineButton: function (field) {
    var me = this,
        element, editTable, jsAddLineButton;
    if (!field) {
      throw "Field is empty";
    }

    element = (typeof field == "string") ? $var(field) : field;

    if (!element) {
      return;
    }
    editTable = me.getTable(element);
    if (!editTable) {
      return;
    }
    jsAddLineButton = editTable.querySelector("input[name='JS_ADDLINE']");
    if (!jsAddLineButton) {
      throw "'JS add line button' not found";
    }
    return jsAddLineButton;
  },

  /**
   * Returns the "JS add line" button
   * @param {String} addLineId Add line ID
   * @return {HTMLElement}
   */
  getJsAddLineButtonById: function (addLineId) {
    var jsAddLineButton;

    if (!addLineId) {
      return;
    }

    jsAddLineButton = document.querySelector("input[name='JS_ADDLINE'][addlineid='" + addLineId + "']");

    if (!jsAddLineButton) {
      throw "'JS add line button' not found";
    }

    return jsAddLineButton;
  },

  /**
   * Adds a listener to an JsAddLineButton
   * @param {String} fieldName Field name within the JS add line table
   * @param {Function} func Function
   */
  addJsAddLineButtonListener: function (fieldName, func) {
    var me = this,
        jsAddLineButton;
    if (!fieldName) {
      throw "Field name is empty";
    }
    if (!func) {
      throw "Function is empty";
    }
    jsAddLineButton = me.getJsAddLineButton(fieldName);
    if (jsAddLineButton) {
      jsAddLineButton.addEventListener("click", func);
    }
  },

  /**
   * Registers an click handler to clear a new line
   * @param {String} columnName Column name
   * @param {String} notEmpty Previous line must not be empty
   */
  registerResetNewLineFunction: function (columnName, notEmpty) {
    var me = this;

    if (!columnName) {
      throw "The column name is empty";
    }
    if (!me.columnExists(columnName)) {
      return;
    }

    me.addJsAddLineButtonListener(columnName + "1", function () {
      me.resetNewLine(columnName, notEmpty);
    });
  },

  /**
   * Resets all input fields of a new line
   * @param {String} columnName Column name
   * @param {String} notEmpty Previous line must not be empty
   */
  resetNewLine: function (columnName, notEmpty) {
    var me = this,
        jsAddLineButton, lastInput, index;

    if (!columnName) {
      throw "The column name is empty";
    }
    if (!me.columnExists(columnName)) {
      return;
    }

    lastInput = me.getLastInput(columnName);
    if (notEmpty) {
      index = me.getFieldNameIndex(lastInput.name);
      if (!$val(columnName + (index - 1))) {
        jsAddLineButton = me.getJsAddLineButton(lastInput.name);
        $removeLine(jsAddLineButton, index);
        return;
      }
    }
    me.enableRow(lastInput, true);
  },

  /**
   * Adds a callback function to the event listener of an input field.
   * @param {string} event Name of the event.
   * @param {string} fieldKey Field key of the input field.
   * @param {object} ctx Exteution context.
   * @param {function} func Callback function.
   */
  addListener: function (event, fieldKey, ctx, func) {
    var field = $var(ctx.field(fieldKey)),
        handlerFct;

    if (!field) {
      return;
    }

    handlerFct = function () {
      func.apply(ctx, arguments);
    };

    if (field.addEventListener) { // Modern
      field.addEventListener(event, handlerFct, false);
    } else if (field.attachEvent) { // Internet Explorer
      field.attachEvent("on" + event, handlerFct);
    }
  },

  /**
   * Returns true, if the fieldname is a table field, otherwise false
   * @param {String} s the fieldname
   * @return {Boolean}
   */
  isTableField: function (s) {
    return (typeof s == "string") &&
      !!s[1] &&
      ((s = +(s[s.length - 1])) === s);
  },

  /**
   * Returns the field index
   * @param {String} fieldName Field name
   * @returns {String}
   */
  getFieldNameIndex: function (fieldName) {
    if (!fieldName) {
      return "";
    }
    var pos = fieldName.search(/\d+$/);
    if (pos > 0) {
      return parseInt(fieldName.substring(pos), 10);
    }
    return "";
  },

  /**
   * Returns the index number of the row
   * @param {HTMLElement} field Field
   * @return {String} Row index
   */
  getRowIndex: function (field) {
    var me = this,
        row, inputs, i, input, fieldName, index;

    if (!field) {
      throw "Field is empty";
    }

    row = me.getRow(field);
    if (!row) {
      throw "Row not found";
    }

    inputs = row.querySelectorAll("input");

    for (i = 0; i < inputs.length; i++) {
      input = inputs[i];
      fieldName = input.name;
      index = me.getFieldNameIndex(fieldName);
      if (index) {
        return index;
      }
    }
  },

  /**
   * Returns the field name without trailing numbers
   * @param {Object|String} source Source element or field name
   * @param {Object} params Parameters
   * @param {String} [params.replaceNumberBy=""] Replace the number by this string
   * @returns {String}
   */
  getFieldNamePrefix: function (source, params) {
    var me = this,
        fieldName;

    if (me.checkSource(source)) {
      fieldName = source.name;
    } else {
      fieldName = source;
    }

    params = params || {};
    params.replaceNumberBy = params.replaceNumberBy || "";

    if (!fieldName) {
      return "";
    }
    fieldName = fieldName.replace(/\d*$/, params.replaceNumberBy);

    return fieldName;
  },

  /**
   * Checks whether a column exists
   * @param {String} columnName Column name
   * @return {Boolean}
   */
  columnExists: function (columnName) {
    if (!columnName) {
      throw "The column name is empty";
    }

    return !!$var(columnName + "1");
  },

  /**
   * Return the last input field in the table
   * @param {String} columnName
   * @return {HTMLElement}
   */
  getLastInput: function (columnName) {
    var currentInput, lastInput,
        i = 1;

    if (!columnName) {
      throw "The column name is empty";
    }

    while (true) {
      currentInput = $var(columnName + i++);
      if (currentInput) {
        lastInput = currentInput;
      } else {
        return lastInput;
      }
    }
  },

  /**
   * Sets rows read-only
   * @param {String} indicatorColumnName Read-only indicator field
   * @param {Function} func Function that must return a boolean to determinate whether the row should be disabled
   */
  disableRowsByIndicatorField: function (indicatorColumnName, func) {
    var me = this;

    if (!indicatorColumnName) {
      throw "The indicator column name is empty";
    }
    if (!$var(indicatorColumnName + "1")) {
      return;
    }

    me.forEachRow(indicatorColumnName, function (rowIndex) {
      var indicatorElement, disable;
      disable = func.call(me, rowIndex);
      indicatorElement = $var(indicatorColumnName + rowIndex);
      me.enableRow(indicatorElement, !disable);
    });
  },


  /**
   * Enables/disables a row
   * @param {type} indicatorElement
   * @param {type} enable
   */
  enableRow: function (indicatorElement, enable) {
    var me = this,
        nodeList, j, row, removeLineButton;
    nodeList = me.getElementsInRow(indicatorElement);
    for (j = 0; j < nodeList.length; j++) {
      nodeList[j].disabled = !enable;
    }
    if (!enable) {
      row = me.getRow(indicatorElement);
      removeLineButton = row.querySelector(".jsRemoveLine");
      if (removeLineButton) {
        removeLineButton.parentNode.removeChild(removeLineButton);
      }
    }
  },

  /**
   * Returns the input fields and buttons in the row
   * @param {HTMLElement} element
   * @return {NodeList}
   */
  getElementsInRow: function (element) {
    var me = this,
        row, nodeList;
    if (!element) {
      throw "Element is empty";
    }
    row = me.getRow(element);
    if (!row) {
      throw "Row not found";
    }
    nodeList = row.querySelectorAll("input,button");
    return nodeList;
  },

  /**
   * Returns the input fields in the row
   * @param {HTMLElement} element
   * @return {NodeList}
   */
  getInputsInRow: function (element) {
    var me = this,
        row, nodeList;
    if (!element) {
      throw "Element is empty";
    }
    row = me.getRow(element);
    if (!row) {
      throw "Row not found";
    }
    nodeList = row.querySelectorAll("input");
    return nodeList;
  },

  /**
   * Returns the JS add line row by ID
   * @param {String} addLineId Add line ID
   * @param {String} lineNo Line number
   * @return {HTMLElement} row
   */
  getJsAddLineRowById: function (addLineId, lineNo) {
    var me = this,
        jsAddLineButton, inputs, index, row;

    jsAddLineButton = me.getJsAddLineButtonById(addLineId);

    if (!jsAddLineButton) {
      throw "Can't find JS add line button";
    }

    row = me.getParentByTagName(jsAddLineButton, "tr");

    while (true) {
      row = row.previousElementSibling;
      if (!row) {
        return;
      }

      inputs = row.querySelectorAll("input");
      if (inputs.length == 0) {
        continue;
      }
      index = me.getFieldNameIndex(inputs[0].name);

      if (index == lineNo) {
        return row;
      } else if (index < lineNo) {
        return;
      }
    }
  },

  /**
   * Iterates over a table.
   * @param {String} endOfTableIndicatorColumnName Name of a column to check if the line exists.
   * @param {Function} func Callback function for the iteration.
   * @param {Object} ctx Execution context.
   */
  forEachRow: function (endOfTableIndicatorColumnName, func, ctx) {
    var me = this,
        i, lines;

    if (!endOfTableIndicatorColumnName) {
      throw "The end of table indicator column name is empty.";
    }
    if (!func) {
      throw "The function parameter is emtpy.";
    }

    lines = me.getLines(endOfTableIndicatorColumnName);
    for (i = 0; i < lines.length; i++) {
      func.call(ctx, i + 1, lines[i]);
    }
  },

  /**
   * Calls the function `inputChanged` for fields changed by a dynamic keyword list
   * @param {Object} dynListItem Dynamic keyword list item
   */
  callInputChangedForDynKwlChanges: function (dynListItem) {
    var fieldName, fieldNameParts, prefix, field;
    for (fieldName in dynListItem) {
      fieldNameParts = fieldName.split("_");
      if (fieldNameParts && (fieldNameParts.length > 0)) {
        prefix = fieldNameParts[0];
        if (!prefix || (prefix.substr(0, 1) == "$")) {
          continue;
        }
        if ((prefix != "IX") && (prefix != "WF")) {
          fieldName = "IX_GRP_" + fieldName;
        }
      }
      field = $var(fieldName);
      if (field) {
        inputChanged($var(fieldName));
      }
    }
  },

  /**
   * Enables or disables forward buttons
   * @param {Boolean} [disable=true] Disable button
   */
  disableForwardButtons: function (disable) {
    var me = this,
        forwardButtons, i, forwardButton;
    forwardButtons = me.getForwardButtons();
    for (i = 0; i < forwardButtons.length; i++) {
      forwardButton = forwardButtons[i];
      forwardButton.disabled = (disable == false) ? false : true;
    }
  },

  /**
   * Returns forward buttons
   * @returns {NodeList}
   */
  getForwardButtons: function () {
    var forwardButtons;
    forwardButtons = document.querySelectorAll("button[name='NEXTNODE']");
    return forwardButtons;
  },

  /**
   * Returns a next button
   * @param {Object} params Parameters
   * @param {String} params.nextNodeTranslationKey Translation key of the next node
   * @return {HTMLElement} Forward button
   */
  getForwardButton: function (params) {
    var me = this,
        buttonIndex, forwardButtons, i, translationKey, forwardButton;

    params = params || {};

    if (params.nextNodeTranslationKey) {
      for (i = 1; i < 1000; i++) {
        translationKey = ELO_PARAMS["KEY_NEXT_" + i];
        if (!translationKey) {
          return;
        }
        if (translationKey == params.nextNodeTranslationKey) {
          buttonIndex = i - 1;
          break;
        }
      }
    }

    if (buttonIndex != undefined) {
      forwardButtons = me.getForwardButtons();
      if (forwardButtons && (buttonIndex < forwardButtons.length)) {
        forwardButton = forwardButtons[buttonIndex];
      }
    }

    return forwardButton;
  },

  /**
   * Adds a class to the ´body´ tag
   * @param {String} className
   */
  addBodyClass: function (className) {
    document.body.className += " " + className;
  },

  /**
   * Removes a class from the ´body´ tag
   * @param {String} className
   */
  removeBodyClass: function (className) {
    document.body.classList.remove(className);
  },

  /**
   * Returns an ISO formatted date
   * @param {String} fieldName Field name
   * @param {Object} params Parameters
   * @param {Boolean} params.startOfDay Start of day
   * @returns {String}
   */
  getIsoDate: function (fieldName, params) {
    var field, isoDate;

    params = params || {};

    if (!$val(fieldName)) {
      return "";
    }
    field = $var(fieldName);
    if (!field) {
      return;
    }
    isoDate = field.getAttribute("isodate") || "";
    if (params.startOfDay || (isoDate.length > 8)) {
      isoDate = isoDate.substr(0, 8) + "000000";
    }

    return isoDate;
  },

  /**
   * Sets an ISO date
   * @param {String} fieldName Field name
   * @param {String} isoDate in ISO format
   * @param {Object} params Parameters
   * @param {Boolean} params.callInputChanged If true the function ´inputChanged´ will be called
   */
  setIsoDate: function (fieldName, isoDate, params) {
    var localizedDate, field;
    params = params || {};
    params.callInputChanged = (typeof params.callInputChanged == "undefined") ? true : params.callInputChanged;
    field = $var(fieldName);
    if (!field) {
      return;
    }
    localizedDate = elo.wf.date.format(isoDate) || "";
    field.setAttribute("autovalidval", localizedDate);
    $update(fieldName, localizedDate);
    if (params.callInputChanged) {
      inputChanged($var(fieldName));
    }
  },

  /**
   * Copies an ISO date
   * @param {String} srcFieldName Source field name
   * @param {String} dstFieldName Destination field name
   * @param {Object} params Parameters
   * @param {Boolean} params.callInputChanged If true the function ´inputChanged´ will be called
   */
  copyIsoDate: function (srcFieldName, dstFieldName, params) {
    var me = this,
        value, isoDate;
    if (!$var(srcFieldName) || !$var(dstFieldName)) {
      return;
    }
    value = $val(srcFieldName);
    if (!value) {
      $update(dstFieldName, "");
      return;
    }
    isoDate = me.getIsoDate(srcFieldName);
    me.setIsoDate(dstFieldName, isoDate, params);
  },

  /**
   * Sets a default value if it's empty
   * @param {String} fieldName Field name
   * @param {String} defaultValue Default value
   */
  setDefaultValue: function (fieldName, defaultValue) {
    var value;
    value = $val(fieldName);
    if (!value) {
      $update(fieldName, defaultValue);
    }
  },

  /**
   * Checks if the specific field has been changed or the form is loaded
   * @param {Object} source Changed HTML element.
   * @param {Array} fieldNames Array of field names.
   * @return {undefined}
   */
  isFieldChangedOrFormLoaded: function (source, fieldNames) {
    var me = this;
    if (!me.checkSource(source)) {
      return true;
    }
    return me.isFieldChanged(source, fieldNames);
  },

  /**
   * Checks if the specific field has been changed
   * @param {Object} source Changed HTML element.
   * @param {Array} fieldNames Array of field names.
   * @return {undefined}
   */
  isFieldChanged: function (source, fieldNames) {
    var me = this;
    if (!source) {
      return false;
    }
    return fieldNames.some(function (fieldName) {
      var fieldBaseName = me.getBaseFieldName(source.name);
      return ((fieldBaseName == fieldName) || (fieldBaseName + me.unitSuffix == fieldName));
    });
  },

  /**
   * Checks if the changed input field es set
   * @param {Object} source Changed input field
   * @return {Boolean}
   */
  checkSource: function (source) {
    return (source && source.name);
  },

  /**
   * Returns the field name without unit suffix and trailing numbers
   * @param {Object|String} source Source element or field name
   * @returns {String}
   */
  getBaseFieldName: function (source) {
    var me = this,
        fieldName, unitSuffixRegExp;

    if (me.checkSource(source)) {
      fieldName = source.name;
    } else {
      fieldName = source;
    }

    if (!fieldName) {
      return "";
    }
    fieldName = fieldName.replace(/\d*$/g, "");
    unitSuffixRegExp = new RegExp(me.unitSuffix + "$");
    fieldName = fieldName.replace(unitSuffixRegExp, "");
    return fieldName;
  },

  /**
   * Sums all values of a given column
   * @param {string} columnName Name of the column
   * @return {Number}
   */
  sumItems: function (columnName) {
    var me = this,
        sum;
    sum = new Decimal(0);
    me.forEachRow(columnName, function (i, field) {
      var decimal;
      decimal = me.toDecimal(field);
      sum = sum.plus(decimal);
    });
    return sum;
  },

  /**
   * Returns a Decimal data type
   * @param {String|HTMLElement} field Field
   * @param {Object} params Parameters
   * @param {Boolean} params.silent Silent
   * @return {Decimal} Decimal
   */
  toDecimal: function (field, params) {
    var decimal, value;

    if (!field) {
      throw "Field is empty";
    }

    params = params || {};

    if (!(field instanceof Element)) {
      field = $var(field);
    }

    if (!field) {
      if (params.silent) {
        return;
      }
      throw "Field '" + field + "' not found.";
    }

    if (field.hasAttribute("decimal")) {
      value = field.getAttribute("decimal");
    } else {
      value = field.value;
      params.thousandsSeparator = ELO.Configuration.Amount.ThousandSep;
      params.decimalSeparator = ELO.Configuration.Amount.DecimalSep;
    }

    value = (!!value) ? value : "0";
    decimal = sol.common.DecimalUtils.toDecimal(value, params);
    return decimal;
  },

  /**
   * Write a Decimal into an input field
   * @param {String} fieldName Field name
   * @param {Decimal} decimal Decimal
   * @param {Object} params Parameters
   * @param {Boolean} params.silent Silent
   * @param {Boolean} params.callInputChanged If true the function ´inputChanged´ will be called
   * @param {Function} params.roundingCurrencyCode Rounding currency code
   */
  updateDecimal: function (fieldName, decimal, params) {
    var me = this,
        field, decimalPlaces;

    params = params || {};

    if (!decimal) {
      if (params.silent) {
        return;
      }
      throw "Decimal is emtpy";
    }
    if (!fieldName) {
      throw "Field name is empty";
    }

    if (!(fieldName instanceof Element)) {
      field = $var(fieldName);
    } else {
      field = fieldName;
    }

    if (!field) {
      if (params.silent) {
        return;
      }
      throw "Field '" + fieldName + "' not found.";
    }

    decimalPlaces = me.getDisplayDecimalPlaces(field);
    me.writeDecimalField(field, decimal, decimalPlaces, params);
  },

  /**
   * Parse to Decimal
   * @param {String} str Number as String
   * @return {Decimal} Decimal
   */
  parseDecimal: function (str) {
    var decimal;

    str = str || "";

    str = str.split(" ").join("");

    if (str == "null") {
      str = "0";
    }
    str = str.split(ELO.Configuration.Amount.ThousandSep).join("");
    str = str.replace(ELO.Configuration.Amount.DecimalSep, ".");

    decimal = new Decimal(str);

    return decimal;
  },

  /**
   * Normalize decimals
   * @param {HTMLElement} field Field
   * @param {Object} params Params
   */
  normalizeDecimal: function (field, params) {
    var me = this,
        decimalPlaces, decimal, fieldName, inputs, i, input;

    if (!field) {
      return;
    }

    if (!me.isNumeric(field)) {
      return;
    }

    fieldName = field.name;

    inputs = document.querySelectorAll("input[name='" + fieldName + "']");

    for (i = 0; i < inputs.length; i++) {
      input = inputs[i];
      input.removeAttribute("decimal");
    }

    if (field.value == "") {
      return;
    }

    decimal = me.toDecimal(field);

    me.writeDecimalField(field, decimal, decimalPlaces, params);
  },

  /**
   * Writes a Decimal
   * @private
   * @param {HTMLElement} field Field
   * @param {Decimal} decimal Decimal
   * @param {Number} decimalPlaces Decimal places
   * @param {Object} params Parameters
   * @param {Boolean} params.callInputChanged If true the function ´inputChanged´ will be called
   * @param {Function} params.roundingCurrencyCode Rounding currency code
   * @param {Function} params.roundingFunction Rounding function
   */
  writeDecimalField: function (field, decimal, decimalPlaces, params) {
    var me = this,
        value, inputs, i, input;

    params = params || {};

    inputs = document.querySelectorAll("input[name='" + field.name + "']");

    if (params.roundingCurrencyCode) {
      params.roundingFunction = me.roundingFunctions[params.roundingCurrencyCode.toUpperCase()];
    }

    if (params.roundingFunction) {
      decimal = params.roundingFunction.call(me, decimal);
    }

    for (i = 0; i < inputs.length; i++) {
      input = inputs[i];
      input.setAttribute("decimal", decimal.toString());
    }

    decimalPlaces = (typeof decimalPlaces != "undefined") ? decimalPlaces : me.getDisplayDecimalPlaces(field);

    value = decimal.toDecimalPlaces(decimalPlaces).toFixed(decimalPlaces);
    value = value.replace(".", ELO.Configuration.Amount.DecimalSep);
    if (value == "Infinity") {
      value = "";
    }
    $update(field.name, value);

    if (params.callInputChanged) {
      inputChanged(field);
    }
  },

  /**
   * Return display decimal decimalPlaces
   * @param {HTMLElement} field Field
   * @return {String}
   */
  getDisplayDecimalPlaces: function (field) {
    var eloverifyString, match, result;
    if (!field) {
      throw "Field is emtpy";
    }
    eloverifyString = field.getAttribute("eloverify");
    if (eloverifyString) {
      match = eloverifyString.match(/nk:(\d+)/);
      if (match && (match.length == 2)) {
        result = parseInt(match[1], 10);
        return result;
      }
    }
  },

  /**
   * Shows or hides columns
   * @param {Array} columnNames Column names
   * @param {Boolean} show Show columns
   * @param {Object} params Parameters
   */
  showColumns: function (columnNames, show, params) {
    var me = this,
        i, columnName;
    for (i = 0; i < columnNames.length; i++) {
      columnName = columnNames[i];
      me.showColumn(columnName, show, params);
    }
  },

  /**
   * Show or hides a column
   * @param {String} columnName Column name
   * @param {Boolean} show Show column
   * @param {Object} params Parameters
   * @param {Boolean} [params.clear=false] Clear fields
   * @param {Boolean} [params.numberOfHeaderLines=1] Number of header lines
   */
  showColumn: function (columnName, show, params) {
    var me = this,
        field, columnIndex, addButton, tr, td, input, i;

    if (!columnName) {
      throw "Column name is empty";
    }

    params = params || {};
    params.clear = (typeof params.clear == "undefined") ? false : true;
    params.numberOfHeaderLines = params.numberOfHeaderLines || 1;

    field = $var(columnName + "1");

    if (!field) {
      return;
    }

    field = $var(columnName + "1");
    columnIndex = me.getColumnIndex(field);

    if (columnIndex > -1) {
      tr = me.getParentByTagName(field, "tr");
      for (i = 0; i < params.numberOfHeaderLines; i++) {
        tr = tr.previousElementSibling;
      }

      while (tr) {
        addButton = tr.querySelector("input[type='button'][name='JS_ADDLINE']");
        if (addButton) {
          return;
        }
        td = tr.querySelector("*:nth-child(" + (columnIndex + 1) + ")");
        if (show) {
          td.style.display = "";
        } else {
          if (params.clear) {
            input = td.querySelector("input[type='text']");
            if (input) {
              $update(input.name, "");
            }
          }
          td.style.display = "none";
        }
        tr = tr.nextElementSibling;
      }
    }
  },

  /**
   * Returns the column index
   * @param {HTMLElement} field Field
   * @return {Number} Column index
   */
  getColumnIndex: function (field) {
    var me = this,
        index = 0,
        td;

    if (field) {
      td = me.getParentByTagName(field, "TD");

      while (true) {
        td = td.previousElementSibling;
        if (td) {
          index++;
        } else {
          return index;
        }
      }
    }

    return -1;
  },

  /**
   * Disables dependent fields
   * @param {Object} disableDependentFieldsConfig Configuration to disable dependent fields
   * @param {Object} changedInput Changed input field
   */
  disableDependentFields: function (disableDependentFieldsConfig, changedInput) {
    var me = this,
        i, disableFieldNames, srcFieldName, key, fieldName, index, disable, lines, line;

    if (!disableDependentFieldsConfig) {
      return;
    }

    if (changedInput) {
      srcFieldName = me.getFieldNamePrefix(changedInput, {
        replaceNumberBy: "{i}"
      });
      disableFieldNames = disableDependentFieldsConfig[srcFieldName];
      if (!disableFieldNames) {
        return;
      }
      disable = !!$val(changedInput.name);
      index = me.getFieldNameIndex(changedInput.name);
      me.disableFields(disableFieldNames, index, disable);
    } else {
      for (key in disableDependentFieldsConfig) {
        fieldName = key.replace("{i}", "");
        lines = me.getLines(fieldName);
        for (i = 0; i < lines.length; i++) {
          line = lines[i];
          if (line.value) {
            disableFieldNames = disableDependentFieldsConfig[key];
            //to get the index of the line we use our field and replace the name with an empty string
            me.disableFields(disableFieldNames, line.name.replace(fieldName, ""), true);
          }
        }
      }
    }
  },

  /**
   * Disable fields
   * @param {Array} fieldNames Field names
   * @param {Number} index Index
   * @param {Boolean} disable
   * @param {Object} params
   */
  disableFields: function (fieldNames, index, disable, params) {
    var me = this,
        i, fieldName;

    for (i = 0; i < fieldNames.length; i++) {
      fieldName = fieldNames[i];
      fieldName = fieldName.replace("{i}", index);
      me.disableField(fieldName, disable);
    }
  },

  /**
   * Disables a field
   * @param {String} fieldName Field name
   * @param {Boolean} disable If true then the field will be disabled
   * @param {Object} params Parameters
   * @return {Boolean} Field is changed
   */
  disableField: function (fieldName, disable, params) {
    var field, clear;

    params = params || {};
    clear = (typeof params.clear == "undefined") ? true : false;

    if (!fieldName) {
      throw "Field name is empty";
    }
    field = $var(fieldName);
    if (!field) {
      return false;
    }
    if (disable) {
      if (clear) {
        $update(fieldName, "");
      }
      field.setAttribute("readonly", "readonly");
      field.style.backgroundImage = "none";
    } else {
      field.removeAttribute("readonly");
      field.style.backgroundImage = "";
    }
    return true;
  },

  /**
   * Disable column
   * @param {String} columnName Column name
   * @param {Boolean} disable If true then the field will be disabled
   * @param {Object} params Parameters
   * @param {Boolean} params.clear Clear value
   */
  disableColumn: function (columnName, disable, params) {
    var me = this,
        i = 0,
        fieldName, result;

    do {
      i++;
      fieldName = columnName + i;
      result = me.disableField(fieldName, disable, params);

    } while (result);
  },

  /**
   * Ensures that a specific table row exists
   * @param {String} fieldName Field name
   */
  ensureRowExists: function (fieldName) {
    var me = this,
        field, firstFieldName, firstField, addButton;

    if (!fieldName) {
      throw "Field name is empty";
    }

    firstFieldName = me.getFieldNamePrefix(fieldName) + "1";
    firstField = $var(firstFieldName);
    if (!firstField) {
      console.warn("First field doesn't exist. fieldName=" + firstFieldName);
      return;
    }
    addButton = addButton || me.getJsAddLineButton(firstFieldName);

    if (!addButton) {
      console.warn("Addbutton not found. fieldName=" + firstFieldName);
      return;
    }

    field = $var(fieldName);

    while (!field) {
      $addLine(addButton, 1);
      field = $var(fieldName);
    }
  },

  /**
   * Finds a JS_ADDLINE button by a given field name
   * @param {String} fieldName Field name
   * @return {HTMLElement}
   */
  getAddLineButton: function (fieldName) {
    var me = this,
        tr, field, button;

    if (!fieldName) {
      throw "Field name is empty";
    }
    field = $var(fieldName);
    if (!field) {
      throw "Can't find field '" + fieldName + "'";
    }

    tr = me.getParentByTagName(field, "tr");
    while (true) {
      tr = tr.nextElementSibling;
      if (!tr) {
        return;
      }
      button = tr.querySelector("input[type='button'][name='JS_ADDLINE']");
      if (button) {
        return button;
      }
    }
  },

  /**
   * Clears remaining lines
   * @param {String} fieldName Field name
   */
  clearRemainingLines: function (fieldName) {
    var me = this,
        field, tr, inputs, i, button, input;

    if (!fieldName) {
      throw "Field name is empty";
    }
    field = $var(fieldName);
    if (!field) {
      return;
    }
    tr = me.getParentByTagName(field, "tr");
    while (true) {
      if (!tr) {
        return;
      }
      button = tr.querySelector("input[type='button'][name='JS_ADDLINE']");
      if (button) {
        return;
      }
      inputs = tr.querySelectorAll("input[type='text']");
      for (i = 0; i < inputs.length; i++) {
        input = inputs[i];
        $update(input.name, "");
      }
      tr = tr.nextElementSibling;
    }
  },

  /**
   * Returns true if the column is empty
   * @param {String} columnName Column name
   * @param {Object} params Parameters
   * @param {Array} params.entriesEqualToEmpty Entries that also count as empty, e.g. `?`
   * @return {Boolean}
   */
  isColumnEmpty: function (columnName, params) {
    var i = 1,
        field, val;

    params = params || {};
    params.entriesEqualToEmpty = params.entriesEqualToEmpty || [];

    do {
      field = $var(columnName + i);
      val = $val(columnName + i);
      if (field && !!val && (params.entriesEqualToEmpty.indexOf(val) < 0)) {
        return false;
      }
      i++;
    } while (field);

    return true;
  },

  /**
   * Returns true if the row is empty
   * @param {String} columnName Column name
   * @return {Boolean}
   */
  isRowEmpty: function (columnName) {
    var me = this,
        field, tr, inputs, i, input;

    field = $var(columnName);

    if (!field) {
      return false;
    }

    tr = me.getParentByTagName(field, "tr");

    if (!tr) {
      return false;
    }

    inputs = tr.querySelectorAll("input");

    for (i = 0; i < inputs.length; i++) {
      input = inputs[i];
      if (input.value) {
        return false;
      }
    }

    return true;
  },

  /**
   * Shows or hides a row
   * @param {String} columnName Column name
   * @param {Function} callback Callback
   */
  showRows: function (columnName, callback) {
    var me = this,
        i = 1,
        field, result, tr;

    if (!columnName) {
      throw "Column name is empty";
    }

    do {
      field = $var(columnName + i);
      if (field) {
        result = (callback) ? callback(i) : true;
        tr = me.getParentByTagName(field, "tr");
        if (tr) {
          tr.setAttribute("style", result ? "" : "display:none;");
        }
      }
      i++;
    } while (field);
  },

  /**
   * Registers a click handler
   * @param {Object} params Parameters
   * @param {String} params.name Button name
   * @param {String} params.prefix Button name prefix
   * @param {Object} params.ctx Context
   * @param {Function} params.func Function
   */
  addClickListener: function (params) {
    var elements, i, element, handlerFunc;

    params = params || {};
    if (!params.name && !params.prefix) {
      throw "Name or prefix must be set";
    }

    if (!params.ctx) {
      throw "Context must be set";
    }

    if (!params.func) {
      throw "Function must be set";
    }

    handlerFunc = function () {
      params.func.apply(params.ctx, arguments);
    };

    if (params.prefix) {
      elements = document.querySelectorAll("input[name^='" + params.prefix + "']");
    } else {
      elements = document.querySelectorAll("input[name='" + params.name + "']");
    }

    for (i = 0; i < elements.length; i++) {
      element = elements[i];
      element.addEventListener("click", handlerFunc, false);
    }
  },

  /**
   * Returns the number of lines
   * @param {String} indicatorColumnName Indicator column name
   * @return {Number} Number of lines
   */
  countLines: function (indicatorColumnName) {
    var me = this;
    return me.getLines(indicatorColumnName).length;
  },

  /**
   * Returns the lines
   * @param {String} indicatorColumnName Indicator column name
   * @return {Object} lines
   */
  getLines: function (indicatorColumnName) {
    var nodeList, fields;

    nodeList = document.querySelectorAll('[name^="' + indicatorColumnName + '"]');
    fields = Array.prototype.slice.call(nodeList).filter(function (elem) {
      //Columns could start with the same prefix, e.g. IX_MAP_INVI_QUANTITY and IX_MAP_INVI_QUANTITY_UNIT.
      //The querySelectorAll returns both columns so we have to filter the result.
      return !isNaN(elem.name.replace(indicatorColumnName, ""));
    });

    return fields;
  },

  /**
   * Calls the registered function of the given name.
   * As parameter a JSON object can be given.
   *
   * @param {String} fctName The name of the registered function to call.
   * @param {Object} paramObj The input data for the function, sent as Serialized String value of the Any object.
   *
   * @param {Function} success Success callback function. The argument value
   *  is the deserialized JSON object from the String value of the Any
   *  object that was returned from the registered function.
   * @param {Function} failure Failure callback function the error object will be given as parameter.
   */
  callRegisteredFunction: function (fctName, paramObj, success, failure) {
    var me = this,
        standardAppName, url, rfParams;

    if (!fctName) {
      throw "Function name is missing";
    }

    standardAppName = "sol.common.apps.BusinessSolution";
    url = me.getAppApiUrl(standardAppName) + "exec_registered_fct/" + encodeURIComponent(fctName);

    rfParams = {
      any: JSON.stringify(paramObj)
    };

    me.sendPost(url, rfParams, function (resp) {
      var respObj;
      try {
        respObj = JSON.parse(resp);
      } catch (e) {
        console.info("Could not parse JSON in responseText: " + resp);
        failure(e);
      }
      success(respObj);
    },
    failure, {
      addTicket: true,
      addCookieTicket: true
    });
  },

  /**
   * Sends a post request
   * @param {String} url URL
   * @param {Object} dataObj Data object
   * @param {Function} success Success callback function.
   * @param {Function} failure Failure callback function the error object will be given as parameter.
   * @param {Object} params Parameters
   * @param {Object} params.reqParamsObj Request params object
   * @param {Boolean} params.addTicket Add ticket
   */
  sendPost: function (url, dataObj, success, failure, params) {
    var me = this,
        xhr, data, reqParamsString;

    dataObj = dataObj || {};
    params = params || {};
    params.reqParamsObj = params.reqParamsObj || {};
    params.contentType = params.contentType || "application/x-www-form-urlencoded";

    if (params.addTicket) {
      params.reqParamsObj.ticket = ELO_PARAMS.ELO_TICKET;
    }

    reqParamsString = me.encodeParams(params.reqParamsObj);
    if (reqParamsString) {
      url += "?" + reqParamsString;
    }

    xhr = new XMLHttpRequest();
    xhr.open("POST", url, true);

    if (params.contentType) {
      xhr.setRequestHeader("Content-type", params.contentType);
    }

    data = me.encodeParams(dataObj);

    xhr.onload = function () {
      var resp;

      resp = xhr.responseText;

      if (xhr.status == 200) {
        success(resp);
      } else {
        failure(resp);
      }
    };

    xhr.send(data);
  },

  /**
   * Encodes parameters
   * @param {Object} params Parameters
   * @param {String} separator Separator
   * @return {String}
   */
  encodeParams: function (params, separator) {
    var parts = [],
        prop, paramsString;

    params = params || {};

    separator = separator || "&";

    for (prop in params) {
      if (params.hasOwnProperty(prop)) {
        parts.push(encodeURIComponent(prop) + "=" + encodeURIComponent(params[prop]));
      }
    }

    if (parts.length == 0) {
      return "";
    }

    paramsString = parts.join(separator);

    return paramsString;
  },

  /**
   * Returns the ELOwf URL
   * @return {String} ELOwf URL
   */
  getWfUrl: function () {
    var currentUrl, wfUrl;

    currentUrl = window.location.href;
    wfUrl = currentUrl.substr(0, currentUrl.lastIndexOf("/wf/"));

    return wfUrl;
  },

  /**
   * Returns the URL for a specific app
   * @param {String} appName App name
   * @return {String} ELOapp URL
   */
  getAppUrl: function (appName) {
    var me = this,
        wfUrl, appUrl;

    wfUrl = me.getWfUrl();
    appUrl = wfUrl + "/apps/app/" + appName + "/";

    return appUrl;
  },

  /**
   * Returns the URL for an app API
   * @param {String} appName App name
   * @return {String} ELOapp API URL
   */
  getAppApiUrl: function (appName) {
    var me = this,
        appUrl, appApiUrl;

    if (!appName) {
      throw "AppName is missing";
    }

    appUrl = me.getAppUrl(appName);

    appApiUrl = appUrl + "api/";

    return appApiUrl;
  },

  /**
   * Insert list
   * @param {String}labelName Label name
   * @param {Array} entries Entries
   * @param {String} entries[].text Text
   * @param {Object} entries[].params Parameters
   */
  showTemplateListWarning: function (labelName, entries) {
    var me = this,
        label, listElements, listElement, list, i, entry, listEntry, listEntryLink, entryTextElement,
        span, spanTextNode;

    label = $var(labelName);

    if (!label) {
      return;
    }

    listElements = label.getElementsByTagName("ul");

    for (i = 0; i < listElements.length; i++) {
      listElement = listElements[i];
      label.removeChild(listElement);
    }

    if (!entries || (entries.length == 0)) {
      me.hideTemplate(label);
      return;
    }

    list = document.createElement("ul");
    for (i = 0; i < entries.length; i++) {
      entry = entries[i];
      listEntry = document.createElement("li");
      if (entry.style) {
        listEntry.setAttribute("style", entry.style);
      }
      listEntryLink = document.createElement("a");


      if (entry.params) {
        if (entry.params.guid) {
          listEntryLink.setAttribute("sordGuid", entry.params.guid);
          listEntryLink.onclick = function () { // eslint-disable-line no-loop-func
            var sordGuid;
            sordGuid = this.getAttribute("sordGuid");
            me.gotoSord(sordGuid);
          };
        }
      }
      entryTextElement = document.createTextNode(entry.text);
      listEntryLink.appendChild(entryTextElement);
      listEntry.appendChild(listEntryLink);
      if (entry.span) {
        span = document.createElement("span");
        if (entry.span.style) {
          span.setAttribute("style", entry.span.style);
        }
        spanTextNode = document.createTextNode(entry.span.text);
        span.appendChild(spanTextNode);
        listEntry.appendChild(span);
      }
      list.appendChild(listEntry);
    }

    label.appendChild(list);

    me.showTemplate(label);
  },

  /**
   * Goto a sord
   * @param {String} guid GUID
   */
  gotoSord: function (guid) {
    if (!guid) {
      return;
    }
    var data = {};
    console.info("sendGoto: " + guid);
    data[api.constants.communication.DATA_GUID] = guid;
    api.communication.Parent.sendGoto(data);
  },

  /**
   * Hides a template
   * @param {HTMLElement} field Field
   */
  hideTemplate: function (field) {
    var me = this,
        template;

    if (!field) {
      return;
    }

    template = me.getTemplate(field);

    if (!template) {
      return;
    }

    template.style.display = "none";
  },

  /**
   * Shows/hides a template
   * @param {HTMLElement} field Field
   * @param {Boolean} [enabled=true]
   */
  showTemplate: function (field, enabled) {
    var me = this,
        template;

    enabled = (typeof enabled == "undefined") ? true : enabled;

    if (!field) {
      return;
    }

    template = me.getTemplate(field);

    if (!template) {
      return;
    }

    if (enabled) {
      template.style.display = "";
    } else {
      template.style.display = "none";
    }
  },

  /**
   * Checks the version of ELO components
   * @param {String} currentVersionString
   * @param {String} requiredVersionString
   * @param {Boolean} checkSubVersion (optional) Checks also the sub version.
   * e.g. there is an fix in all versions in wf *.09. So if you want to check the major and sub version you have to set this boolean to true.
   * Otherwise the function will return true for this example because 20.03 is greater than 12.09.
   * @return {Boolean} Return true if the current version is equal or higher then the required version
   */
  checkVersion: function (currentVersionString, requiredVersionString, checkSubVersion) {
    var result = true,
        currentRegex, requiredRegex, currentVersionMatch, requiredVersionMatch, currentPart, requiredPart, partIndex = 0;

    currentRegex = /([0-9]+(\\.[0-9]+)*)/g;
    requiredRegex = /([0-9]+(\\.[0-9]+)*)/g;
    currentVersionMatch = currentRegex.exec(currentVersionString);
    requiredVersionMatch = requiredRegex.exec(requiredVersionString);

    while (requiredVersionMatch !== null) {
      currentPart = (currentVersionMatch) ? parseInt(currentVersionMatch[0], 10) : 0;
      requiredPart = parseInt(requiredVersionMatch[0], 10);
      if (requiredPart > currentPart) {
        result = false;
        break;
      } else if (requiredPart < currentPart) {
        if (partIndex == 0 && checkSubVersion) {
          result = false;
          break;
        }
        result = true;
        break;
      }
      currentVersionMatch = currentRegex.exec(currentVersionString);
      requiredVersionMatch = requiredRegex.exec(requiredVersionString);
      partIndex++;
    }

    return result;
  },

  /**
   * This creates a hidden field to write values to the sord.
   * If a field already exists the existing field will be updated.
   * @param {Object} cfg
   * @param {String} [cfg.type=GRP] GRP|MAP|WFMAP
   * @param {String} cfg.key
   * @param {String} value
   */
  setHiddenValue: function (cfg, value) {
    var field, input, form;

    switch (cfg.type) {
      case "MAP":
        field = "IX_MAP_" + cfg.key;
        break;
      case "WFMAP":
        field = "WF_MAP_" + cfg.key;
        break;
      default:
        field = "IX_GRP_" + cfg.key;
        break;
    }

    input = $var(field);

    if (!input) {
      form = document.getElementById("elo_wf_form");
      input = document.createElement("input");
      input.type = "hidden";
      input.name = field;
      form.append(input);
    }

    $update(field, value);
  },

  /**
   * Removes the field name prefix
   * @param {String} fieldName Field name
   * @return {String} Field name without prefix
   */
  removeFieldNamePrefix: function (fieldName) {
    if (!fieldName) {
      return "";
    }
    fieldName = fieldName.replace(/^(IX|WF)_(GRP|MAP|BLOB)_/, "");

    return fieldName;
  },

  /**
   * Returns a template Sord
   * @return {Object} Template sord
   */
  getTemplateSord: function () {
    var me = this,
        tplSord, value, name, shortName, inputs, i, input, dataType;

    tplSord = {
      objKeys: {},
      mapKeys: {},
      wfMapKeys: {}
    };

    inputs = document.querySelectorAll("input");

    for (i = 0; i < inputs.length; i++) {
      input = inputs[i];
      name = input.name;
      dataType = me.getInputDataType(input);
      if (dataType == "isodate") {
        value = me.getIsoDate(name);
      } else {
        value = $val(name);
      }

      if (name.indexOf("IX_GRP_") == 0) {
        shortName = name.substr(7);
        tplSord.objKeys[shortName] = value;
      } else if (name.indexOf("IX_MAP_") == 0) {
        shortName = name.substr(7);
        tplSord.mapKeys[shortName] = value;
      } else if (name.indexOf("WF_MAP_") == 0) {
        shortName = name.substr(7);
        tplSord.wfMapKeys[shortName] = value;
      }
    }

    return tplSord;
  },

  /**
   * Updates values
   * @param {Object} updates Updates
   *     {
   *         objKeys: {
   *         },
   *         mapKeys: {
   *           "MYKEY": "myvalue"
   *         }
   *     }
   */
  updateValues: function (updates) {
    var me = this;

    if (!updates) {
      return;
    }

    me.writeUpdates(updates.objKeys, "IX_GRP_");
    me.writeUpdates(updates.mapKeys, "IX_MAP_");
  },

  /**
   * @private
   * @param {Object} entries Entries
   *     {
   *       "MYKEY": "myvalue"
   *     }
   * @param {String} prefix Prefix
   */
  writeUpdates: function (entries, prefix) {
    var me = this,
        value, dataType, key, fieldName, field;

    for (key in entries) {
      fieldName = prefix + key;
      value = entries[key];

      field = $var(fieldName);

      dataType = me.getInputDataType(field);
      if (dataType == "isodate") {
        me.setIsoDate(fieldName, value, {
          callInputChanged: false
        });
      } else {
        $update(fieldName, value);
      }
    }
  },

  /**
   * Returns the input type
   * @param {HTMLElement} input Input
   * @return {String} Data type
   */
  getInputDataType: function (input) {
    var inputDataType, eloVerify, eloVerifyElements;

    inputDataType = "text";

    if (!input) {
      throw "Input field is missing";
    }

    eloVerify = input.getAttribute("eloverify");
    if (eloVerify) {
      eloVerifyElements = eloVerify.split(" ");
    }

    if (eloVerifyElements && (eloVerifyElements.indexOf("date") > -1)) {
      inputDataType = "isodate";
    }

    return inputDataType;
  },

  /**
   * Sets a `changed` Flag
   * @param {HTMLElement} field Field
   */
  setFieldChanged: function (field) {
    if (!field) {
      return;
    }
    field.setAttribute("changed", "");
  },

  /**
   * Creates an easily editable field from a multiindex field value.
   * e.g. mi = MultiIndex("abc¶def¶xyz");
   *      mi.values() => ["abc", "def", "xyz"]
   *      mi.add("test") => ["abc", "def", "xyz", "test"]
   *      mi.remove("def") => ["abc", "xyz", "test"]
   *      mi.contains("abc") => true
   *      String(mi) => "abc¶xyz¶test"
   *      mi.save("IX_GRP_MYFIELD") => $updates MYFIELD with current String representation
   *
   * e.g. mi = MultiIndex("abc¶def¶xyz¶xyz")
   *      mi.removeAll("xyz") => ["abc", "def"]
   *
   * Hint: remove() only removes the first matching value
   *
   * @param {String|FormWrapper Field} f
   * @param {Object} options different options
   * @param {Boolean} options.unique Determine whether multiIndex field has the possibility to store same values (set to true by default).
   * @returns MultiIndex field
   */
  MultiIndex: function (f, options) {
    var a, iA;
    f || (f = "");

    options = options || {};
    options.unique = options.hasOwnProperty("unique") ? options.unique : true;

    try {
      a = (typeof f === "string" ? f : ((f || {}).value() || "")).split("¶");
    } catch (_e) {
      console.log("MultiIndex requires a string or FormWrapper field as constructor parameter.");
    }
    ((a.length === 1) && (a[0] === "") && (a = []));
    iA = {
      values: a.slice.bind(a),
      toString: function () {
        return a.reduce(function (s, val, i) {
          s += val;
          (i + 1 < a.length) && (s += "¶");
          return s;
        }, "");
      },
      valueOf: NaN,
      add: function (val) {
        if (!options.unique || !this.contains(val)) {
          a.push(String(val));
        }
        return a;
      },
      remove: function (val) {
        var i = a.indexOf(val),
            valExists = ~i;
        return (valExists && a.splice(i, 1)), a;
      },
      removeAll: function (val) {
        do {
          var i = a.indexOf(val),
              valExists = ~i;
          valExists && a.splice(i, 1);
        } while (valExists);
        return a;
      },
      contains: function (val) {
        return !!~a.indexOf(val);
      },
      save: function (fieldName) {
        var val = String(this);
        fieldName
          ?
          (f.form ? f.form.fields[fieldName].set(val) : $update(fieldName, val)) :
          ((f && f.set) ? f.set(val) : console.log("could not save. no field specified"));
      }
    };
    iA.toJSON = iA.toString;
    return iA;
  },

  /**
   * Returns the time zone from the URL
   * @return {String} Time zone
   */
  getTimeZoneFromUrl: function () {
    var me = this,
        urlParams, timeZone;

    urlParams = me.getUrlParams();

    timeZone = urlParams["timezone"];

    return timeZone;
  },

  /**
   * Returns the URL params
   * @return {Object} URL parameters
   */
  getUrlParams: function () {
    var params = {};

    window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, function (m, key, value) {
      params[decodeURIComponent(key)] = decodeURIComponent(value);
    });

    return params;
  },

  /**
   * Checks whether a field is missing
   * @param {Array} fieldNames Field names
   */
  fieldIsMissing: function (fieldNames) {
    var i;

    if (!fieldNames) {
      return true;
    }

    for (i = 0; i < fieldNames.length; i++) {
      if (!$var(fieldNames[i])) {
        return true;
      }
    }

    return false;
  },

  /**
   * Moves a row after another row
   *
   * @param {String} rowToMoveFieldName Row to move field name
   * @param {String} dstRowFieldName Destination row field name
   * @param {Object} params Parameters
   * @param {String} params.identifyTemplateByFieldName Identify the template by a specific field name
   */
  moveRowAfter: function (rowToMoveFieldName, dstRowFieldName, params) {
    var me = this,
        rowToMoveField, dstRowField, rowToMove, dstRow, tbody, templateField, template;

    params = params || {};

    if (!rowToMoveFieldName) {
      throw "Row to move field name is empty";
    }

    if (!dstRowFieldName) {
      throw "Destination row field name is empty";
    }

    if (params.identifyTemplateByFieldName) {
      templateField = $var(params.identifyTemplateByFieldName);
      template = me.getTemplate(templateField);
      rowToMoveField = me.getFieldInTemplate(template, rowToMoveFieldName);
    } else {
      rowToMoveField = $var(rowToMoveFieldName);
    }

    if (!rowToMoveField) {
      throw "Can't find row to move field '" + rowToMoveFieldName + "'";
    }

    dstRowField = $var(dstRowFieldName);

    if (!dstRowField) {
      throw "Can't find destination row field '" + dstRowFieldName + "'";
    }

    rowToMove = me.getRow(rowToMoveField);

    dstRow = me.getRow(dstRowField);

    tbody = me.getParentByTagName(dstRow, "tbody");

    tbody.insertBefore(rowToMove, dstRow.nextSibling);
  },

  /**
   * Returns a field within a specific template
   * @param {HTMLElement} template Template
   * @param {HTMLElement} fieldName Field name
   */
  getFieldInTemplate: function (template, fieldName) {
    var field;

    if (!template) {
      throw "Template is empty";
    }

    if (!fieldName) {
      throw "Field name is empty";
    }

    field = template.querySelector("input[name='" + fieldName + "']");

    return field;
  },

  /**
   * Sets a label
   * @param {String} lblName Label name
   * @param {String} translationKey Translation key
   */
  setLabel: function (lblName, translationKey) {
    var me = this,
        label, text;

    if (!lblName) {
      throw "Field name is emtpy";
    }

    label = $var(lblName);

    if (!label) {
      throw "Label '" + lblName + "' not found";
    }

    text = me.getTranslation(translationKey);

    $update(lblName, text);
  },

  /**
   * Replaces translation keys on a form
   * Usually you'd add a string like "{{package}}" within the translation keys of your form
   * to avoid the automatic translation of those keys. With this function you can now replace
   * {{package}} with the real package name, get the translated term and replace the text on the form
   *
   * This works only well when the first part of your new translateKey (sol.meeting_enterprise) is the same
   * as the prefix of the current form (sol.meeting). Otherwise the new key wouldn't be loaded
   * @param {String} search Search for this string (e.g. {{package}})
   * @param {String} replace Replace with this string (e.g. meeting_enterprise)
   * @param {String} fallback (optional) Fallback string in case a translateTerm is not found for the replaced translateKey
   */
  replaceTranslations: function (search, replace, fallback) {
    var me = this, texts, text, i, placeholders, placeholder, buttons, tooltips, tooltip;

    // this is looking for all text elements in the form
    // elowf keeps them always between <acronym>-tags
    texts = document.querySelectorAll("acronym");

    for (i = 0; i < texts.length; i++) {
      text = texts[i].innerText;
      if (text.indexOf(search) > -1) {
        try {
          texts[i].innerText = me.getTranslation(text.replace(search, replace));
        } catch (e) {
          if (fallback) {
            texts[i].innerText = me.getTranslation(text.replace(search, fallback));
          }
        }
      }
    }

    placeholders = document.querySelectorAll("[placeholder*='" + search + "']");

    for (i = 0; i < placeholders.length; i++) {
      placeholder = placeholders[i].attributes["placeholder"];
      try {
        placeholder.textContent = me.getTranslation(placeholder.textContent.replace(search, replace));
      } catch (e) {
        if (fallback) {
          placeholder.textContent = me.getTranslation(placeholder.textContent.replace(search, fallback));
        }
      }
    }

    buttons = document.querySelectorAll("input[type=button][value*='" + search + "'");
    for (i = 0; i < buttons.length; i++) {
      try {
        buttons[i].value = me.getTranslation(buttons[i].value.replace(search, replace));
      } catch (e) {
        if (fallback) {
          buttons[i].value = me.getTranslation(buttons[i].value.replace(search, fallback));
        }
      }
    }

    tooltips = document.querySelectorAll("input[savedtitle*='" + search + "']");

    for (i = 0; i < tooltips.length; i++) {
      tooltip = tooltips[i].attributes["savedtitle"];
      try {
        tooltip.value = me.getTranslation(tooltip.value.replace(search, replace));
      } catch (e) {
        if (fallback) {
          tooltip.value = me.getTranslation(tooltip.value.replace(search, fallback));
        }
      }
    }

    // since some parts like header texts could have been replaced, a layout reset is required
    sol.common.forms.Utils.resetFormLayout();
  },

  /**
   * Returns a translation
   * @param {String} translationKey Translation key
   * @return Translation
   */
  getTranslation: function (translationKey) {
    var translation = "";

    if (!translationKey) {
      throw "Translation key is empty";
    }

    if (elo && elo.locale && elo.locale.store) {
      translation = elo.locale.store[translationKey];
    }

    if (!translation) {
      console.info("Translation key '" + translationKey + "' not found");
      return translationKey;
    }

    return translation;
  },

  /**
   * Adds a validation attibute
   * @param {String} fieldName
   * @param {String} [validationAttribute=notemptyforward]
   */
  addValidation: function (fieldName, validationAttribute) {
    var field, eloverify;

    if (!fieldName) {
      throw "Field name is missing";
    }

    validationAttribute = validationAttribute || "notemptyforward";

    field = $var(fieldName);

    if (!field) {
      return;
    }

    eloverify = field.getAttribute("eloverify");

    if (eloverify.indexOf(validationAttribute) == -1) {
      eloverify += " " + validationAttribute;
      field.setAttribute("eloverify", eloverify);
    }
  },

  /**
   * Removes a validation attibute
   * @param {String} fieldName
   * @param {String} [validationAttribute=notemptyforward]
   */
  removeValidation: function (fieldName, validationAttribute) {
    var field, eloverify;

    if (!fieldName) {
      throw "Field name is missing";
    }

    validationAttribute = validationAttribute || "notemptyforward";

    field = $var(fieldName);

    if (!field) {
      return;
    }

    eloverify = field.getAttribute("eloverify");
    eloverify = eloverify.replace(validationAttribute, "");
    field.setAttribute("eloverify", eloverify);
  },

  /**
   * Checks wether a field is numeric
   * @param {HTMLElement} field
   */
  isNumeric: function (field) {
    var types, i, type, eloverify;

    types = ["num", "amount"];

    if (!field) {
      throw "Field is missing";
    }

    eloverify = field.getAttribute("eloverify");

    if (!eloverify) {
      return false;
    }

    for (i = 0; i < types.length; i++) {
      type = types[i];
      if (eloverify.indexOf(type) > -1) {
        return true;
      }
    }

    return false;
  },

  /**
   * Removes a class from a specified field
   * @param {String} fieldName Field name
   * @param {String} className Class name
   */
  removeFieldClass: function (fieldName, className) {
    var me = this,
        field, td;

    if (!fieldName) {
      throw "Field name is empty";
    }

    field = $var(fieldName);

    if (!field || !className) {
      return;
    }

    td = me.getParentByTagName(field, "td");

    td.classList.remove(className);
  }
});