    /**
     * @abstract
     * Provides extensive utilities for working with ELO-forms.
     *
      *
       * Using FormWrapper's simple state-manager, you can define
       * application states consisting of fields and their respective:
       *
       *  - values
       *  - labels
       *  - tooltips
       *  - placeholder
       *  - attributes (readonly / hidden / optional)
       *  - responders:
       *    a responder is a function which will get called when the field's value is changed
       *  - validators:
       *    a validator is a function which will get called when a field is clicked. Afterwards
       *    the validation massage is shown in the client.
       *
       *  There is also support for behaviour templates which provide common behaviours to a group of fields.
       *  You can use templates defined in FormWrapper.Templates (end of this file)
       *
       * # Setup
       *
       *  The Wrapper must be initialized in a form's `Header.txt`.
       *  e.g.:
       *
       *     <script type="text/javascript" src="lib_sol.common.forms.FormWrapper"></script>
       *     <script type="text/javascript" src="lib_sol.hr.forms.PersonnelFile"></script>
       *
       *  `lib_sol.hr.forms.PersonnelFile` will be a separate class extending
       *  the form-wrapper by the customer specific logic. Therefore, also add these entries to your `Header.txt`:
       *
       *     // ELO < 10.1 does not support onInit here. Just add these lines to inputChanged source == null instead.
       *     function onInit() {
       *       this.form = sol.create("sol.hr.forms.PersonnelFile");
       *       this.form.OnInitAndTabChange();
       *     }
       *
       *     function tabChanged(id) {
       *       this.form.TabChanged(id);
       *     }
       *
       *     function inputChanged(source) {
       *       source == null ?
       *         window.setTimeout(function() { sol.common.forms.Utils.initializeForm(); }, 100)
       *         : this.form.InputChanged(source);
       *     }
       *
       *     function nextClicked(id) {
       *       return (
       *         sol.common.forms.Utils.disableCancelButtonValidation(id, ["sol.common.wf.node.cancel"])
       *         || this.form.OnSave()
       *       );
       *     }
       *
       *     function saveClicked() {
       *       return this.form.OnSave();
       *     }
       *
       *     function addLineClicked(addLineId, groupIndex) {
       *       this.form.LineAdded(addLineId, groupIndex);
       *     }
       *
       *     function removeLineClicked(addLineId, groupIndex) {
       *       return this.form.LineRemoved(addLineId, groupIndex);
       *     }
       *
       *     function onDynListItemSelected(item, inputName) {
       *       this.form.InputChanged(item, inputName);
       *     }
       *
       *  By defining all customer specific logic in the extending class, your `Header.txt` will be much cleaner.
       *  It is recommended not to call any additional customer specific util-functions in `Header.txt`. Instead, you
       *  should `include` your customer-specific logic solely in the extending class.
       *
       *  You can define and use multiple extending classes in one Header.txt.
       *
       *  Minimal example for an extending class `PersonnelFile`:
       *
       *     sol.define("sol.hr.forms.PersonnelFile", {
       *       extend: "sol.common.forms.FormWrapper",
       *       prefix: "HR_PERSONNEL"  // usually all customer-specific fields have some kind of prefix. You can define it here.
       *     }
       *
       * # Usage
       *  The wrapper functions are now available in the form. These are some common use-cases:
       *
       * ### Field functions (this.form.fields.FIELDNAME)
       * Every field on the form (MAP,GRP) is represented as an object.
       *    .value(opts)-   returns the field's value.
       *                    (isodate for date-fields and the key for localized Kwls).
       *                    pass `{full: true}` to receive the localized value of an lKwl instead of key only
       *                    pass `{asNumber: true}` to receive the value as a number. (e.g. valuable for localized number strings)
       *
       *    .set(val)   -   replaces the field's value by `val`
       *                    (for val, pass an isodate date-fields and for localizedKwls pass the key)
       *
       *    .element()  -   returns the field's HTML element
       *
       *    .show()     -   displays the field in the form
       *                    (will also show the field's label and selector if called with .(true, true))
       *
       *    .hide()     -   hides the field
       *                    (will also hide the field's label and selector if called with .(true, true))
       *
       *    .setAttribute("readonly", true)
       *                -   renders a field readonly (pass false to make it writeable again)
       *
       *    .setAttribute("optional", true)
       *                -   makes a field optional. Pass false to make a field required
       *
       *    .tooltip()
       *                -   if a tooltip is defined, returns the tooltip as a String
       *
       *    .writeTooltip(tooltip)
       *                -   Adds a tooltip to the field
       *                -   removes tooltip if falsy value is passed
       *
       *    .setPlaceholder(value)
       *                -   set a placeholder text on the related field input element
       *
       *    .setImage(guid)   -   only works on image-fields! Downloads an image specified by guid and sets the image-fields src-attribute
       *                         if no guid is passed, but the sord contains a guid as the fields value, it will be used as a fallback
       *
       *  show and hide of labels will only work, if you assign the label's "variable" field a value of "LBL_" + me.prefix + `Fieldname without (WF/IX)_(MAP/GRP) and without prefix`
       *  and the label's respective input-field's variable field contains the prefix defined in the extending class definition (e.g. HR_PERSONNEL)
       *
       * ### Form functions (this.form)
       *
       *    .setState(statename)
       *                -   transforms the form according to the state-definition
       *
       *    .activeState
       *                -   stores the currently active state
       *
       *    .getActiveTabId()
       *                -   returns the id of the currently active tab (e.g. "_501_time_phases")
       *
       *    .today()    -   returns the current client date as an isodate
       *
       *    .logFieldValues(b) -   logs all fieldnames and their values to the console (as a table if implemented). pass true as parameter to return fields and values as an array instead of a console-log
       *
       *
       * ### Tabs functions (this.form.tabs)
       *    ._tabName  -   returns the tab object of the tab "tabName"
       *
       *    .activeTab  -   returns the tab object of the active tab
       *
       *    .all  - holds an object containing all templates which are available on the form
       *
       * ### Tab functions (this.form.tabs._tabName)
       *    .parts      -   holds an object containing the tab's templates (called "parts" in this framework)
       *
       *    .parts._partName - holds the part-object having the name "partName"
       *
       *    .containsCover  - boolean. does the tab contain a business solution coversheet? (__cover)
       *
       *    .hidePartsContainingOnlyEmptyFields() - hides all parts of a tab which where none of each part's fields has a value.
       *
       *    .hideUnnecessaryParts(array) - hides all parts which do not contain any of the fields listed in the passed array
       *
       * ### Template/Part functions (this.form.tabs_tabName.parts._partName)
       *    .show()     -   shows the template/part
       *
       *    .hide()     -   hides the template/part
       *
       *    .isCover    -   is the part a business solution coversheet? (__cover)
       *
       * ### Table functions (this.form.tables[tableId])
       *    .columns    -   holds an array containing the names of all columns of the respective table
       *
       *    .size       -   number of rows in the table
       *
       *    .getRow(x)   -   receives the specified table row with index x. returns a row even if it does not exist.
       *                     you can check for a non-existant row by using .getRow(x).row == undefined
       *
       *    .getRowByField(field) - receives table row by field. Use fieldindex to return the corresponding table row.
       *                     returns undefined if field isn't a column in given table.  Method is only supported on table object.
       *                     example: form.tables.tableId.getRowByField(field)
       *
       *    .insert(obj) -  inserts an object or an array of objects containing column: value pairs. ({my_column: "test"}).
       *                    optional second parameter: array of fieldnames. if specified fields are all empty, the latest existing row is used. otherwise, data is inserted in a new row.
       *                    returns an array of all added rows
       *    .forEach(cb)  -   like forEach of arrays. the callback receives an object containing the columns of a row.
       *
       *    .map(cb)    -   like map of arrays.
       *
       *    .filter(cb) -   like filter of arrays.
       *
       *    .slice()    -   like slice of arrays. useful to convert a table to an array on which other array function can be used. (e.g. .some, .any, .reduce ...)
       *
       * # States:
       *  The class extending sol.common.forms.FormWrapper can define multiple states, which can be
       *  set e.g. on input-changed events (responders). Usually, states are changed in "responder"-functions or
       *  the "BeforeOnInitAndTabChange" function, which will be called once during initialization
       *  of the wrapper and again on every tab-change. "AfterOnInitAndTabChange" is called
       *  after all states' "OnInitAndTabChange"-functions were called. (see e.g. templates, which
       *  define their own OnInitAndTabChange function).
       *  There is also "BeforeInputChanged" and "AfterInputChanged", which will be called
       *  before/after the changed field's responder has been executed.
       *  LineRemoved calls OnRemoveLine, which you can define in your extending class. Also,
       *  AfterOnRemoveLine is called when the form has finished removing the table row. You
       *  will usually want to use AfterOnRemoveLine, since it also gets passed the removed row values.
       *  You should not use the obsolete OnLineChange function, since you would have to find out
       *  manually if a line has been added or removed each time (which could be impossible to determine).
       *
       *
       * ### Order of computation
       *     (# means, you can define this function in the extending class)
       *     initialize: called during sol.create
       *       #OnInit
       *     OnInitAndTabChange: called when initializing form and on a tab change
       *       #BeforeOnInitAndTabChange
       *       Calls all OnInitAndTabChange functions which are defined in templates or states. (#template.OnInitAndTabChange and #state.OnInitAndTabChange)
       *       #AfterOnInitAndTabChange
       *     InputChanged: called when an input field´s value changes
       *       #BeforeInputChanged
       *       Calls the changed field´s #responder function
       *       #AfterInputChanged
       *     LineRemoved: called, when a JS_REMOVELINE button is clicked.
       *       #OnRemoveLine
       *       #AfterOnRemoveLine
       *     LineAdded: called, when a JS_ADDLINE button is clicked.
       *       #OnAddLine
       *     OnSave: called when "save" is clicked
       *       #BeforeOnSave
       *       Calls all rules defined in #onSaveRules
       *       #AfterOnSave
       *
       *
       *
       * ### Exemplary Formwrapper extending class:
       *     sol.define("sol.hr.forms.PersonnelFile", {
       *       extend: "sol.common.forms.FormWrapper",
       *       prefix: "HR_PERSONNEL",   // IX_MAP_HR_PERSONNEL_DATEOFJOINING -> Prefix = HR_PERSONNEL
       *       defaultState: "myDefaultState",
       *       //states begin
       *       states: {
       *         stateInit: {  //stateInit is called automatically when the form is initially loaded
       *           //declare your desired initial state here (usually responders and validators)
       *           fieldProperties: {
       *             IX_MAP_DURATION_TYPE: {
       *               responder: function(form, state, field, value) {
       *                 if (value == "") { form.setState("myDefaultState")
       *                 } else { form.setState("durationTypeSelected") };
       *               }
       *             }
       *           }
       *         },
       *         myDefaultState: {
       *           fieldProperties: {
       *             IX_MAP_HR_PERSONNEL_DEPARTMENT: {
       *               hidden: true
       *             }
       *           }
       *         },
       *         durationTypeSelected: {
       *           fieldProperties: {
       *             IX_MAP_HR_PERSONNEL_DEPARTMENT: {
       *               value: "Sales"
       *               hidden: false
       *             }
       *           }
       *         },
       *         departmentHasAValue: {
       *           fieldProperties: { IX_MAP_HR_PERSONNEL_DEPARTMENT: { hidden: false } }
       *         },
       *       }
       *     }
       *
       * ### Explanation
       *
       *  First, during initialization, the stateInit state is set. Here, we assign a responder
       *  function to the field "IX_MAP_DURATION_TYPE", which will set the state "durationTypeSelected"
       *  as soon as the field has any value.
       *
       *  The "myDefaultState" is set directly after "stateInit", because we defined it as me.defaultState
       *  In "myDefaultState", the field IX_MAP_HR_PERSONNEL_DEPARTMENT will be hidden.
       *
       *  If someone enters a value into the "IX_MAP_DURATION_TYPE" field, its responder will be executed.
       *  The responder sets the state "durationTypeSelected", which sets the DEPARTMENT-field's value to
       *  "Sales" and unhides/shows the field in the form.
       *
       *  Validators:
       *  You can define Validators in the fieldProperties of a field by assigning a "validator:" property.
       *  Tip: You don't have to define the validation function ("JS_VAL_...") in the form designer anymore!
       *
       *  BodyClasses:
       *  You can define CSS-Bodyclasses in every state:
       *  myDefaultState: { bodyClasses: ["greatDesign", "flexibleTable"] }
       *  Attention: body classes will be removed, as soon as you execute "setState" for another state.
       *  To keep body-classes during state-change, you must define the property e.g.
       *  "myDefaultState.removePreviousBodyClasses: false" in subsequent states
       *
       *  Template:
       *  To use a template, you have to define it instead of (or alongside) a field's properties.
       *  e.g.
       *
       *     ...
       *     fieldProperties: {
       *       IX_MAP_HR_PERSONNEL_CASSATIONTOGGLE: {
       *         template: "toggle"
       *         config: { ... } //see documentation at the end of this file for template configs.
       *       }
       *     }
       *     ...
       *
       *  Batchprocessing:
       *  It is tedious to define field:{hidden: true} if you want to hide many fields at once.
       *  You can use all field properties (hidden, readonly, optional, value, responder, ...) in batch
       *  processing. Example:
       *
       *     fieldProperties: {
       *       _batch: [{
       *         hidden: {
       *           val: true,
       *           fields: [
       *             "IX_GRP_HR_PERSONNEL_DATEOFJOINING", "IX_MAP_HR_PERSONNEL_PROBATIONARYPERIODDURATION"
       *           ]
       *         }
       *       }]
       *     }
       *
       *  Of course you can combine batch-processing and ´normal´ attribute setting.
       *
       *  If you want to define your own templates, you can do so in the "OnInit" function.
       *  Just add your own templates to form.templates. E.g.:
       *
       *     form.templates.mygreattemplate = function (config) { ...  return configuredTemplate; };
       *
       * ### Conclusion
       *
       *  OnInitAndTabChange should be used to set up the environment for the user.
       *  (what happens if the user switches tabs,... )
       *
       *  Any other state should be defined as ´state´.
       *  If you need more control than the standard field properties (value, hide,...) can offer,
       *  you can and should implement the logic directly in a field's responder instead of creating
       *  a state which won't have sufficient facilities to represent the desired state anyways.
       *
       *
       * # Additional features
       * ### Business Solution Coversheet
       *  The Business Solution Coversheet provides an easy way to display information in a form as readonly-labels
       *  instead of input boxes.
       *
       *  To display any MAP/GRP field in the coversheet, prepend "VIEW_" to its name when defining it in the coversheet
       *  template. e.g. IX_MAP_TEST -> VIEW_IX_MAP_TEST
       *
       *  The template containing the coversheet must be named ending with "__cover" (two underscores)
       *
       *  The required CSS rules will then be applied automatically.
       *
     * @author ESt, ELO Digital Office GmbH
     *
     * @elowf
     * @requires sol.common.DateUtils
     * @requires sol.common.forms.Utils
     */
    sol.define("sol.common.forms.FormWrapper", {

      /**
       * will contain all fields of the form after initializeFields has been called
       */
      fields: {},

      /**
       * will contain all tables which are part of the form
       */
      tables: {},

      /**
       * will contain all tabs of the form after initializeTabs has been called
       */
      tabs: {
        all: {
          parts: {}
        }
      },

      /**
       * will contain rules, which are applied when a user presses the save button
       */
      onSaveRules: {},

      /**
       * will contain all template OnInitAndTabChange functions after setState has been called
       */
      onInitAndTabChangeFunctions: [],

      /**
       * holds the active state's name as a string
       */
      activeState: undefined,

      /**
       * true if the form is running in workflow mode. (set in initialize)
       */
      workflowActive: false,

      /**
       * Version of ELO-wf parsed to an Integer. (10.04.032 = 1004032)
       */
      wfVersion: 0,

      /**
       * Name of ELO-wf (e.g. wf-Solutions)
       */
      wfName: "",

      /**
       * Default editor options. editor won't be initialize at start by default
       * because of backwards compatibility
       */
      editorOptions: { initImmediately: false, redactorOptions: {} },

      /**
       *  logs all fieldnames and their values to the console (as a table if implemented in the JS-Engine).
       *  @param {Boolean} nolog returns fields and their values as an array instead of doing a console-log
       *  @returns {[{fieldName: String, fieldValue: String}] || undefined}
       */
      logFieldValues: function (nolog) {
        var me = this;
        return (function (keys) {
          return (nolog && keys) || (console.table ? console.table(keys) : console.log(keys));
        })(
          Object.keys(me.fields)
            .sort(function (a, b) {
              return (a).localeCompare(b);
            })
            .map(function (field) {
              return {
                fieldName: field,
                fieldValue: me.fields[field].value()
              };
            })
        );
      },

      /**
       * returns true if the passed string `str` ends with passed string `needle`, otherwise false
       */
      endsWith: function (str, needle) {
        return (typeof str === "string" && typeof needle === "string")
          && (str.lastIndexOf(needle) + needle.length === str.length);
      },

      /**
       * returns true if the running WF's version is >= the passed `requiredVersion`
       * requiredVersion (String)
       *  e.g. "10.02.000"
       */
      wfMeetsVersionRequirement: function (requiredVersion) {
        var me = this;
        return (typeof requiredVersion === "string" ? +(requiredVersion.replace(/\./g, "", true)) : 0) <= me.wfVersion;
      },

      /**
       * determines the active tab's id e.g. "_510_time_details" and returns it
       * @returns {String}
       */
      getActiveTabId: function () {
        var activeTab = document.getElementsByClassName("selected");
        if (activeTab.length > 0) {
          return activeTab[0].hash && activeTab[0].hash.replace(/^#/, "_") || "none";
        } else {
          return "none";
        }
      },

      /**
       * sets a state from the "states"-object by name
       * @param {String} stateName name of the state as defined in the "states"-object
       */
      setState: function (stateName) {
        var me = this,
            state = me.states[stateName],
            delOldClasses;

        if (state === undefined) {
          return;
        }
        delOldClasses = state.removePreviousBodyClasses;
        // default is to delete old classes
        (delOldClasses === undefined || delOldClasses) && (me.activeState != undefined) && me.removeOldBodyClasses();

        me.setNewBodyClasses(state.bodyClasses);
        me.setFieldProperties(state.fieldProperties);
        me.setTabProperties(state.tabProperties);
        state.reassignlkwls && me.reassignLocalizedKwlValues({
          force: true
        });
        me.assignValidators(state);
        me.assignValidationFunctionsToWindow();
        me.assignResponders(state);

        if (state.OnInitAndTabChange && !(state.OnInitAndTabChange in me.onInitAndTabChangeFunctions)) {
          me.onInitAndTabChangeFunctions.push(state.OnInitAndTabChange);
        }

        me.activeState = stateName;
      },

      /**
       * sets localizedKwl keys again (e.g. to receive translated text)
       * this is usually run once during initialization, but can be called manually at any time
       * @param {force: Boolean} opts if defined, forces a reassignment of all localizedKwl values.
       *                              Usually, only incomplete localizedKwl values are reassigned
       */
      reassignLocalizedKwlValues: function (opts) {
        var me = this;
        for (var fieldname in me.fields) {
          var field = me.fields[fieldname],
              fValue = field.value({
                full: true
              });
          if (field.isLocalized) {
            field.localizedDynKwlName
              && (
                fValue
                && (
                  (opts && opts.force) ||
                  (fValue.indexOf(" - ") === -1) ||
                  (fValue.search(/\w+ - ?$/g) === 0)
                )
              ) && field.set(field.value());
          }
        }
      },

      /**
       * removes all bodyClasses which were set by the previous state
       */
      removeOldBodyClasses: function () {
        var me = this;
        me.removeBodyClasses(me.states[me.activeState].bodyClasses);
      },

      /**
       * removes bodyclasses
       * @private
       */
      removeBodyClasses: function (bodyClasses) {
        var me = this;
        bodyClasses && bodyClasses.forEach(function (bc) {
          me.removeBodyClass(bc);
        });
      },

      /**
       * @private
       */
      setNewBodyClasses: function (bodyClasses) {
        var me = this;
        me.setBodyClasses(bodyClasses);
      },

      /**
       * @private
       */
      setBodyClasses: function (bodyClasses) {
        var me = this;
        bodyClasses && bodyClasses.forEach(function (bc) {
          me.addBodyClass(bc);
        });
      },

      /**
       * determines the current day as isoDate using the beginning of the day as time and returns it
       * @returns {Integer (isodate)}
       */
      today: function () {
        return sol.common.DateUtils.dateToIso(new Date(), {
          startOfDay: true
        });
      },

      /**
       * iterates over the fieldProperties defined in a state and assigns these properties
       * to the respective field
       * @private
       */
      setFieldProperties: function (fieldProperties) {
        var me = this;
        for (var fieldName in fieldProperties) {
          if (fieldName == "_batch") {
            me.batchProcessProperties(fieldProperties[fieldName]);
          } else {
            me.processAFieldsProperties(fieldName, fieldProperties[fieldName]);
          }
        }
      },

      /**
       * iterates over the batchProperties defined in a state and performs the batch-tasks accordingly
       * @private
       */
      batchProcessProperties: function (batchProperties) {
        var me = this,
            propsAsArray = [];
        if (!(batchProperties instanceof Array)) {
          for (var key in batchProperties) {
            propsAsArray.push(batchProperties[key]);
            batchProperties = propsAsArray;
          }
        }
        try {
          batchProperties.forEach(function (o) {
            for (var prop in o) {
              var fields = o[prop].fields,
                  value = o[prop].val,
                  property = {},
                  noLog;
              property[prop] = value;

              if (fields === "all") {
                noLog = true;
                fields = Object.keys(me.fields);
              }
              fields.forEach(function (name) {
                me.processAFieldsProperties(name, property, noLog);
              });
            }
          });
        } catch (e) {
          console.log("_batch property not defined correctly:", e);
        }
      },

      /**
       * used in setFieldProperties to map the defined properties to functions
       * @private
       */
      processAFieldsProperties: function (fieldName, aFieldsProperties, noLog) {
        var me = this, workflowOnlyModes = Array.isArray(aFieldsProperties["workflowOnly"]) && aFieldsProperties["workflowOnly"];

        for (var option in aFieldsProperties) {
          var optionValue = aFieldsProperties[option],
              overwrite = aFieldsProperties["overwrite"],
              field = me.fields[fieldName];
          if (!field || !field.element()) {
            noLog || console.log("Field '" + fieldName + "' does not exist! Skipping ...");
            return;
          }

          if (!me.workflowActive && (workflowOnlyModes && (workflowOnlyModes.indexOf(option) > -1))) {
            noLog || console.log("Rule '" + option + "' for '" + fieldName + "' only active in workflows! Skipping ...");
            continue;
          }

          switch (option) {
            case "value":
              (overwrite && field.set(optionValue)) || (!field.value() && field.set(optionValue));
              break;
            case "readonly":
              field.setAttribute(option, optionValue);
              break;
            case "optional":
              field.setAttribute(option, optionValue);
              break;
            case "hidden":
              field.setAttribute(option, optionValue);
              break;
            case "tooltip":
              field.writeTooltip(optionValue);
              break;
            case "placeholder":
              field.setPlaceholder(optionValue);
              break;
            case "template":
              me.applyTemplate(optionValue);
              field.template = optionValue; // it's nice to have access to the assigned template later...
              break;
            default:
              break;
          }
        }
      },

      /**
       * iterates over the tabProperties defined in a state and assigns these properties
       * to the respective tab. However, you should not try to assign properties to tabs
       * but to parts. (tab names can change too easily which winds up to orphaned rules)
       * @private
       */
      setTabProperties: function (tabProperties) {
        var me = this;
        for (var tabId in tabProperties) {
          //if (tabId == "_batch") {
          //me.batchProcessProperties(tabProperties[fieldName]); // not implemented yet
          //} else { no more else, see description }
          for (var partId in tabProperties[tabId]) {
            me.processAPartsProperties(partId, tabId, tabProperties[tabId][partId]);
          }
        }
      },

      /**
       * used in setTabProperties to map the defined properties to functions
       * @private
       */
      processAPartsProperties: function (partId, tabId, aPartsProperties) {
        var me = this;
        for (var option in aPartsProperties) {
          var optionValue = aPartsProperties[option],
              tab = me.tabs[tabId],
              part, resolvedOptionValue;

          if (!tab) {
            console.log("Tab '" + tabId + "' does not exist! Skipping ...");
            return;
          }

          part = tab.parts[partId];
          if (!part) {
            console.log("Formpart '" + partId + "' does not exist! Skipping ...");
            return;
          }
          switch (option) {
            case "hidden":
              resolvedOptionValue = typeof optionValue === "function"
                ? optionValue.call(this)
                : optionValue;
              part.setAttribute(option, resolvedOptionValue);
              break;
            case "template":
              me.applyTemplate(optionValue);
              part.template = optionValue; // it's nice to have access to the assigned template later...
              break;
            default:
              break;
          }
        }
      },

      setRowProperties: function (row, rowProperties) {
        var me = this, field;
        for (var tableId in rowProperties) {
          for (var columnName in rowProperties[tableId]) {
            field = row[columnName];
            field && (function (f) {
              me.processAFieldsProperties(f.name, rowProperties[tableId][columnName]);
              f.responder = rowProperties[tableId][columnName].responder;
            })(field);
          }
        }
      },

      /**
       * renders a template using the passed config and adds it to the statemanager
       * @private
       */
      applyTemplate: function (templateObject) {
        var me = this,
            tName = templateObject.name,
            tConfig = templateObject.config,
            template = me.templates[tName],
            result;

        result = template.call(me, tConfig);
        me.addTemplateStatesToStates(result.states);

        if (result.OnInitAndTabChange && !(result.OnInitAndTabChange in me.onInitAndTabChangeFunctions)) {
          me.onInitAndTabChangeFunctions.push(result.OnInitAndTabChange);
        }

        if (result.OnSaveRule) {
          me.onSaveRules[result.OnSaveRule.name] = result.OnSaveRule.rule;
        }

        if (result.GlobalFunctions) {
          for (var functionName in result.GlobalFunctions) {
            window[functionName] = result.GlobalFunctions[functionName];
          }
        }
      },

      /**
       * adds states to the statemanager
       * @private
       */
      addTemplateStatesToStates: function (templateStates) {
        var me = this;
        for (var stateName in templateStates) {
          var state = templateStates[stateName];
          if (!state) {
            continue;
          }
          if (me.states[stateName] === undefined) {
            me.states[stateName] = {
              fieldProperties: {}
            };
          }
          for (var fieldName in state.fieldProperties) {
            var field = state.fieldProperties[fieldName];
            if (me.states[stateName].fieldProperties[fieldName] === undefined) {
              me.states[stateName].fieldProperties[fieldName] = {};
            }
            if (me.states[stateName].fieldProperties === undefined) {
              me.states[stateName].fieldProperties = {};
            }
            for (var propName in field) {
              var fieldInState = me.states[stateName].fieldProperties[fieldName];
              fieldInState[propName] = field[propName];
            }
          }
          for (var tabId in state.tabProperties) {
            var tab = state.tabProperties[tabId];
            if (me.states[stateName].tabProperties === undefined) {
              me.states[stateName].tabProperties = {};
            }
            if (me.states[stateName].tabProperties[tabId] === undefined) {
              me.states[stateName].tabProperties[tabId] = {};
            }
            for (var propName in tab) {
              var tabInState = me.states[stateName].tabProperties[tabId];
              tabInState[propName] = tab[propName];
            }
          }
        }
      },

      removeBodyClass: sol.common.forms.Utils.removeBodyClass,
      addBodyClass: sol.common.forms.Utils.addBodyClass,

      /**
       * determines a short name of a field using the prefix e.g. HR_PERSONNEL.
       *
       * IX_MAP_HR_PERSONNEL_DEPARTMENT -> department
       * @param {String} fieldName e.g. IX_MAP_HR_PERSONNEL_DEPARTMENT
       * @param {String} prefix e.g. HR_PERSONNEL
       * @returns {String}
       */
      getShortName: function (fieldName, prefix) {
        if (fieldName.indexOf(prefix) != -1) {
          return fieldName.slice(fieldName.indexOf(prefix) + prefix.length + 1).toLowerCase();
        } else {
          var match = /IX_\w{2,5}_(.+)/g.exec(fieldName);
          if (match && match[1]) {
            return match[1].toLowerCase();
          }
        }
        return fieldName;
      },

      determineUndefinedFields: function (params) {
        var me = this;
        return Object.keys(params || {})
          .filter(function (param) {
            return me.propIsField(param) && (!$var(param));
          });
      },

      /**
       * builds the "fields" object, which is accessible via me.fields
       * using the FormWrapper.Field Class
       * @private
       */
      initializeFields: function () {
        var me = this;

        me.getNamesOfAllFieldsOnForm().forEach(function (name) {
          var field = sol.create("sol.common.forms.FormWrapper.Field", {
            fName: name,
            shortName: me.getShortName(name, me.prefix),
            validator: undefined,
            responder: undefined,
            prefix: me.prefix,
            form: me
          });
          if (field) {
            me.fields[field.fName] = field; // makes field accessible by long name
            field.viewSource = field.fName.indexOf("VIEW_") === 0 ? field.fName.replace(/^VIEW_/, "") : undefined;
          }
        });
        // if fields are views, fill them with values from their source
        for (var fieldName in me.fields) {
          var field = me.fields[fieldName], viewSource, num, numVal = undefined;
          if (field.viewSource) {
            field.viewSource = me.fields[field.viewSource];
            field.element().classList.add("sol-coversheetfield");
            if (field.viewSource && field.viewSource.value()) {
              if (num = ((viewSource = field.viewSource).element().getAttribute("inputtype") === "num")) {
                numVal = ELOF.numberToAmount(String(viewSource.value({ asNumber: true })), viewSource.element());
                field.element().innerHTML = numVal;
              }
              if (!num || numVal == undefined) {
                field.viewSource.isDate
                  ? field.set(elo.wf.date.format(field.viewSource.value()))
                  : field.element().innerHTML = (field.viewSource.value({
                    localizedStringOnly: true
                  }).replace(/(?:\r\n|\r|\n)/g, "<br />"));
              }
            }
          }
        }
      },

      setFocusField: function () {
        var me = this,
            startElement = me.getStartElementToFocus();

        if (startElement) {
          startElement.focus();
        }
      },

      getStartElementToFocus: function () {
        var activeTabButton = document.querySelector("div.tabs > ul a.selected[tabfocus]"),
            startElementName = activeTabButton ? activeTabButton.getAttribute("tabfocus") : null;

        return startElementName ? $var(startElementName) : null;
      },

      determineTableSize: function (tableCols) {
        var me = this, tableSize = 0;
        Object.keys(me.fields)
          .forEach(function (name) {
            var match = name.match(/(.*[^0-9])(\d+)$/), row;
            if (match && ~tableCols.indexOf(match[1])) {
              row = +(match[2]);
              (row > tableSize) && (tableSize = row);
            }
          });
        return tableSize;
      },

      initializeTables: function () {
        var me = this, tables = {};
        Array.prototype.slice.call(document.querySelectorAll("tbody[eloelems*='JS_ADDLINE']"))
          .forEach(function (table) {
            var jsAdd, id, colCount, cols, tb;
            jsAdd = table.querySelector("input[name='JS_ADDLINE']");
            if (jsAdd) {
              id = jsAdd.getAttribute("addlineid") || "default";
              tb = (tables[id] = {});

              tb.columns = table.getAttribute("eloelems").split(",")
                .filter(function (name) {
                  return (name.indexOf("1") === (name.length - 1)) && me.fields[name];
                })
                .map(function (col) {
                  return col.substr(0, col.length - 1);
                });
              colCount = (cols = tb.columns).length;
              tb.size = me.determineTableSize(tb.columns);
              tb.id = id;
              tb.form = me;
              tb.getRow = me.getTableRow.bind(me, cols, colCount);
              tb.isEmpty = me.isEmpty;
              tb.isEmptyRow = me.isEmptyRow;
              tb.addRow = me.addRow.bind(tb, jsAdd.onclick.bind(jsAdd));
              tb.removeRow = me.removeRow.bind(tb);
              tb.insert = me.insert.bind(tb);
              tb.forEach = me.tableForEach;
              tb.map = me.tableMap;
              tb.filter = me.tableFilter;
              tb.slice = me.tableSlice;
              tb.applyRowProperties = me.applyRowProperties;
              tb.getRowByField = me.getRowByField.bind(tb); // bind current table at 'this' context

              /**
               *  each field should have access to it's own table via tableId
               */
              tb.forEach(function (row) {
                Object.keys(row)
                  .filter(function (prop) {
                    return prop !== "row";
                  })
                  .map(function (fieldName) {
                    return row[fieldName];
                  })
                  .forEach(function (colField) {
                    colField && (colField.tableId = tb.id);
                  });
              });
            }
          });

        me.tables = tables;

        Object.keys(tables)
          .forEach(function (tableId) {
            var table = tables[tableId];
            me.removeDeletedTrailingRows(table.id, 1, table.size);
          });

        me.fields = me.getNewFieldsObject();
      },

      /**
       * Determine whether the table is empty or not.
       *
       * @param {Array|String|undefined} [checkColumns] - checks every passed column whether columns has some content
       *                                                  default all row columns
       * @returns true if each column is empty of the whole table
       */
      isEmpty: function (checkColumns) {
        var me = this,
            cols = checkColumns || me.columns;

        // use all columns if columns was not set
        cols = Array.isArray(cols)
          ? cols
          : [cols];

        return me.map(function (row) {
          return row;
        })
          .every(function (row) {
            return me.isEmptyRow(row, cols);
          });
      },

      isEmptyRow: function (row, columns) {
        return !columns.some(function (fieldName) {
          var field = row[fieldName], value = field && field.value();
          return (value !== undefined && value !== "");
        });
      },

      addRow: function (fn, empty) {
        var me = this, isEmpty = false, row = me.getRow(me.size);
        if (empty) {
          // TODO: refactor to use isEmptyRow here
          isEmpty = !(Array.isArray(empty) ? empty : [empty])
            .some(function (fieldName) {
              var field = row[fieldName], value = field && field.value();
              return (value !== undefined && value !== "");
            });
        }

        isEmpty || fn(); // only add new row if last row is not empty

        return me.getRow(me.size);
      },

      applyRowProperties: function (row, attributes, options) {
        var me = this, columns;

        options = options || {};
        attributes = attributes || {};
        // client can override columns to use via options
        columns = options.columns || me.columns;

        if (Object.keys(attributes).length === 0) {
          return;
        }

        if (!Array.isArray(columns)) {
          throw Error("columns should be provided as an Array!", columns);
        }

        // set all attributes to each cell of the passed row
        columns.forEach(function (column) {
          var cell = row[column];
          cell && me.form.processAFieldsProperties(cell.fName, attributes);
        });

      },

      /**
       * Remove the passed row object
       *
       * Unfortunately there is no well provided api function
       * to remove a row from a table, so be careful with this function.
       *
       * It's actually a workaround to find the
       * remove button within the same row and simulate a click on
       * the JS_REMOVELINE button (like addRow function in formwrapper)
       *
       * @param {Object} row - the table row object which should be removed
       * @param {Object} [options]
       * @param {String} [options.indicatorField] source column to find JS_REMOVELINE Button
       *                 If not set use first column name as default
       */
      removeRow: function (row, options) {
        var me = this,
            indicatorColumn = options && options.indicatorColumn;

        if (!indicatorColumn && me.columns.length > 0) {
          // use first column as an indicatorColumn
          indicatorColumn = me.columns[0];
        }

        if (row) {
          try {
            row[indicatorColumn].element()
              .closest("tr") // find parent tr and search then the JS_REMOVELINE Button from there
              .querySelector("a.jsRemoveLine")
              .click();
          } catch (ex) {
            console.warn("Could not determine remove button - Did you add JS_REMOVELINE and JS_ADDLINE in your form?");
          }
        } else {
          console.warn("row could not be remove because row is not set");
        }
      },

      insert: function (data, emptyFields) {
        var me = this, inserted = [];
        (Array.isArray(data) ? data : [data])
          .forEach(function (obj) {
            var row = me.addRow(emptyFields);
            Object.keys(obj || {})
              .forEach(function (col) {
                row[col] && row[col].set(obj[col]);
              });
            inserted.push(row);
          });
        return inserted;
      },

      getTableRow: function (cols, colCount, row) {
        var me = this, sizeOutOfBounds = (row > me.size || row < 1),
            prettyCol = { row: sizeOutOfBounds ? undefined : row },
            formFields = me.fields, k, col;
        for (k = 0; k < colCount; k++) {
          prettyCol[col = cols[k]] = formFields[col + row];
        }
        return prettyCol;
      },

      tableForEach: function (cb, thisArg) {
        var table = thisArg || this, size = table.size, i;
        for (i = 1; i <= size; i++) {
          cb.call(table, table.getRow(i), i, table);
        }
      },

      tableMap: function (cb, thisArg) {
        var table = thisArg || this, size = table.size, i, result = [];
        for (i = 1; i <= size; i++) {
          result.push(cb.call(table, table.getRow(i), i, table));
        }
        return result;
      },

      tableFilter: function (cb, thisArg) {
        var table = thisArg || this, size = table.size, i, prettyCol, result = [];
        for (i = 1; i <= size; i++) {
          cb.call(table, (prettyCol = table.getRow(i)), i, table) && result.push(prettyCol);
        }
        return result;
      },

      tableSlice: function (from, to) {
        var me = this, size = me.size, i, result = [], max = to || size + 1;
        for (i = from || 1; i <= size && i < max; i++) {
          result.push(me.getRow(i));
        }
        return result;
      },

      getRowByField: function (field) {
        var me = this, // should be a table object via bind
            result;
        if (!field) {
          return;
        }

        result = me.columns
          .filter(function (col) {
            return field.fName.startsWith(col);
          })
          .map(function (colName) {
            return field;
          })
          .map(function (colField) {
            return me.getRow(sol.common.forms.Utils.getFieldNameIndex(colField.fName));
          });

        return result[0]; // return undefined if field is not a column field
      },

      /**
       * determines if the passed string is a field which can be handled by the FormWrapper.
       * @returns {Boolean}
       */
      propIsField: function (prop) {
        return (
          prop.indexOf("IX_MAP_") === 0
          || prop.indexOf("WF_MAP_") === 0
          || prop.indexOf("IX_GRP_") === 0
          || prop.indexOf("VIEW_") === 0
        );
      },

      /**
       * collects all fields that may be used in the form
       * @private
       */
      getNamesOfAllFieldsOnForm: function () {
        var me = this,
            tables = document.getElementsByTagName("tbody"),
            fields = [],
            tableNo = -1;
        while (tables[++tableNo]) {
          fields = fields.concat(tables[tableNo].getAttribute("eloelems").split(","));
        }

        // find viewfields
        for (var tabName in me.tabs) {
          var tab = me.tabs[tabName];
          if (tab && tab.containsCover) {
            Array.prototype.slice.call(tab.element.querySelectorAll("[elonodename]")).filter(function (element) {
              return element.id.indexOf("VIEW_") === 0;
            }).forEach(function (field) {
              fields.push(field.id.trim());
            });
          }
        }

        for (var prop in ELO_PARAMS) {
          var me = this,
              value = ELO_PARAMS[prop];
          typeof value === "string" &&
            me.propIsField(prop) &&
            fields.push(prop);
        }

        return fields.filter(function (item, index, a) {
          return a.indexOf(item) == index;
        });
      },

      removeFromUndefinedFields: function (name) {
        var me = this, foundAt;
        ((foundAt = me.undefinedFields.indexOf(name)) > -1)
          && me.undefinedFields.splice(foundAt, 1);
      },

      /**
       * adds a new field to the "fields" object, which is accessible via me.fields
       * using the FormWrapper.Field Class
       * @private
       */
      initializeNewField: function (name) {
        var me = this,
            field;
        field = sol.create("sol.common.forms.FormWrapper.Field", {
          fName: name,
          shortName: me.getShortName(name, me.prefix),
          validator: undefined,
          responder: undefined,
          prefix: me.prefix,
          form: me
        });
        if (field) {
          me.fields[field.fName] = field; // makes field accessible by long name
          me.removeFromUndefinedFields(name); // field has been defined on the form, so remove it from undefined fields
          return field;
        }
      },

      /**
       * adds new fields (which were added to the form since initialize) to the "fields" object, which is accessible via me.fields
       * using the FormWrapper.Field Class
       */
      initializeNewFields: function () {
        var me = this;
        me.getNewFieldsOnForm().forEach(function (name) {
          var field = sol.create("sol.common.forms.FormWrapper.Field", {
            fName: name,
            shortName: me.getShortName(name, me.prefix),
            validator: undefined,
            responder: undefined,
            prefix: me.prefix,
            form: me
          });
          if (field) {
            me.fields[field.fName] = field; // makes field accessible by long name
          }
        });
      },

      /**
       * collects all fields that were added to the form since initialize
       * @private
       */
      getNewFieldsOnForm: function () {
        var me = this,
            oldFields = Object.keys(me.fields),
            newFields = [];
        for (var prop in ELO_PARAMS) {
          var value = ELO_PARAMS[prop];
          typeof value === "string" &&
            me.propIsField(prop) &&
            oldFields.indexOf(prop) === -1 && newFields.push(prop);
        }
        return newFields;
      },

      /**
       * Takes an arbitrary amount of arguments which can either be a String or an object.
       * When an argument is a string, it will be used to look up the respective field/template/table
       * from the form. Otherwise the argument is passed to the callback as is.
       *
       * The callback, which must be passed as the last parameter, will only be executed, if none of
       * the calls arguments are undefined. If some of the arguments are undefined the function
       * always returns false.
       *
       * Example:
       * If the field IX_GRP_MY_FIELD exists and the field IX_DESC exists and a template called mytemplate exists
       * and a table called mytable exists in the form, execute the callback.
       *
       *
       *     form.when("IX_GRP_MY_FIELD", form.fields.IX_DESC, "mytemplate", "mytable", function (field, desc, template) {
       *       field.set(desc.value());
       *       template.hide();
       *       return field.value();
       *     });
       */
      when: function () {
        function isDefined(arg) {
          return arg !== undefined;
        }
        function toWrapperObj(r) {
          if (typeof r == "string" && (r = r.trim())) { // sanitize
            return me.fields[r] || me.tabs.all.parts[r] || me.tables[r];
          }
          return r;
        }

        var me = this,
            required = Array.prototype.slice.call(arguments),
            cb = required.pop(),
            args = required.map(toWrapperObj),
            allDefined = args.every(isDefined);

        if (typeof cb == "function" && allDefined) {
          return cb.apply(cb, args);
        } else {
          return allDefined;
        }
      },

      /**
       * builds the "tabs" object, which is accessible via me.tabs
       * using the FormWrapper.Tab Class
       * @private
       */
      initializeTabs: function () {
        var me = this;
        me.tabs.all = sol.create("sol.common.forms.FormWrapper.Tab", { parentForm: me, parts: {} });
        me.getElementsOfAllTabsOnForm().forEach(function (tabDiv) {
          var tab = sol.create("sol.common.forms.FormWrapper.Tab", {
            name: tabDiv.id,
            element: tabDiv,
            parentForm: me
          });
          if (tab && tab.name && tab.element) {
            tab.id = tab.name.replace(/^/, "_");
            me.tabs[tab.id] = tab; // makes tab accessible
            me.tabs[tab.id].parts = {};
            me.getPartsOfTab(tab).forEach(function (partDiv) {
              var children, part;

              children = partDiv && partDiv.id && partDiv.getAttribute && partDiv.getAttribute("eloelems");
              part = sol.create("sol.common.forms.FormWrapper.Part", {
                name: partDiv && partDiv.id,
                element: partDiv,
                childrenFields: children ? children.split(",") : []
              });
              if (part && part.name && part.element) {
                part.id = part.name.replace(/^part/, "");
                part.isCover = me.endsWith(part.id, "__cover") ? true : false;
                if (me.endsWith(part.id, "__cover")) {
                  tab.element.classList.add("sol-coversheet-tab");
                  tab.containsCover = true;
                }
                part.isCover && part.element.classList.add("sol-coversheet");
                if (part.id) {
                  me.tabs[tab.id].parts[part.id] = part;
                  me.tabs["all"].parts[part.id] = part;
                }
              }
            });
          }
        });
      },

      /**
       * determines all tab Elements
       * @returns [{} (HTML-div)]
       * @private
       */
      getElementsOfAllTabsOnForm: function () {
        var tabDivs = [],
            tabElements = document.getElementsByClassName("tabContent");

        for (var n in tabElements) {
          var tabDiv = tabElements.namedItem ? (tabElements.namedItem((!isNaN(parseFloat(n)) && isFinite(n)) ? tabElements[n].id : n)) : tabElements[n];
          tabDiv && tabDivs.push(tabDiv);
        }
        return tabDivs;
      },

      /**
       * determines all template/part elements inside tabs
       * @returns [{} (HTML-div)]
       * @private
       */
      getPartsOfTab: function (tab) {
        var partDivs = [],
            partElements = tab.element.getElementsByTagName("tbody");
        for (var n in partElements) {
          var partDiv = partElements.namedItem ? partElements.namedItem((!isNaN(parseFloat(n)) && isFinite(n)) ? partElements[n].id : n) : partElements[n];
          partDiv && partDivs.push(partDiv);
        }
        return partDivs;
      },

      /**
       * assigns the validator function to the window object, just like ELO WF does it.
       * @private
       */
      assignValidationFunctionsToWindow: function () {
        var me = this;
        for (var field in me.fields) {
          if (me.fields[field].validator || (me.fields[field].validator === false)) {
            window["JS_VAL_" + field.toUpperCase()] = me.fields[field].validator;
            me.reassignEloVerify(me.fields[field].element(), "JS_VAL_" + field.toUpperCase());
          }
        }
      },

      /**
       * used by assignValidationFunctionToWindow
       * @private
       */
      reassignEloVerify: function (element, newJsVal) {
        var eloVerify = element.getAttribute("eloverify"),
            match, newVerify = newJsVal;
        if (eloVerify) {
          if (eloVerify.indexOf("JS_VAL_") == -1) {
            newVerify = eloVerify + " " + newJsVal;

          } else {
            match = /(.*) ?JS_VAL_[^ ](.*)/g.exec(eloVerify);
            newVerify = match && match[1] + " " + newJsVal + " " + match[2];
          }
        }
        element.setAttribute("eloverify", newVerify);
      },

      /**
       * assigns props defined in a state object to the respective fields
       */
      assignPropToFields: function (state, propname) {
        var me = this;
        for (var fieldName in state.fieldProperties) {
          var field = state.fieldProperties[fieldName];
          if (me.fields[fieldName] && (field[propname] || field[propname] === false)) {
            me.fields[fieldName][propname] = field[propname];
          }
        }
      },

      /**
       * Convert a MultiIndexField to a mapTable
       *
       * Current restriction limited to one column
       * @param {Field} multiIndexField
       * @param {String} mapTableColumn
       *
       * @throws If multiIndexField is undefined
       * @throws If mapTableColumn is undefined
       * @since 1.11.000
       */
      updateMultiIndexFromMapTableColumn: function (multiIndexField, mapTableColumn) {

        if (!multiIndexField) {
          throw Error("multiIndexField must be set");
        }

        if (!mapTableColumn) {
          throw Error("mapTableColumn must be set");
        }

        var indexArray = sol.common.forms.Utils.MultiIndex(),
            fields = multiIndexField.form.fields,
            tableIndexPos = mapTableColumn.length,
            pilcrows = new RegExp(String.fromCharCode(182), "g");

        Object.keys(fields)
          .filter(function (fieldName) {
            return fieldName.indexOf(mapTableColumn) === 0;
          })
          .filter(function (fieldName) {
            return !isNaN(+(fieldName.substr(tableIndexPos)));
          })
          .sort(function (a, b) {
            return +(a.substr(tableIndexPos)) - +(b.substr(tableIndexPos));
          })
          .forEach(function (fieldName) {
            var val = fields[fieldName].value({ full: true }).replace(pilcrows, " "); // if dynkwl field, receive complete value
            val && indexArray.add(val);
          });

        indexArray.save(multiIndexField.fName);
      },

      /**
       * Convert each value of a MultiIndex field to a specific column of a table
       *
       * The function create dyanmically a new row for each value of the MultiIndex value
       * @param {Field} multiIndexField source
       * @param {String} mapTableColumn All values of the MultiIndex field copied to this column
       * @param {String} [tableId=default] the table of the column
       *
       * @throws Will throw an error if tableId doesn't exist in form
       * @since 1.11.000
       */
      updateMapTableColumnsFromMultiIndex: function (multiIndexField, mapTableColumn, tableId) {
        var me = this, multiIndexArray, firstColumn = mapTableColumn + "1",
            pilcrows = new RegExp(String.fromCharCode(182), "g"),
            table = tableId || "default";

        if (!me.tables || !me.tables[tableId]) {
          throw Error("table " + tableId + " doesn't exist in form. Do you use the right name?");
        }

        multiIndexArray = multiIndexField.value().split(pilcrows);

        // set first entry manually because the row exists per definition
        me.fields[firstColumn].set(multiIndexArray.shift());

        setTimeout(function () {
          multiIndexArray.forEach(
            function (value) {
              var row = me.tables[table].addRow();
              row[mapTableColumn].set(value);
            }
          );
        }, 0);
      },

      /**
       * calculates a date in the future using input parameters
       * @param {Integer }isoDate         isoDate as starting point
       * @param {Integer }durationNumber  number representing e.g. weeks, days (any momentJS unit)
       * @param {String} durationUnit    momentJS unit descriptor (y, Q, M, w, d)
       * @param {String} terminationPoint    momentJS unit descriptor (y, Q, M, w, d)
       * @returns {Integer} the new date as an isoDate
       */
      calculateDate: function (isoDate, durationNumber, durationUnit, terminationPoint, offsetNumber, offsetUnit) {

        var srcDate, dstDate;

        srcDate = sol.common.DateUtils.isoToDate(String(isoDate));
        durationNumber = Number(durationNumber);
        dstDate = sol.common.DateUtils.shift(srcDate, durationNumber, {
          unit: durationUnit
        });

        dstDate = (terminationPoint && (moment(dstDate.getTime())).endOf(terminationPoint).toDate()) || dstDate;

        if (offsetNumber) {
          dstDate = sol.common.DateUtils.shift(dstDate, offsetNumber, {
            unit: offsetUnit || "d"
          });
        }

        return sol.common.DateUtils.dateToIso(dstDate, {
          startOfDay: true
        });
      },

      /**
       * see setCalculatedDate
       */
      returnCalculatedDate: function (srcDateFieldName, durationFieldName, terminationPointFieldName, dstDateFieldName, offsetNumber, offsetUnit) {
        return this.setCalculatedDate(srcDateFieldName, durationFieldName, terminationPointFieldName, dstDateFieldName, offsetNumber, offsetUnit, true);
      },

      dayIsDay: function (isoDate1, isoDate2) {
        return isoDate1.substr(6, 2) === isoDate2.substr(6, 2);
      },

      /**
       * uses calculateDate internally to simplify setting the value to a target field
       * @param {String} srcDateFieldName    start date field name or isoDate
       * @param {String} durationFieldName   field name of a field which holds a durationNumber
       * @param {String} terminationPointFieldName    field name of a field which holds a termination point (at the end of the month, year, quarter,...)
       * @param {String} dstDateFieldName    this field will receive the return value of calculateDate
       * @param {String} offsetNumber    adjusts the calculated date by value (+-x)
       * @param {String} offsetUnit    unit for adjustment
       * @param {Boolean} onlyReturn    if true, returns the calculated value instead of setting the value
       * @returns {undefined || Integer (isoDate)}
       */
      setCalculatedDate: function (srcDateFieldName, durationFieldName, terminationPointFieldName, dstDateFieldName, offsetNumber, offsetUnit, onlyreturn) {
        var me = this,
            srcIsoDate, durationNumber, durationUnit, terminationPoint, dstIsoDate;

        if (srcDateFieldName === undefined) {
          srcIsoDate = sol.common.DateUtils.dateToIso(new Date(), {
            startOfDay: true
          }); //today
        } else if (String(srcDateFieldName) !== "" && (String(srcDateFieldName).indexOf("IX_") == -1)) {
          srcIsoDate = +(srcDateFieldName); // isoDate from parameter
        } else {
          srcIsoDate = me.fields[srcDateFieldName] && me.fields[srcDateFieldName].value(); // isoDate from Field
        }

        if (srcIsoDate && me.fields[durationFieldName]) {
          durationNumber = me.fields[durationFieldName].value() || 0;
          terminationPoint = (terminationPointFieldName && me.fields[terminationPointFieldName].value()) || "";
          durationUnit = me.fields[me.fields[durationFieldName].selector.name].value(); // B-)
          // moment.js initial calculation
          dstIsoDate = me.calculateDate(srcIsoDate, durationNumber, durationUnit, terminationPoint);
          // lap years
          if ((!terminationPoint || (terminationPoint === "d")) && ((durationUnit !== "d" && me.dayIsDay(dstIsoDate, srcIsoDate)) || (durationUnit === "d" || durationUnit === "w"))) {
            dstIsoDate = me.calculateDate(dstIsoDate, -1, "d");
          }
          // calculate offset
          if (offsetNumber && offsetUnit) {
            dstIsoDate = me.calculateDate(dstIsoDate, 0, "d", undefined, offsetNumber, offsetUnit);
          }

          if (onlyreturn) {
            return dstIsoDate;
          }
          me.fields[dstDateFieldName].set(dstIsoDate);
        }
      },

      /**
       * assigns validators of a state to the respective field
       * @private
       */
      assignValidators: function (state) {
        var me = this;
        me.assignPropToFields(state, "validator");
      },

      /**
       * assings responders of a state to the respective field
       * @private
       */
      assignResponders: function (state) {
        var me = this;
        me.assignPropToFields(state, "responder");
      },

      OnLineChange: function (lineAdded, name, index) {
        return true;
      },

      OnAddLine: function (name, index, rowFields) {
        return true;
      },

      OnRemoveLine: function (name, index, rowFields) {
        return true;
      },

      AfterOnRemoveLine: function (name, index, removedFieldValues) {
        return true;
      },
      LineAdded: function (tableId, index) {
        var me = this;
        tableId || (tableId = "default");
        (index > me.tables[tableId].size) && (me.tables[tableId].size = index);
        return me.OnAddLine(tableId, index, me.initializeFieldsOfTableRow(tableId, index));
      },

      anyIdChanged: function (ids) {
        return ids
          .some(function (arr) {
            return arr[0].element().id !== arr[1];
          });
      },

      LRCb: function (tableId, index, ids, removedFieldValues) {
        var me = this;
        if (me.anyIdChanged(ids) || me.tableSizeChanged(tableId)) {
          me.removeDeletedTrailingRows(tableId, index, me.tables[tableId].size);
          me.tables[tableId].size = me.findTableSize(tableId, me.tables[tableId].size);
          me.fields = me.getNewFieldsObject();

          me.AfterOnRemoveLine(tableId, index, removedFieldValues);
        } else {
          window.setTimeout(function () {
            me.LRCb(tableId, index, ids, removedFieldValues);
          }, 1);
        }
      },

      tableSizeChanged: function (tableId) {
        var me = this;
        return me.tables[tableId].size != me.findTableSize(tableId, me.tables[tableId].size);
      },

      LineRemoved: function (tableId, index) {
        var me = this, ids, possiblyRemovedFields = {}, possiblyRemovedFieldValues = {};
        tableId || (tableId = "default");
        (ids = me.collectIdsOfTableRow(tableId, index))
          .forEach(function (arr) {
            var cur = arr[0];
            possiblyRemovedFields[cur.fName] = cur;
            possiblyRemovedFieldValues[cur.fName] = cur.value();
          });
        me.LRCb(tableId, index, ids, possiblyRemovedFieldValues); // start timeout callback for AfterOnRemoveLine

        return me.OnRemoveLine(tableId, index, possiblyRemovedFields);
      },

      removeDeletedTrailingRows: function (tableId, startFrom, tableSize) {
        var me = this, cur;
        for (cur = startFrom; cur <= tableSize; cur++) {
          me.initializeFieldsOfTableRow(tableId, cur); // reinitialize.
        }
      },

      initializeFieldsOfTableRow: function (tableId, rowNo) {
        var me = this, tableFields = me.tables[tableId].columns,
            length = tableFields.length, i, name, sName, fields = {}, state = me.getStateInit();
        // TODO: we should set responder of current state and tableId to the initializedField
        for (i = 0; i < length; i++) {
          if (me.element(name = (sName = tableFields[i]) + rowNo)) {
            !me.fields[name] && (function () {
              var f = me.initializeNewField(name);
              f.tableId = tableId;
              fields[sName] = f;
            })();
          } else {
            me.fields[name] = undefined;
          }
        }
        // initialize state for each row
        state && state.rowProperties && me.setRowProperties(me.tables[tableId].getRow(rowNo), state.rowProperties);
        return fields;
      },

      /**
       * wrapper around $var, can also work with images (which $var can't)
       * @param {String} fName the field name
       * @returns {HTML-DIV}
       */
      element: function (fName) {
        var el = $var(fName);
        if (!el) {
          el = document.getElementsByName(fName);
          el = el.length > 0 && el[0]; // var does not seem to find images, therefore try this
        }
        return el;
      },

      getActiveState: function () {
        var me = this;
        return me.states[me.activeState] || me.getStateInit();
      },

      getStateInit: function () {
        var me = this;
        return me.states["stateInit"];
      },

      collectIdsOfTableRow: function (tableId, rowNo) {
        var me = this, tableFields = me.tables[tableId].columns, length = tableFields.length, i, name, el, ids = [];
        for (i = 0; i < length; i++) {
          if (el = $var(name = tableFields[i] + rowNo)) {
            ids.push([me.fields[name], el.id]);
          }
        }
        return ids;
      },

      findTableSize: function (tableId, lastKnown) {
        var me = this, tableFields = me.tables[tableId].columns, length = tableFields.length, i;
        // start checking backwards from the last column to be known to be the largest
        while (lastKnown > 1) {
          for (i = 0; i < length; i++) {
            if ($var(tableFields[i] + lastKnown)) {
              return lastKnown;
            }
          }
          lastKnown--;
        }

        return lastKnown;
      },

      getNewFieldsObject: function () {
        var me = this, newFieldsObject = {}, fieldName, fields = me.fields, field;
        for (fieldName in fields) {
          (fieldName !== "")
            && (field = me.fields[fieldName])
            && (newFieldsObject[fieldName] = field);
        }
        return newFieldsObject;
      },

      /**
       * wrapper around OnInitAndTabChange which also unhides all fields on the form
       * @private
       */
      TabChanged: function () {
        var me = this;
        for (var fieldName in me.fields) {
          if (fieldName !== "") {
            var field = me.fields[fieldName];
            field.show(true, true);
          }
        }
        for (var tabId in me.tabs) {
          if (tabId !== "") {
            var tab = me.tabs[tabId];
            for (var partId in tab.parts) {
              if (partId !== "") {
                var part = tab.parts[partId];
                part.show();
              }
            }
          }
        }

        me.OnInitAndTabChange();
      },

      /**
       * Executes registered callbacks when the tab is changed or the form is initialized.
       * @private
       */
      OnInitAndTabChange: function () {
        var me = this;
        me.tabs.activeTab = me.tabs[me.getActiveTabId()];
        if (me.tabs.activeTab === undefined) {
          me.tabs.activeTab = {};
          me.tabs.activeTab.name = "";
        }

        // can be defined in extending class
        me.BeforeOnInitAndTabChange && me.BeforeOnInitAndTabChange(me.tabs.activeTab);

        me.onInitAndTabChangeFunctions.forEach(function (f) {
          f.call(me, me.tabs.activeTab);
        });

        // can be defined in extending class
        me.AfterOnInitAndTabChange && me.AfterOnInitAndTabChange(me.tabs.activeTab);
      },

      getNameOfKwlSource: function (source) {
        if (typeof source === "object" && source.$KEY && source.$VALUE) {
          return Object.keys(source).filter(function (prop) {
            return (prop !== "$KEY") && (prop !== "$VALUE");
          })[0];
        }
      },

      initializeRowFieldsOfMapTableKwl: function (kwlFields) {
        var me = this;
        kwlFields
          .forEach(function (field) {
            me.propIsField(field) && !me.fields[field] && me.initializeNewField(field);
          });
      },

      /**
       * Basically executes the responder of a field which was changed by user input.
       * It is also possible to define a BeforeInputChanged and a AfterInputChanged
       * function in the extending class to make changes before/after any field-change
       * without specifically depending on one field.
       * @private
       */
      InputChanged: function (source, name) {
        var me = this,
            fieldname, field, val, row;

        fieldname = source.name || name || me.getNameOfKwlSource(source);
        if (!me.fields[fieldname] && source.$KEY && source.$VALUE && ((fieldname.indexOf("IX_") !== 0) && (fieldname.indexOf("WF_") !== 0))) { //kwl
          fieldname = "IX_GRP_" + fieldname;
        }
        !me.fields[fieldname] && me.initializeNewField(fieldname);
        (!source.name && name) && me.initializeRowFieldsOfMapTableKwl(Object.keys(source || {}));
        field = me.fields[fieldname];
        val = field.value();

        // can be defined in extending class
        me.BeforeInputChanged && me.BeforeInputChanged(field);

        if (field.tableId) {
          row = me.tables[field.tableId].getRowByField(field);
        }

        field.responder && field.responder(me, me.activeState, field, val, row); // row is undefined if field is not a table row object

        // can be defined in extending class
        me.AfterInputChanged && me.AfterInputChanged(field);
      },

      /**
       * @private
       * Only returns false if the rule matches and saveValues is false.
       * Also executes "registerUpdate", if defined.
       * @param {Object} rule
       * @return {Boolean}
       */
      shouldISave: function (rule) {
        var mask = rule.maskName,
            soltype = rule.solType,
            ruleActive, saveValues;
        ruleActive =
          (mask && soltype && mask === ELO_PARAMS.IX_MASKNAME && soltype === ELO_PARAMS.IX_GRP_SOL_TYPE) ||
          (mask && !soltype && mask === ELO_PARAMS.IX_MASKNAME) ||
          (!mask && soltype && soltype === ELO_PARAMS.IX_GRP_SOL_TYPE);

        if (ruleActive) {
          rule.registerUpdate && sol.common.forms.Utils.registerUpdate((typeof rule.registerUpdate === "string") ? rule.registerUpdate : null);
          saveValues = ((typeof rule.saveValues) === "function") ? rule.saveValues.call(this) : rule.saveValues;
          return (saveValues !== undefined) ? saveValues : true;
        } else {
          return true;
        }
      },

      /**
       * executes OnSaveRules and consolidates their results
       * @private
       */
      executeOnSaveRules: function () {
        var me = this,
            combinedResults = true,
            result;
        for (var ruleName in me.onSaveRules) {
          var rule = me.onSaveRules[ruleName];
          if (rule) {
            result = me.shouldISave(rule);
            if (combinedResults) {
              combinedResults = result;
            }
          }
        }
        return combinedResults;
      },

      /**
       * OnSave callback
       * @private
       */
      OnSave: function () {
        var me = this,
            result = true;
        me.BeforeOnSave && me.BeforeOnSave();
        result = me.executeOnSaveRules();
        me.AfterOnSave && me.AfterOnSave(result);
        return result;
      },

      /**
       * initializes the class.
       */
      initialize: function () {
        var me = this, defaultState;

        me.workflowActive = ELO_PARAMS.ELO_FLOWID !== "-1";
        me.wfVersion = typeof ELOWF_VERSION === "string" ? +(ELOWF_VERSION.replace(/\./g, "", true)) : 0;
        if (window.location && typeof window.location.pathname === "string") {
          if (window.location.pathname.indexOf("plugin/de.elo.ix.plugin.proxy/wf")) {
            me.wfName = window.location.pathname.split("/");
            me.wfName = me.wfName.slice(1, me.wfName.length - 2).join("/");
          } else {
            me.wfName = window.location.pathname.split("/");
            me.wfName = me.wfName.length > 1 ? me.wfName[1] : "";
          }
        }

        me.initializeTabs();
        me.tabs.activeTab = me.tabs[me.getActiveTabId()];
        me.templates = sol.create("sol.common.forms.FormWrapper.Templates");
        me.undefinedFields = me.determineUndefinedFields(ELO_PARAMS); // fields which are not on the form, but in ELO_PARAMS
        me.initializeFields();
        me.setFocusField();
        me.initializeTables();
        me.OnInit && me.OnInit();
        me.setState("stateInit");

        defaultState = (typeof me.defaultState === "function")
          ? me.defaultState.call(me)
          : me.defaultState;

        defaultState && me.setState(defaultState);

        me.reassignLocalizedKwlValues();
      }
    });

    sol.define("sol.common.forms.FormWrapper.Tab", {
      initialize: function (config) {
        var me = this;
        me.$super("sol.Base", "initialize", [config]);
      },

      /**
      * hides all parts of a tab which where none of each part's fields has a value.
      */
      hidePartsContainingOnlyEmptyFields: function () {
        var me = this;
        Object.keys(me.parts)
          .filter(function (partName) {
            return (
              me.parts[partName].childrenFields
                .filter(function (fieldName) {
                  return me.parentForm.fields[fieldName].value() != "";
                }).length === 0
            );
          })
          .forEach(function (partName) {
            me.parts[partName].hide();
          });
      },

      /**
      * hides all parts which do not contain any of the fields listed in the passed array
      */
      hideUnnecessaryParts: function (necessaryFields) {
        var me = this;
        Object.keys(me.parts)
          .filter(function (partName) {
            return (
              me.parts[partName].childrenFields
                .filter(function (fieldName) {
                  return necessaryFields.indexOf(fieldName) > -1;
                }).length === 0
            );
          })
          .forEach(function (partName) {
            me.parts[partName].hide();
          });
      }
    });

    /**
     * Represents a form-tab's template/part
     *
     * @author ESt, ELO Digital Office GmbH
     *
       */
    sol.define("sol.common.forms.FormWrapper.Part", {
      initialize: function (config) {
        var me = this;
        me.$super("sol.Base", "initialize", [config]);
      },

      /**
       * Hides the template/part
       */
      hide: function () {
        var me = this;
        me.changeVisibility(false);
      },

      /**
       * Unhides/shows the template/part
       */
      show: function () {
        var me = this;
        me.changeVisibility(true);
      },

      /**
       * sets the style attribute accordingly
       * @private
       */
      changeVisibility: function (visible) {
        var me = this;
        me.element.style.display = visible ? "" : "none";
      },

      /**
       * used by FormWrapper to map state properties to the respective functions
       * @private
       */
      setAttribute: function (attribute, value) {
        var me = this;
        switch (attribute) {
          case "hidden":
            value ? me.hide() : me.show();
            break;
          default:
            break;
        }
      }
    });

    /**
     * Represents a form-template's field.
     *
     * @author ESt, ELO Digital Office GmbH
     *
     * Fields are automatically linked to their label, if they are called
     *
     *     "LBL_" + me.prefix + me.getShortName();
     *
     * To make things easier, localizedKwls will automatically be linked to
     * a selector:
     *
     *     me.fName + "_UNIT"
     *
     * e.g.
     *
     *     IX_MAP_HR_PERSONNEL_DEPARTMENT
     *     label: LBL_HR_PERSONNEL_DEPARTMENT
     *     selector: IX_MAP_HR_PERSONNEL_DEPARTMENT_UNIT
     *
     * This is important for the ´smart´ "set" and "value" functions to work!
     *
     * Setting a localizedKwl Value the old way:
     *
     *     //insert complicated, localizedKwl specific code here
     *
     * Setting a localizedKwl Value the new way:
     *
     *     form.fields.IX_MAP_HR_PERSONNEL_DEPARTMENT.set("DEV")
     *
     * The same also works for dates and of course "normal fields".
     *
     * field.show() and field.hide() take two parameters:
     * includingLabel     Boolean
     * includingSelector  Boolean
     * If both parameters are set to true, the field, its label and its unit-selector will be hidden/shown.
     */
    sol.define("sol.common.forms.FormWrapper.Field", {
      initialize: function (config) {
        var me = this,
            parent, selector, unitSelectorName;

        me.$super("sol.Base", "initialize", [config]);
        me.name = me.fName;
        if (!me.name) {
          return;
        }

        parent = $var(me.fName);
        parent = parent && parent.parentElement;

        me.isDate = (parent && parent.getAttribute("elonodename") == "DATE") || (me.element() && me.element().getAttribute("eloverify") == "date");
        me.datePicker = me.isDate && me.element().parentElement.children[1];

        me.editor = me.isEditor()
          && sol.create("sol.common.forms.FormWrapper.RedactorApi", {
            field: me,
            immediately: me.form.editorOptions.initImmediately,
            redactorOptions: me.form.editorOptions.redactorOptions
          });

        unitSelectorName = me.prefix + "_" + me.shortName.toUpperCase() + "_UNIT";

        me.label = $var("LBL_" + me.prefix + "_" + me.shortName.toUpperCase());

        if (me.isDate) {
          selector = $var(me.fName); // yep
        } else {
          selector = $var("IX_MAP_" + unitSelectorName) || $var("IX_GRP_" + unitSelectorName);
        }
        me.selector = selector;
        me.localizedDynKwlName = me.getDynKwlName();
        me.localizedDynKwl = me.localizedDynKwlName && me.element().nextSibling;
      },

      isEditor: function () {
        var me = this;
        return !!(me.element() && me.element().closest(".redactor3"));
      },

      /**
       * determines the dynamic keyword list's name, if the field is linked to one and returns it
       * @returns {String}
       */
      getDynKwlName: function () {
        var me = this,
            kwl = me.element() && me.element().nextSibling,
            name, result, elocompl;
        if (!me.element()) {
          return;
        }
        name = (me.element().getAttribute("swlname") || (kwl && kwl.getAttribute("swlname")));

        elocompl = (me.element().getAttribute("elocompl") || (kwl && kwl.getAttribute("elocompl")));

        me.localizedDynKwlOrigName = name || elocompl;

        name = (name === "#DATE#" ? undefined : name);

        result = name && ((name.indexOf("DYNSWL_") == 0 && name.replace("DYNSWL_", "")) || name) || undefined;
        me.isLocalized = result && (!elocompl); //localized kwls do not have elocompl attribute
        return result;
      },

      /**
       * removes selector from a kwl-field (e.g. for making it readonly)
       * @private
       */
      kwlStyle: function (mode) {
        var me = this;
        mode === "remove"
          ? me.localizedDynKwlName && me.element().removeAttribute("elocompl")
          : me.localizedDynKwlName && me.element().setAttribute("elocompl", me.localizedDynKwlOrigName || me.localizedDynKwlName);
      },

      /**
       * wrapper around $var, can also work with images (which $var can't)
       * @returns {HTML-DIV}
       */
      element: function () {
        var el = $var(this.fName);
        if (!el) {
          el = document.getElementsByName(this.fName);
          el = el.length > 0 && el[0]; // var does not seem to find images, therefore try this
        }
        return el;
      },

      valueFromEloParams: function () {
        var me = this;
        return (me.form.undefinedFields.indexOf(me.fName) === -1)
          ? "" // only lookup in ELO_PARAMS if field was never on form
          : (ELO_PARAMS[me.fName] === "undefined" ? "" : ELO_PARAMS[me.fName]);
      },

      /**
       * determines the field's value.
       *
       * returns an isoDate if the field is a date.
       * returns its key, if the field is a localizedKwl.
       *  @returns {String}
       */
      value: function (opts) {
        var me = this, value;

        value = (
          (me.isDate && sol.common.forms.Utils.getIsoDate(me.fName, {
            startOfDay: true
          }))
          || (me.localizedDynKwlName && me.getSelectedLocalizedDynKwlValue(opts))
          || ($var(me.fName) ? $val(me.fName) : ($val(me.fName) || me.valueFromEloParams()))
          || ""
        );

        return (opts && opts.asNumber)
          ? toNum(value) // ELO WF global function
          : value;
      },

      /**
       * returns current tooltip as a String
       */
      tooltip: function () {
        var me = this;
        return (me.element() && me.element().getAttribute("savedtitle")) || "";
      },

      /**
       * writes tip as the new tooltip
       */
      writeTooltip: function (tip) {
        var me = this, el = me.element();
        if (!el) {
          return;
        }
        if (typeof tip === "string" && tip) {
          if (el.getAttribute("savedtitle")) {
            el.setAttribute("savedtitle", tip);
          } else {
            el.setAttribute("title", tip);
            ELOF.assignTooltips(el.parentElement);
          }
        } else {
          el.getAttribute("savedtitle")
            ? el.onmouseover = undefined
            : console.log(me.fName + ": No tooltip created for value '" + tip + "' (type:" + typeof tip + ")");
        }
      },

      /**
       * Write a placeholder to the current field element
       */
      setPlaceholder: function (placeholderValue) {
        var me = this, el = me.element(), localizedValue;

        if (!el) {
          return;
        }

        localizedValue = placeholderValue;
        if (typeof localizedValue === "object") {
          try {
            localizedValue = elo.helpers.Text.getText(placeholderValue.key);
          } catch (e) {
            /** In case of an error, the placeholder should reset to avoid inconsistent state */
            console.warn("could not translate  " + placeholderValue.key, e);
            localizedValue = "";
          }
        }

        el.setAttribute("placeholder", localizedValue);
      },

      /**
       * @private please use value() for the same effect
       */
      getSelectedLocalizedDynKwlValue: function (opts) {
        var me = this;
        if (opts) {
          if (opts.full) {
            return $val(me.fName) || "";
          }
          if (opts.localizedStringOnly) {
            return me.getSelectedLocalizedDynKwlString() || "";
          }
        }
        return me.getSelectedLocalizedDynKwlKey() || "";
      },

      /**
       * @private please use getSelectedLocalizedDynKwlValue() without a parameter for the same effect
       */
      getSelectedLocalizedDynKwlKey: function (key) {
        var me = this,
            val = key || $val(me.name) || "";
        if (val.search(/\w+ - ?.*/g) === -1) {
          return val;
        } else {
          return val.slice(0, val.indexOf(" -"));
        }
      },

      /**
       * @private please use getSelectedLocalizedDynKwlValue() with { localizedStringOnly:true } for the same effect
       */
      getSelectedLocalizedDynKwlString: function () {
        var me = this,
            val = $val(me.name);
        if (val.search(/\w+ - ?.*/g) === -1) {
          return val;
        } else {
          return val.slice(val.indexOf(" -") + 3);
        }
      },

      /**
       * set a field's value to `value`.
       *
       * pass an isoDate if the field is a date.
       *
       * pass a key, if it's a localizedKwl
       *
       * @param {String || Integer(isoDate)} value
       */
      set: function (value) {
        var me = this, children;
        (me.isDate && value === "" && ($update(me.fName, value) || true))
          || (me.isDate && (sol.common.forms.Utils.setIsoDate(me.fName, value) || true))
          || (!me.localizedDynKwl && me.localizedDynKwlName && (me.selectDynKwlEntry(value) || true))
          || (me.localizedDynKwlName && (me.setLocalizedDynKwlKey(value) || true))
          || (me.editor && (me.editor.set(value) || true))
          || $update(me.fName, value);

        children = me.element().childNodes;
        value && me.viewSource && children.length === 1 && children[0].tagName === "ACRONYM" && me.element().replaceChild(document.createTextNode(value), children[0]);
      },

      selectDynKwlEntry: function (value) {
        var me = this, obj = {};
        obj[me.fName] = value;
        (typeof scatterDynamicField === "function" ? scatterDynamicField : _scatterDynamicField)(obj, me.fName); //wf function
      },

      convertToDiv: function (url) {
        function replace(a, b) {
          a.parentElement.appendChild(b).parentElement.removeChild(a);
        }
        var img = this.element(), div = img.tagName === "DIV" ? img : document.createElement("div");
        div.setAttribute("name", img.getAttribute("name"));
        div.style = "background-image:url(\'" + url + "\');";
        div.value || (div.value = "");
        (img.tagName !== "DIV") && replace(img.parentElement, div);
      },

      /**
       * sets an Image divs Image to the image having the passed guid. If no guid is passed, and the field's value contains a guid, it will be used instead
       * @param {String} guid
       */
      setImage: function (guid, elementType) {
        var me = this, url, el = me.element();
        guid = guid || me.value();
        !guid && me.hide();
        if (me.form.wfMeetsVersionRequirement("10.02.000") && me.form.wfName) {
          url = "/" + me.form.wfName + "/apps/rest/api/download/" + guid + "?ticket=" + ELO_PARAMS.ELO_TICKET;
          if (elementType === "div") {
            me.convertToDiv(url);
          } else {
            el.src = url;
          }
        } else {
          guid && sol.common.forms.Utils.initializeIxSession(function () {
            elo.IX.ix().checkoutDoc(guid, null, elo.CONST.EDIT_INFO.mbDocument, elo.CONST.LOCK.NO, new de.elo.ix.client.AsyncCallback(
              function (doc) {
                url = doc.document.docs[0].url;
                if (elementType === "div") {
                  me.convertToDiv(url);
                  (url === "./images/") ? me.hide() : me.show();
                } else {
                  el.src = url;
                  (url === "./images/") ? me.hide() : me.show();
                }
              },
              function () {
                null;
              }));
          });
        }
      },

      /**
       * Sets an image by an URL
       * @param {String} url URL
       */
      setImageUrl: function (url, elementType) {
        var me = this, el = me.element();
        if (url) {
          if (elementType === "div") {
            me.convertToDiv(url);
            me.show();
            el.style = "background-image:url('" + url + "');";
          } else {
            me.show();
            el.src = url;
          }
        }
      },

      /**
       * @private please use set() for the same effect
       * @param {String} key
       */
      setLocalizedDynKwlKey: function (key) {
        var me = this;
        if (key) {
          $update(me.fName, me.getSelectedLocalizedDynKwlKey(key));
          $listDyn(me.localizedDynKwlName, me.fName, undefined, function (data) {
            var result = data.table.find(function (entry) {
              return entry[0] === key;
            });
            result && $update(me.fName, result[2]);
          });
        } else {
          $update(me.fName, "");
        }
      },
      /**
       * used by FormWrapper to map state properties to the respective functions
       * @private
       */
      setAttribute: function (attribute, value) {
        var me = this,
            element = me.element(), parentFun;

        if (!element) {
          // setAttribute on missing field element should not have any impact
          console.warn("setAttribute(" + attribute + ") on " + me.name + " skipped");
          return;
        }

        switch (attribute) {
          case "readonly":
            if (element.type === "checkbox") {
              element[value ? "setAttribute" : "removeAttribute"]("disabled", value);
            } else {
              parentFun = value && element.parentElement.setAttribute.bind(element.parentElement) ||
              element.parentElement.removeAttribute.bind(element.parentElement);
              element.readOnly = value;
              parentFun("isreadonly", value);
              $setReadOnly(me.fName, value);
              me.kwlStyle(value && "remove");
            }
            break;
          case "optional":
            value ? me.makeOptional() : me.makeMandatory();
            break;
          case "hidden":
            value ? me.hide(true, true) : me.show(true, true);
            break;
          default:
            break;
        }
      },

      /**
       * @private please use setAttribute("optional", true)
       */
      makeOptional: function () {
        var me = this;
        me.modifyEloVerify(me.element(), "remove");
        me.label && me.label.parentElement && me.label.parentElement.classList.remove("required");
      },

      /**
       * used by assignValidationFunctionToWindow
       * @private
       */
      modifyEloVerify: function (element, action) {
        var me = this, eloVerify = element.getAttribute("eloverify"),
            match;
        if (eloVerify || (eloVerify.trim() === "")) {
          switch (action) {
            case "remove":
              element.setAttribute("eloverify", me.removeKeyFromEloVerify(eloVerify));
              break;
            case "add":
              element.setAttribute("eloverify", me.addKeyToEloVerify(eloVerify));
              break;
            default:
              break;
          }
        }
      },
      removeKeyFromEloVerify: function (eloVerify) {
        var match = /(.* ?)notemptyforward( ?.*)/g.exec(eloVerify);
        if (match) {
          eloVerify = match[1] + match[2];
        }
        return eloVerify.trim();
      },

      /**
       * @private please use setAttribute("optional", false)
       */
      makeMandatory: function () {
        var me = this;
        me.modifyEloVerify(me.element(), "add");
        me.label && me.label.parentElement && me.label.parentElement.classList.add("required");
      },
      addKeyToEloVerify: function (eloVerify) {
        var match = /(.* ?)notemptyforward( ?.*)/g.exec(eloVerify);
        if (!match) {
          eloVerify += " notemptyforward";
        }
        return eloVerify.trim();
      },

      /**
       * can be used to set an HTML Attribute
       * @param {String} attribute  name of the style attribute
       * @param {String} value  value for the attribute
       * @param {Boolean} includingLabel also set attribute for field's label
       * @param {Boolean} includingSelector also set attribute for field's selector
       */
      setStyleAttribute: function (attribute, value, includingLabel, includingSelector) {
        var me = this,
            field = me.element(),
            label = me.label,
            selector = me.selector,
            selectorButton = selector && selector.parentElement.children[1];
        field.style[attribute] = value;
        if (includingLabel && label) {
          label.style[attribute] = value;
        }
        if (includingSelector && selector) {
          selector.style[attribute] = value;
          selectorButton.style[attribute] = value;
        }
      },

      /**
       * applies a manipulator function to the field
       * @private
       */
      applyStandardManipulator: function (manipulator, includingLabel, includingSelector) {
        var me = this,
            field = me.fName,
            label = me.label && me.label.id,
            selector = me.selector && me.selector.name;
        manipulator(field);

        includingLabel && manipulator(label);
        includingSelector && manipulator(selector);
      },

      /**
       * @private please use show() or hide() for the same effect
       */
      changeVisibility: function (visible, includingLabel, includingSelector) {
        var me = this;
        // 9.3
        if (visible) {
          if (me.localizedDynKwl) {
            me.localizedDynKwl.style.visibility = "visible";
          }
          if (me.datePicker) {
            me.datePicker.children[0].classList.add("calbutton");
          }
        } else {
          if (me.localizedDynKwl) {
            me.localizedDynKwl.style.visibility = "visible";
          }
          if (me.datePicker) {
            me.datePicker.children[0].classList.remove("calbutton");
          }
        }
        // 9.3 end
        me.applyStandardManipulator(visible ? $show.bind(me) : $hide.bind(me), includingLabel, includingSelector);
      },

      /**
       * unhides/shows the field on the form
       * @param {Boolean} includingLabel also unhide/show the field's label
       * @param {Boolean} includingSelector also unhide/show the field's selector
       */
      show: function (includingLabel, includingSelector) {
        this.changeVisibility(true, includingLabel, includingSelector);
      },

      /**
       * hides the field on the form
       * @param {Boolean} includingLabel also hides the field's label
       * @param {Boolean} includingSelector also hides the field's selector
       */
      hide: function (includingLabel, includingSelector) {
        this.changeVisibility(false, includingLabel, includingSelector);
      }
    });

    sol.define("sol.common.forms.FormWrapper.RedactorApi", {

      requiredConfig: ["field"],

      initialize: function (config) {
        var me = this;
        me.$super("sol.Base", "initialize", [config]);
        console.log("init redactor field", me.field);
        if (me.immediately) {
          me.initRedactor(me.redactorOptions);
        }
      },

      /**
       *
       */
      initRedactor: function (options) {
        var me = this,
            el = me.field.element();
        $R(el, options || {});
      },

      /**
       *
       */
      set: function (content) {
        var me = this,
            el = me.field.element();
        el && $R(el, "source.setCode", content);
      },

      /**
       *
       */
      get: function (content) {
        var me = this,
            el = me.field.element();
        return (el && $R(el, "source.getCode"));
      }

    });

    /**
     * FormWrapper Templates
     *
     * @author ESt, ELO Digital Office GmbH
     *
     * @elowf
     *
     * The FormWrapper provides a simple templating mechanism.
     * To use a template, define a field in any state and set its template.name property
     * to the name of the template:
     *
     *     IX_MAP_HR_CASSATIONTOGGLE: {
     *       template: { name: "toggle", config: { ... } }
     *     }
     *
     * A template is simply a function which creates a state-object using the config passed via template.config
     * and returns the created state-object in the end.
     *
     * Simplest template:
     *
     *     myReadOnlyTemplate: function (config) {
     *       var configuredTemplate;
     *
     *       configuredTemplate = {
     *         states: {
     *           myDefaultState: {
     *             fieldProperties: {},
     *             tabProperties: {}
     *           }
     *         }
     *       }
     *
     *       configuredTemplate.states.myDefaultState.fieldProperties[config.name] = {readonly: config.value};
     *       return configuredTemplate;
     *     }
     *
     * This template `myReadOnlyTemplate` would do only one thing: set a field readonly when the defaultState is set.
     *
     * If you define state properties in a template, they will overwrite existing states, be careful. Never overwrite the stateInit state itself, but only a field's properties in its `fieldProperties`!
     *
     * You can define a template like the one above in the OnInit function, if you define it in the extending class.
     *
     *     OnInit: function {
     *       var me = this;
     *       me.templates.myReadOnlyTemplate = me.myReadOnlyTemplate;
     *     }
     *
     * Now you can use the template in stateInit:
     *
     *     IX_GRP_HR_PERSONNEL_FIRSTNAME: {
     *       template: { name: "myReadOnlyTemplate", config: { name: "IX_GRP_HR_PERSONNEL_FIRSTNAME", value: true } };
     *     }
     *
     * Using the template in stateInit initializes the template. The template then adds the desired attributes to the myDefaultState state.
     * You usually don't use templates in other states than stateInit.
     *
     * Of course, more complex templates make more sense, since a fieldProperty can only be assigned one single template.
     *
     * You can not use a template and properties (e.g. "hidden", "readonly", ...) in a fieldProperty at the same time!
     *
     * Hint:
     *
     *     Define a template in OnInit. Initialize it in stateInit. The template then takes in effect when the specific state is triggered (in this case myDefaultState)
     */
    sol.define("sol.common.forms.FormWrapper.Templates", {
      /**
       * Toggle Template.
       *
       * ### Example config
       * You have a form containing a toggle. Depending on which option is toggled, you want to hide, or respectively unhide fields.
       *
       * A toggle always has an "on-state", an "off-state" and an internal "nothing toggled" state
       *
       *     {
       *        toggleOptionFieldName: "IX_MAP_HR_CASSATIONTOGGLE",
       *        onValue: "sol.hr.form.personnelfiledocument.cassationactive",
       *        offValue: "sol.hr.form.personnelfiledocument.nocassation",
       *        onState: {
       *          fieldProperties: {
       *            IX_MAP_HR_PERSONNEL_CASSATIONPERIOD: {
       *              hidden: false
       *            },
       *            IX_MAP_HR_PERSONNEL_CASSATIONPERIOD_UNIT: {
       *              hidden: false
       *            },
       *            IX_GRP_HR_PERSONNEL_CASSATIONDATE: {
       *              hidden: false
       *            }
       *          }
       *        },
       *        offState: {
       *          fieldProperties: {
       *            IX_MAP_HR_PERSONNEL_CASSATIONPERIOD: {
       *              hidden: true
       *            },
       *            IX_MAP_HR_PERSONNEL_CASSATIONPERIOD_UNIT: {
       *              hidden: true
       *            },
       *            IX_GRP_HR_PERSONNEL_CASSATIONDATE: {
       *              hidden: true,
       *              value: ""
       *            }
       *          }
       *        }
       *      }
       */
      toggle: function (config) {
        var configuredTemplate,
            triggerLogic = function (toggledOption) {
              var me = this;
              if (toggledOption == config.onValue) {
                me.setState("toggle_" + config.toggleOptionFieldName + "_anyOptionSelected");
                me.setState("toggle_" + config.toggleOptionFieldName + "_OnOptionSelected");
              } else if (toggledOption == config.offValue) {
                me.setState("toggle_" + config.toggleOptionFieldName + "_anyOptionSelected");
                me.setState("toggle_" + config.toggleOptionFieldName + "_OffOptionSelected");
              }
            };

        configuredTemplate = {
          states: {
            stateInit: {
              fieldProperties: {},
              tabProperties: {}
            }
          },
          OnInitAndTabChange: function () {
            var me = this,
                toggledOption = me.fields[config.toggleOptionFieldName].value();

            if (!config.tabs || config.tabs.length === 0 || config.tabs.indexOf(me.tabs.activeTab.name) > -1) {
              toggledOption == "" ?
                me.setState("toggle_" + config.toggleOptionFieldName + "_defaultState") :
                triggerLogic.call(me, toggledOption);
            }
          }
        };

        configuredTemplate.states.stateInit.fieldProperties[config.toggleOptionFieldName] = {
          responder: function (form, state, field, value) {
            triggerLogic.call(form, value);
          }
        };

        configuredTemplate.states["toggle_" + config.toggleOptionFieldName + "_defaultState"] = config.defaultState;
        configuredTemplate.states["toggle_" + config.toggleOptionFieldName + "_OnOptionSelected"] = config.onState;
        configuredTemplate.states["toggle_" + config.toggleOptionFieldName + "_OffOptionSelected"] = config.offState;
        configuredTemplate.states["toggle_" + config.toggleOptionFieldName + "_anyOptionSelected"] = config.anyState;

        return configuredTemplate;
      },

      /**
       * Filechooser Variants (drag&drop, filechooser, webcam picture capturing).
       * This template has various dependencies (jar-file, libs, ...)
       *
       * In some forms, you'd maybe like to let the user select a picture. And save it for later use.
       * If so, please use the 113_capturepic_webcam "HR"-Form as an inspiration.
       *
       * As soon as you defined your own form having all required fields, you can use the following template
       * ### Example config
       *
       *     {
       *       name: "personnelphotopicker",
       *       webcamName: "personnelphotocam",
       *       webcamConfig: {
       *         javaStartupButton: "WEBCAM_JAVA",
       *         varNameBtnReset: "JS_WEBCAM_RESET",
       *         varNameBtnSnap: "JS_WEBCAM_SNAP",
       *         varNameContainer: "WEBCAM_INIT",
       *         width: 540,
       *         height: 390,
       *         dest_width: 720,
       *         dest_height: 520,
       *         crop_width: 400,
       *         crop_height: 520,
       *         image_format: "jpeg",
       *         jpeg_quality: 90,
       *         swfURL: "lib_webcam.swf",
       *         fps: 45,
       *         showIfNoCam: true
       *       },
       *       dropZoneId: "dropZone",
       *       filePickerId: "filePicker",
       *       accept: "image/jpeg, image/jpg, image/png",
       *       maxSize: "3", //Megabyte (float values possible)
       *       maskNameForRule: "Personnel file",
       *       solTypeForRule: "PERSONNELFILE",
       *       photoReferenceField: "HR_PERSONNEL_PHOTO_GUID",
       *       photoReferenceFieldObjId: "HR_PERSONNEL_PHOTO_OBJID",
       *       clearPreviewField: "JS_PICTURE_CLEAR",
       *       filePickerField: "JS_FILEPICKER",
       *       photoConfig: {
       *         maskName: "Personnel file document",
       *         pictureName: "Mitarbeiterfoto"
       *       }
       *     }
       */
      fileChooserVariants: function (config) {
        var configuredTemplate;

        configuredTemplate = {
          states: {
            stateInit: {
              fieldProperties: {},
              tabProperties: {}
            }
          },
          OnInitAndTabChange: function () {
            var me = this;
            me.fcv = sol.create("sol.common.forms.FileChooserVariants", config);
          },
          OnSaveRule: {
            name: "FileDragDropAndDialog" + config.name,
            rule: {
              maskName: config.maskNameForRule,
              solType: config.solTypeForRule,
              saveValues: function () {
                var me = this;
                me.fcv.uploadFile();
                return true;
              }
            }
          }
        };

        return configuredTemplate;
      },

      /**
       * Template for a date which will receive its value from a unit field and its selector.
       *
       * Validation messages will be displayed, if the user tries to change a field which would falsify the calculation
       *
       * e.g.
       * Start date "20170101"
       *
       * unit "4"
       *
       * unitselector value "days"
       *
       * target date = 20170105
       *
       * Please always name your selector unit field "FIELDNAME_UNIT"
       * Other names are not supported yet.
       *
       * ### Example config
       *
       *     {
       *       startDateFnOrValue: "IX_GRP_HR_PERSONNEL_DATEOFJOINING",
       *       unitValueFieldName: "IX_MAP_HR_PERSONNEL_PROBATIONARYPERIODDURATION",
       *       unitSelectorFieldName: "IX_MAP_HR_PERSONNEL_PROBATIONARYPERIODDURATION_UNIT",
       *       targetDateFieldName: "IX_MAP_HR_PERSONNEL_ENDOFPROBATIONARY",
       *       validationMessage: "Da im Feld `Ende der Probezeit` ein von der Berechnung abweichender Wert eingegeben wurde, ist dieses Feld jetzt gesperrt. Leeren Sie das Feld, wenn Sie die Berechnungsfunktion verwenden möchten!"
       *     }
       *
       */
      dateFromUnitSelectorRestrictive: function (config) {
        var configuredTemplate;
        configuredTemplate = {
          states: {
            stateInit: {
              fieldProperties: {},
              tabProperties: {}
            }
          }
        };

        configuredTemplate.states["dfus_" + config.targetDateFieldName + "_HasNoValue"] = {
          fieldProperties: {}
        };
        configuredTemplate.states["dfus_" + config.targetDateFieldName + "_HasNoValue"].fieldProperties[config.unitValueFieldName] = {
          readonly: false,
          validator: false
        };
        configuredTemplate.states["dfus_" + config.targetDateFieldName + "_HasNoValue"].fieldProperties[config.unitSelectorFieldName] = {
          readonly: false,
          validator: false
        };

        configuredTemplate.states["dfus_" + config.targetDateFieldName + "_HasCustomValue"] = {
          fieldProperties: {}
        };
        configuredTemplate.states["dfus_" + config.targetDateFieldName + "_HasCustomValue"].fieldProperties[config.unitValueFieldName] = {
          readonly: true,
          validator: function () {
            return config.validationMessage;
          }
        };
        configuredTemplate.states["dfus_" + config.targetDateFieldName + "_HasCustomValue"].fieldProperties[config.unitSelectorFieldName] = {
          readonly: true,
          validator: function () {
            return config.validationMessage;
          }
        };

        configuredTemplate.states.stateInit.fieldProperties[config.targetDateFieldName] = {
          responder: function (form, state, field, value) {
            if (value === "") {
              form.setState("dfus_" + config.targetDateFieldName + "_HasNoValue");
            } else if (value !== form.returnCalculatedDate(config.startDateFnOrValue, config.unitValueFieldName, config.targetDateFieldName, config.offsetNumber, config.offsetUnit)) {
              form.setState("dfus_" + config.targetDateFieldName + "_HasCustomValue");
            }
          }
        };
        configuredTemplate.states.stateInit.fieldProperties[config.unitValueFieldName] = {
          responder: function (form, state, field, value) {
            if (field.value() && form.fields[config.unitSelectorFieldName].value()) {
              form.setCalculatedDate(config.startDateFnOrValue, config.unitValueFieldName, config.targetDateFieldName, config.offsetNumber, config.offsetUnit);
            }
          }
        };

        configuredTemplate.states.stateInit.fieldProperties[config.unitSelectorFieldName] = {
          responder: function (form, state, field, value) {
            if (field.value() && form.fields[config.unitSelectorFieldName].value()) {
              form.setCalculatedDate(config.startDateFnOrValue, config.unitValueFieldName, config.targetDateFieldName, config.offsetNumber, config.offsetUnit);
            }
          }
        };

        return configuredTemplate;
      },

      /**
      * Template for a date which will receive its value from a unit field, its duration-selector and an optional "termination-Point" selector.
      *
      * Please always name your selector unit field "FIELDNAME_UNIT"
      * Other names are not supported yet.
      *
      * Also, it is recommended, to name your termination point field "FIELDNAME_TP"
      *
      * ### Example config
      *
      *     IX_MAP_HR_PERSONNEL_NEXTPOSSIBLEDISMISSAL: {
      *       template: {
      *         name: "dateFromUnitSelector",
      *         config: {
      *           recalculate: true,
      *           startDateFnOrValue: undefined,  // == today
      *           unitValueFieldName: "IX_MAP_HR_PERSONNEL_PERIODOFNOTICE",
      *           unitSelectorFieldName: "IX_MAP_HR_PERSONNEL_PERIODOFNOTICE_UNIT",
      *           terminationPointFieldName: "IX_MAP_HR_PERSONNEL_PERIODOFNOTICE_TP",
      *           targetDateFieldName: "IX_MAP_HR_PERSONNEL_NEXTPOSSIBLEDISMISSAL",
      *         }
      *       }
      *     }
      *
      */
      dateFromUnitSelector: function (config) {
        var configuredTemplate;
        configuredTemplate = {
          states: {
            stateInit: {
              fieldProperties: {},
              tabProperties: {}
            }
          }
        };

        configuredTemplate.states["dfus_" + config.targetDateFieldName + "_HasCustomValue"] = {
          fieldProperties: {}
        };
        configuredTemplate.states["dfus_" + config.targetDateFieldName + "_HasCustomValue"].fieldProperties[config.unitValueFieldName] = {
          value: "",
          overwrite: true
        };
        configuredTemplate.states["dfus_" + config.targetDateFieldName + "_HasCustomValue"].fieldProperties[config.unitSelectorFieldName] = {
          value: "",
          overwrite: true
        };

        configuredTemplate.states.stateInit.fieldProperties[config.targetDateFieldName] = {
          responder: function (form, state, field, value) {
            if (value !== form.returnCalculatedDate(config.startDateFnOrValue, config.unitValueFieldName, config.terminationPointFieldName || "", config.targetDateFieldName, config.offsetNumber, config.offsetUnit)) {
              form.setState("dfus_" + config.targetDateFieldName + "_HasCustomValue");
            }
          }
        };

        if (config.terminationPointFieldName) {
          configuredTemplate.states.stateInit.fieldProperties[config.terminationPointFieldName] = {
            responder: function (form, state, field, value) {
              if (field.value() && form.fields[config.terminationPointFieldName].value()) {
                form.setCalculatedDate(config.startDateFnOrValue, config.unitValueFieldName, config.terminationPointFieldName || "", config.targetDateFieldName, config.offsetNumber, config.offsetUnit);
              }
            }
          };
        }

        configuredTemplate.states.stateInit.fieldProperties[config.unitValueFieldName] = {
          responder: function (form, state, field, value) {
            if (field.value() && form.fields[config.unitSelectorFieldName].value()) {
              form.setCalculatedDate(config.startDateFnOrValue, config.unitValueFieldName, config.terminationPointFieldName || "", config.targetDateFieldName, config.offsetNumber, config.offsetUnit);
            }
          }
        };

        configuredTemplate.states.stateInit.fieldProperties[config.unitSelectorFieldName] = {
          responder: function (form, state, field, value) {
            if (field.value() && form.fields[config.unitSelectorFieldName].value()) {
              form.setCalculatedDate(config.startDateFnOrValue, config.unitValueFieldName, config.terminationPointFieldName || "", config.targetDateFieldName, config.offsetNumber, config.offsetUnit);
            }
          }
        };

        if (config.recalculate) {
          configuredTemplate.OnInitAndTabChange = function () {
            var me = this,
                unitValueField = me.fields[config.unitValueFieldName];
            if (unitValueField && unitValueField.value()) {
              me.setCalculatedDate(config.startDateFnOrValue, config.unitValueFieldName, config.terminationPointFieldName || "", config.targetDateFieldName, config.offsetNumber, config.offsetUnit);
            }
          };
        }

        return configuredTemplate;
      }

    });