text: /**
   * System signature module
   */
  
  define([
      'jquery',
      'app/init',
      'app/util',
      'bootbox',
      'app/counter',
      'app/map/map',
      'app/map/util',
      'app/ui/form_element'
  ], ($, Init, Util, bootbox, Counter, Map, MapUtil, FormElement) => {
      'use strict';
  
      let config = {
          // module info
          modulePosition: 4,
          moduleName: 'systemSignature',
          moduleHeadClass: 'pf-module-head',                                      // class for module header
          moduleHandlerClass: 'pf-module-handler-drag',                           // class for "drag" handler
  
          // system signature module
          moduleTypeClass: 'pf-system-signature-module',                          // class for this module
  
          // headline toolbar
          moduleHeadlineIconClass: 'pf-module-icon-button',                       // class for toolbar icons in the head
          moduleHeadlineIconAddClass: 'pf-module-icon-button-add',                // class for "add signature" icon
          moduleHeadlineIconReaderClass: 'pf-module-icon-button-reader',          // class for "signature reader" icon
          moduleHeadlineIconLazyClass: 'pf-module-icon-button-lazy',              // class for "lazy delete" toggle icon
          moduleHeadlineProgressBarClass: 'pf-system-progress-scanned',           // class for signature progress bar
  
          // tables
          tableToolsActionClass: 'pf-table-tools-action',                         // class for "new signature" table (hidden)
  
          // table toolbar
          sigTableClearButtonClass: 'pf-sig-table-clear-button',                  // class for "clear" signatures button
  
          // signature table
          sigTableId: 'pf-sig-table-',                                            // Table id prefix
          sigTableClass: 'pf-sig-table',                                          // Table class for all Signature Tables
          sigTablePrimaryClass: 'pf-sig-table-primary',                           // class for primary sig table
          sigTableSecondaryClass: 'pf-sig-table-secondary',                       // class for secondary sig table
          sigTableRowIdPrefix: 'pf-sig-row_',                                     // id prefix for table rows
  
          sigTableEditSigNameInput: 'pf-sig-table-edit-name-input',               // class for editable fields (sig name)
  
          tableCellConnectionClass: 'pf-table-connection-cell',                   // class for "connection" cells
          tableCellFocusClass: 'pf-table-focus-cell',                             // class for "tab-able" cells. enable focus()
          tableCellCounterClass: 'pf-table-counter-cell',                         // class for "counter" cells
          tableCellActionClass: 'pf-table-action-cell',                           // class for "action" cells
  
          // xEditable
          editableNameInputClass: 'pf-editable-name',                             // class for "name" input
          editableDescriptionInputClass: 'pf-editable-description',               // class for "description" textarea
          editableUnknownInputClass: 'pf-editable-unknown',                       // class for input fields (e.g. checkboxes) with "unknown" status
  
          signatureGroupsLabels: Util.getSignatureGroupOptions('label'),
          signatureGroupsNames: Util.getSignatureGroupOptions('name')
      };
  
      let lockedTables = {};                                                      // locked tables (e.g. disable cops&paste, disable table update)
  
      let sigNameCache = {};                                                      // cache signature names
  
      let validSignatureNames = [                                                 // allowed signature type/names
          'Cosmic Anomaly',
          'Cosmic Signature',
          'Kosmische Anomalie',
          'Kosmische Signatur',
          'Anomalie cosmique',
          'Signature cosmique',
          'Космическая аномалия',                                                 // == "Cosmic Anomaly"
          'Источники сигналов'                                                    // == "Cosmic Signature"
      ];
  
      let emptySignatureData = {
          id: 0,
          name: '',
          groupId: 0,
          typeId: 0
      };
  
      let editableDefaults = {                                                    // xEditable default options for signature fields
          url: Init.path.saveSignatureData,
          dataType: 'json',
          container: 'body',
          highlight: false,   // i use a custom implementation. xEditable uses inline styles for bg color animation -> does not work properly on datatables "sort" cols
          error: function(jqXHR, newValue){
              let reason = '';
              let status = 'Error';
              if(jqXHR.statusText){
                  reason = jqXHR.statusText;
              }else if(jqXHR.name){
                  // validation error new sig (new row data save function)
                  reason = jqXHR.name;
                  // re-open "name" fields (its a collection of fields but we need "id" field)
                  jqXHR.name.field.$element.editable('show');
              }else{
                  reason = jqXHR.responseJSON.text;
                  status = jqXHR.status;
              }
  
              Util.showNotify({title: status + ': save signature', text: reason, type: 'error'});
              $(document).setProgramStatus('problem');
              return reason;
          }
      };
  
      /**
       * get custom "metaData" from dataTables API
       * @param tableApi
       * @returns {*}
       */
      let getTableMetaData = tableApi => {
          let data = null;
          if(tableApi){
              data = tableApi.init().pfMeta;
          }
          return data;
      };
  
      /**
       * lock signature tableApi and lockType
       * @param tableApi
       * @param lockType
       */
      let lockTable = (tableApi, lockType = 'update') => {
          let metaData = getTableMetaData(tableApi);
          if(metaData.systemId){
              if( !lockedTables.hasOwnProperty(metaData.systemId) ){
                  lockedTables[metaData.systemId] = {};
              }
              lockedTables[metaData.systemId][lockType] = true;
          }else{
              console.warn('metaData.systemId required in lockTable()', metaData.systemId);
          }
      };
  
      /**
       * check whether a signature tableApi is locked by lockType
       * @param tableApi
       * @param lockType
       * @returns {boolean}
       */
      let isLockedTable = (tableApi, lockType = 'update') => {
          let locked = false;
          if(tableApi){
              let metaData = getTableMetaData(tableApi);
              if(metaData.systemId){
                  if(
                      lockedTables.hasOwnProperty(metaData.systemId) &&
                      lockedTables[metaData.systemId].hasOwnProperty(lockType)
                  ){
                      locked = true;
                  }
              }else{
                  console.warn('metaData.systemId required in isLockedTable()', metaData.systemId);
              }
          }
  
          return locked;
      };
  
      /**
       * unlock signature tableApi and lockType
       * @param tableApi
       * @param lockType
       */
      let unlockTable = (tableApi, lockType = 'update') => {
          if(tableApi){
              let metaData = getTableMetaData(tableApi);
              if(isLockedTable(tableApi, lockType)){
                  delete lockedTables[metaData.systemId][lockType];
              }
              if(
                  lockedTables.hasOwnProperty(metaData.systemId) &&
                  !Object.getOwnPropertyNames(lockedTables[metaData.systemId]).length
              ){
                  delete lockedTables[metaData.systemId];
              }
          }
      };
  
      /**
       * get dataTable id
       * @param mapId
       * @param systemId
       * @param tableType
       * @returns {string}
       */
      let getTableId = (mapId, systemId, tableType) => Util.getTableId(config.sigTableId, mapId, systemId, tableType);
  
      /**
       * get a dataTableApi instance from global cache
       * @param mapId
       * @param systemId
       * @param tableType
       * @returns {*}
       */
      let getDataTableInstance = (mapId, systemId, tableType) => Util.getDataTableInstance(config.sigTableId, mapId, systemId, tableType);
  
      /**
       * Update/set tooltip for an element
       * @param element
       * @param title
       */
      let updateTooltip = (element, title) => {
          $(element).attr('data-container', 'body')
              .attr('title', title.toUpperCase())
              .tooltip('fixTitle').tooltip('setContent');
      };
  
      /**
       * sum up all options in nested (or not nested) object of objects
       * -> e.g.
       * {
       *     first: {
       *         count = [4, 2, 1]
       *         test = { ... }
       *     },
       *     second: {
       *         count = [12, 13]
       *         test = { ... }
       *     }
       * }
       * -> getOptionsCount('count', obj) => 5;
       * @param key
       * @param obj
       * @returns {number}
       */
      let getOptionsCount = (key, obj) => {
          let sum = 0;
          for(let entry of obj){
              if(entry.hasOwnProperty(key)){
                  sum += entry[key].length;
              }else{
                  sum++;
              }
          }
          return sum;
      };
  
      /**
       * get possible frig holes that could spawn in a system
       * filtered by "systemTypeId"
       * @param systemTypeId
       * @returns {{}}
       */
      let getFrigateHolesBySystem = systemTypeId => {
          let signatureNames = {};
          if(Init.frigateWormholes[systemTypeId]){
              signatureNames =  Init.frigateWormholes[systemTypeId];
          }
          return signatureNames;
      };
  
      /**
       * get all signature types that can exist for a given system
       * -> result is partially cached
       * @param systemData
       * @param systemTypeId
       * @param areaId
       * @param groupId
       * @returns {Array}
       */
      let getAllSignatureNames = (systemData, systemTypeId, areaId, groupId) => {
          systemTypeId    = parseInt(systemTypeId || 0);
          areaId          = parseInt(areaId || 0);
          groupId         = parseInt(groupId || 0);
          let newSelectOptions = [];
          let newSelectOptionsCount = 0;
  
          if(!systemTypeId || !areaId || !groupId){
              return newSelectOptions;
          }
  
          let cacheKey = [systemTypeId, areaId, groupId].join('_');
  
          // check for cached signature names
          if(sigNameCache.hasOwnProperty( cacheKey )){
              // cached signatures do not include static WHs!
              // -> ".slice(0)" creates copy
              newSelectOptions =  sigNameCache[cacheKey].slice(0);
              newSelectOptionsCount = getOptionsCount('children', newSelectOptions);
          }else{
              // get new Options ----------
              // get all possible "static" signature names by the selected groupId
              let tempSelectOptions = Util.getAllSignatureNames(systemTypeId, areaId, groupId);
  
              // format options into array with objects advantages: keep order, add more options (whs), use optgroup
              if(tempSelectOptions){
                  let fixSelectOptions = [];
                  for(let key in tempSelectOptions){
                      if(
                          key > 0 &&
                          tempSelectOptions.hasOwnProperty(key)
                      ){
                          newSelectOptionsCount++;
                          fixSelectOptions.push({value: newSelectOptionsCount, text: tempSelectOptions[key]});
                      }
                  }
  
                  if(newSelectOptionsCount > 0){
                      if(groupId === 5){
                          // "wormhole" selected => multiple <optgroup> available
                          newSelectOptions.push({ text: 'Wandering', children: fixSelectOptions});
                      }else{
                          newSelectOptions = fixSelectOptions;
                      }
                  }
              }
  
              // wormhole (cached signatures)
              if( groupId === 5 ){
  
                  // add possible frigate holes
                  let frigateHoles = getFrigateHolesBySystem(areaId);
                  let frigateWHData = [];
                  for(let frigKey in frigateHoles){
                      if(
                          frigKey > 0 &&
                          frigateHoles.hasOwnProperty(frigKey)
                      ){
                          newSelectOptionsCount++;
                          frigateWHData.push( {value: newSelectOptionsCount, text: frigateHoles[frigKey]} );
                      }
                  }
  
                  if(frigateWHData.length > 0){
                      newSelectOptions.push({ text: 'Frigate', children: frigateWHData});
                  }
  
                  // add possible incoming holes
                  let incomingWHData = [];
                  for(let incomingKey in Init.incomingWormholes){
                      if(
                          incomingKey > 0 &&
                          Init.incomingWormholes.hasOwnProperty(incomingKey)
                      ){
                          newSelectOptionsCount++;
                          incomingWHData.push( {value: newSelectOptionsCount, text: Init.incomingWormholes[incomingKey]} );
                      }
                  }
  
                  if(incomingWHData.length > 0){
                      newSelectOptions.push({ text: 'Incoming', children: incomingWHData});
                  }
              }else{
                  // groups without "children" (optgroup) should be sorted by "value"
                  // this is completely optional and not necessary!
                  newSelectOptions = newSelectOptions.sortBy('text');
              }
  
              // update cache (clone array) -> further manipulation to this array, should not be cached
              sigNameCache[cacheKey] = newSelectOptions.slice(0);
          }
  
          // static wormholes (DO NOT CACHE) (not all C2 WHs have the same statics,...
          if( groupId === 5 ){
              // add static WH(s) for this system
              if(systemData.statics){
                  let staticWHData = [];
                  for(let wormholeName of systemData.statics){
                      let wormholeData = Object.assign({}, Init.wormholes[wormholeName]);
                      let staticWHName = wormholeData.name + ' - ' + wormholeData.security;
  
                      newSelectOptionsCount++;
                      staticWHData.push( {value: newSelectOptionsCount, text: staticWHName} );
                  }
  
                  if(staticWHData.length > 0){
                      newSelectOptions.unshift({ text: 'Static', children: staticWHData});
                  }
              }
          }
  
          return newSelectOptions;
      };
  
      /**
       * get all signature types that can exist for a system (jQuery obj)
       * @param systemElement
       * @param groupId
       * @returns {Array}
       */
      let getAllSignatureNamesBySystem = (systemElement, groupId) => {
          let systemTypeId = systemElement.data('typeId');
          let areaId = Util.getAreaIdBySecurity(systemElement.data('security'));
          let systemData = {statics: systemElement.data('statics')};
          return getAllSignatureNames(systemData, systemTypeId, areaId, groupId);
      };
  
      /**
       * get all connection select options
       * @param mapId
       * @param systemData
       * @returns {Array}
       */
      let getSignatureConnectionOptions = (mapId, systemData) => {
          let map = Map.getMapInstance( mapId );
          let systemId = MapUtil.getSystemId(mapId, systemData.id);
          let systemConnections = MapUtil.searchConnectionsBySystems(map, [systemId], 'wh');
          let newSelectOptions = [];
          let connectionOptions = [];
  
          for(let systemConnection of systemConnections){
              let connectionData = MapUtil.getDataByConnection(systemConnection);
  
              // connectionId is required (must be stored)
              if(connectionData.id){
                  // check whether "source" or "target" system is relevant for this connection
                  // -> hint "source" === 'target' --> loop
                  if(systemData.id !== connectionData.target){
                      let targetSystemData = MapUtil.getSystemData(mapId, connectionData.target);
                      if(targetSystemData){
                          // take target...
                          connectionOptions.push({
                              value: connectionData.id,
                              text: connectionData.targetAlias + ' - ' + targetSystemData.security
                          });
                      }
                  }else if(systemData.id !== connectionData.source){
                      let sourceSystemData = MapUtil.getSystemData(mapId, connectionData.source);
                      if(sourceSystemData){
                          // take source...
                          connectionOptions.push({
                              value: connectionData.id,
                              text: connectionData.sourceAlias + ' - ' + sourceSystemData.security
                          });
                      }
                  }
              }
          }
  
          if(connectionOptions.length > 0){
              newSelectOptions.push({ text: 'System', children: connectionOptions});
          }
  
          return newSelectOptions;
      };
  
      /**
       * show/hides a table <tr> rowElement
       * @param rowElement
       */
      let toggleTableRow = rowElement => {
  
          let toggleTableRowExecutor = (resolve, reject) => {
              let cellElements = rowElement.children('td');
              let duration = 350;
              // wrap each <td> into a container (for better animation performance)
              // slideUp new wrapper divs
              if(rowElement.is(':visible')){
                  // hide row
  
                  // stop sig counter by adding a stopClass to each <td>, remove padding
                  cellElements.addClass('stopCounter')
                      .velocity({
                          paddingTop: [0, '4px'],
                          paddingBottom: [0, '4px'],
                          opacity: [0, 1]
                      },{
                          duration: duration,
                          easing: 'linear'
                      }).wrapInner('<div>')
                      .children()
                      .css({
                          'willChange': 'height'
                      }).velocity('slideUp', {
                      duration: duration,
                      easing: 'linear',
                      complete: function(animationElements){
                          // remove wrapper
                          $(animationElements).children().unwrap();
  
                          resolve({
                              action: 'rowHidden',
                              row: rowElement
                          });
                      }
                  });
              }else{
                  // show row
  
                  // remove padding on "hidden" cells for smother animation
                  cellElements.css({
                      'padding-top': 0,
                      'padding-bottom': 0,
                      'willChange': 'padding-top, padding-top, height'
                  });
  
                  // add hidden wrapper for ea
                  cellElements.wrapInner($('<div>').hide());
  
                  // show row for padding animation
                  rowElement.show();
  
                  cellElements.velocity({
                      paddingTop: ['4px', 0],
                      paddingBottom: ['4px', 0]
                  },{
                      duration: duration,
                      queue: false,
                      complete: function(){
                          // animate <td> wrapper
                          cellElements.children()
                              .css({
                                  'willChange': 'height'
                              }).velocity('slideDown', {
                              duration: duration,
                              complete: function(animationElements){
                                  // remove wrapper
                                  for(let i = 0; i < animationElements.length; i++){
                                      let currentWrapper = $(animationElements[i]);
                                      if(currentWrapper.children().length > 0){
                                          currentWrapper.children().unwrap();
                                      }else{
                                          currentWrapper.parent().html( currentWrapper.html() );
                                      }
                                  }
  
                                  resolve({
                                      action: 'rowShown',
                                      row: rowElement
                                  });
                              }
                          });
                      }
                  });
              }
          };
  
          return new Promise(toggleTableRowExecutor);
      };
  
      /**
       * update scanned signatures progress bar
       * @param tableApi
       * @param options
       */
      let updateScannedSignaturesBar = (tableApi, options) => {
          let tableElement = tableApi.table().node();
          let moduleElement = $(tableElement).parents('.' + config.moduleTypeClass);
          let progressBar = moduleElement.find('.progress-bar');
          let progressBarLabel = moduleElement.find('.progress-label-right');
  
          let percent = 0;
          let progressBarType = '';
          let columnGroupData = tableApi.column('group:name').data();
          let sigCount = columnGroupData.length;
          let sigIncompleteCount = columnGroupData.filter((value, index) => !value).length;
  
          if(sigCount){
              percent = 100 - Math.round( 100 / sigCount * sigIncompleteCount );
          }
  
          if(percent < 30){
              progressBarType = 'progress-bar-danger';
          }else if(percent < 100){
              progressBarType = 'progress-bar-warning';
          }else{
              progressBarType = 'progress-bar-success';
          }
  
          progressBarLabel.text(percent + '%');
          progressBar.removeClass().addClass('progress-bar').addClass(progressBarType);
          progressBar.attr('aria-valuenow', percent);
          progressBar.css({width: percent + '%'});
  
          // show notifications
          if(options.showNotice !== false){
              let notification = (sigCount - sigIncompleteCount) + ' / ' + sigCount + ' (' + percent + '%) signatures scanned';
  
              if(percent < 100){
                  Util.showNotify({title: 'Unscanned signatures', text: notification, type: 'info'});
              }else{
                  Util.showNotify({title: 'System is scanned', text: notification, type: 'success'});
              }
          }
      };
  
      /**
       * open "signature reader" dialog for signature table
       * @param systemData
       */
      $.fn.showSignatureReaderDialog = function(systemData){
          let moduleElement = $(this);
  
          requirejs(['text!templates/dialog/signature_reader.html', 'mustache'], (template, Mustache) => {
              let signatureReaderDialog = bootbox.dialog({
                  title: 'Signature reader',
                  message: Mustache.render(template, {}),
                  buttons: {
                      close: {
                          label: 'cancel',
                          className: 'btn-default'
                      },
                      success: {
                          label: '<i class="fas fa-paste fa-fw"></i>&nbsp;update signatures',
                          className: 'btn-success',
                          callback: function(){
                              let form = this.find('form');
                              let formData = form.getFormValues();
                              let signatureOptions = {
                                  deleteOld: (formData.deleteOld) ? 1 : 0
                              };
  
                              let mapId = moduleElement.data('mapId');
                              let systemId = moduleElement.data('systemId');
                              let tableApi = getDataTableInstance(mapId, systemId, 'primary');
  
                              updateSignatureTableByClipboard(tableApi, systemData, formData.clipboard, signatureOptions);
                          }
                      }
                  }
              });
  
              // dialog shown event
              signatureReaderDialog.on('shown.bs.modal', function(e){
                  signatureReaderDialog.initTooltips();
  
                  // set focus on sig-input textarea
                  signatureReaderDialog.find('textarea').focus();
              });
          });
      };
  
      /**
       * parses a copy&paste string from ingame scanning window
       * @param systemData
       * @param clipboard
       * @returns {Array}
       */
      let parseSignatureString = (systemData, clipboard) => {
          let signatureData = [];
  
          if(clipboard.length){
              let signatureRows = clipboard.split(/\r\n|\r|\n/g);
              let signatureGroupOptions = config.signatureGroupsNames;
              let invalidSignatures = 0;
  
              for(let i = 0; i < signatureRows.length; i++){
                  let rowData = signatureRows[i].split(/\t/g);
                  if(rowData.length === 6){
                      // check if sig Type = anomaly or combat site
                      if(validSignatureNames.indexOf( rowData[1] ) !== -1){
  
                          let sigGroup = $.trim(rowData[2]).toLowerCase();
                          let sigDescription = $.trim(rowData[3]);
                          let sigGroupId = 0;
                          let typeId = 0;
  
                          // get groupId by groupName
                          for(let groupOption of signatureGroupOptions){
                              let reg = new RegExp(groupOption.text, 'i');
                              if(reg.test(sigGroup)){
                                  sigGroupId = groupOption.value;
                                  break;
                              }
                          }
  
                          // wormhole type cant be extracted from signature string -> skip function call
                          if(sigGroupId !== 5){
                              // try to get "typeId" by description string
                              typeId = Util.getSignatureTypeIdByName(systemData, sigGroupId, sigDescription);
  
                              // set signature name as "description" if signature matching failed
                              sigDescription = (typeId === 0) ? sigDescription : '';
                          }else{
                              sigDescription = '';
                          }
  
                          // map array values to signature Object
                          let signatureObj = {
                              systemId: systemData.id,
                              name: $.trim( rowData[0] ).toLowerCase(),
                              groupId: sigGroupId,
                              typeId: typeId,
                              description: sigDescription
                          };
  
                          signatureData.push(signatureObj);
                      }else{
                          invalidSignatures++;
                      }
                  }
              }
  
              if(invalidSignatures > 0){
                  let notification = invalidSignatures + ' / ' + signatureRows.length + ' signatures invalid';
                  Util.showNotify({title: 'Invalid signature(s)', text: notification, type: 'warning'});
              }
          }
  
          return signatureData;
      };
  
      /**
       * updates the signature table with all signatures pasted into the "signature reader" dialog
       * -> Hint: copy&paste signature data (without any open dialog) will add signatures as well
       * @param tableApi
       * @param systemData
       * @param clipboard data stream
       * @param options
       */
      let updateSignatureTableByClipboard = (tableApi, systemData, clipboard, options) => {
          if(isLockedTable(tableApi, 'clipboard')) return;
  
          let saveSignatureData = signatureData => {
              // lock update function until request is finished
              lockTable(tableApi);
  
              // lock copy during request (prevent spamming (ctrl + c )
              lockTable(tableApi, 'clipboard');
  
              let requestData = {
                  signatures: signatureData,
                  deleteOld: (options.deleteOld) ? 1 : 0,
                  systemId: parseInt(systemData.id)
              };
  
              $.ajax({
                  type: 'POST',
                  url: Init.path.saveSignatureData,
                  data: requestData,
                  dataType: 'json',
                  context: {
                      tableApi: tableApi
                  }
              }).done(function(responseData){
                  // unlock table for update
                  unlockTable(this.tableApi);
                  // updates table with new/updated signature information
                  updateSignatureTable(this.tableApi, responseData.signatures, false);
              }).fail(function(jqXHR, status, error){
                  let reason = status + ' ' + error;
                  Util.showNotify({title: jqXHR.status + ': Update signatures', text: reason, type: 'warning'});
                  $(document).setProgramStatus('problem');
              }).always(function(){
                  unlockTable(this.tableApi);
                  unlockTable(this.tableApi, 'clipboard');
              });
          };
  
          // parse input stream
          let signatureData = parseSignatureString(systemData, clipboard);
          if(signatureData.length > 0){
              // valid signature data parsed
  
              // check if signatures will be added to a system where character is currently in
              // if user is not in any system -> id === undefined -> no "confirmation required
              let currentLocationData = Util.getCurrentLocationData();
              if(
                  currentLocationData.id &&
                  currentLocationData.id !== systemData.id
              ){
  
                  let systemNameStr = (systemData.name === systemData.alias) ? '"' + systemData.name + '"' : '"' + systemData.alias + '" (' + systemData.name + ')';
                  systemNameStr = '<span class="txt-color txt-color-warning">' + systemNameStr + '</span>';
  
                  let msg = 'Update signatures in ' + systemNameStr + ' ? This not your current location, "' + currentLocationData.name + '" !';
                  bootbox.confirm(msg, function(result){
                      if(result){
                          saveSignatureData(signatureData);
                      }
                  });
              }else{
                  // current system selected -> no "confirmation" required
                  saveSignatureData(signatureData);
              }
          }
      };
  
      /**
       * deletes signature rows from signature table
       * @param tableApi
       * @param rows
       */
      let deleteSignatures = (tableApi, rows) => {
          // get unique id array from rows -> in case there are 2 rows with same id -> you never know
          let signatureIds = [...new Set(rows.data().toArray().map(rowData => rowData.id))];
  
          let requestData = {
              signatureIds: signatureIds
          };
  
          $.ajax({
              type: 'POST',
              url: Init.path.deleteSignatureData,
              data: requestData,
              dataType: 'json',
              context: {
                  tableApi: tableApi
              }
          }).done(function(responseData){
              // promises for all delete rows
              let promisesToggleRow = [];
              // get deleted rows -> match with response data
              let rows = this.tableApi.rows((idx, rowData, node) => responseData.deletedSignatureIds.includes(rowData.id));
              // toggle hide animation for rows one by one...
              rows.every(function(rowIdx, tableLoop, rowLoop){
                  let row = this;
                  let rowElement = row.nodes().to$();
  
                  rowElement.pulseBackgroundColor('deleted');
  
                  promisesToggleRow.push(toggleTableRow(rowElement));
              });
  
              // ... all hide animations done ...
              Promise.all(promisesToggleRow).then(payloads => {
                  // ... get deleted (hide animation done) and delete them
                  this.tableApi.rows(payloads.map(payload => payload.row)).remove().draw();
  
                  // update signature bar
                  updateScannedSignaturesBar(this.tableApi, {showNotice: false});
  
                  // update connection conflicts
                  checkConnectionConflicts();
  
                  let notificationOptions = {
                      type: 'success'
                  };
                  if(payloads.length === 1){
                      notificationOptions.title = 'Signature deleted';
                  }else{
                      notificationOptions.title = payloads.length + ' Signatures deleted ';
                  }
                  Util.showNotify(notificationOptions);
              });
          }).fail(function(jqXHR, status, error){
              let reason = status + ' ' + error;
              Util.showNotify({title: jqXHR.status + ': Delete signature', text: reason, type: 'warning'});
              $(document).setProgramStatus('problem');
          });
      };
  
      /**
       * updates a single cell with new data (e.g. "updated" cell)
       * @param tableApi
       * @param rowIndex
       * @param columnSelector
       * @param data
       */
      let updateSignatureCell = (tableApi, rowIndex, columnSelector, data) => {
          tableApi.cell(rowIndex, columnSelector).data(data);
      };
  
      /**
       * check connectionIds for conflicts (multiple signatures -> same connection)
       * -> show "conflict" icon next to select
       */
      let checkConnectionConflicts = () => {
          setTimeout(() => {
              let connectionSelects = $('.' + config.tableCellConnectionClass + '.editable');
              let connectionIds = [];
              let duplicateConnectionIds = [];
              let groupedSelects = [];
  
              connectionSelects.each(function(){
                  let select = $(this);
                  let value = parseInt(select.editable('getValue', true) )|| 0;
  
                  if(
                      connectionIds.indexOf(value) > -1 &&
                      duplicateConnectionIds.indexOf(value) === -1
                  ){
                      // duplicate found
                      duplicateConnectionIds.push(value);
                  }
  
                  if(groupedSelects[value] !== undefined){
                      groupedSelects[value].push(select[0]);
                  }else{
                      groupedSelects[value] = [select[0]];
                  }
  
                  connectionIds.push(value);
              });
  
              // update "conflict" icon next to select label for connectionIds
              connectionSelects.each(function(){
                  let select = $(this);
                  let value = parseInt(select.editable('getValue', true) )|| 0;
                  let conflictIcon = select.find('.fa-exclamation-triangle');
                  if(
                      duplicateConnectionIds.indexOf(value) > -1 &&
                      groupedSelects[value].indexOf(select[0]) > -1
                  ){
                      conflictIcon.removeClass('hide');
                  }else{
                      conflictIcon.addClass('hide');
                  }
              });
          }, 200);
      };
  
      /**
       * get group label by groupId
       * @param groupId
       * @returns {string}
       */
      let getGroupLabelById = (groupId) => {
          let options = config.signatureGroupsLabels.filter(option => option.value === groupId);
          return options.length ? options[0].text : '';
      };
  
      /**
       * helper function - get cell by columnSelector from same row as cell
       * @param tableApi
       * @param cell
       * @param columnSelector
       * @returns {*}
       */
      let getNeighboringCell = (tableApi, cell, columnSelector) => {
          return tableApi.cell(tableApi.row(cell).index(), columnSelector);
      };
  
      /**
       * get next cell by columnSelector
       * @param tableApi
       * @param cell
       * @param columnSelectors
       * @returns {*}
       */
      let searchNextCell = (tableApi, cell, columnSelectors) => {
          if(columnSelectors.length){
              // copy selectors -> .shift() modifies the orig array, important!
              columnSelectors = columnSelectors.slice(0);
              let nextCell = getNeighboringCell(tableApi, cell, columnSelectors.shift());
              let nextCellElement = nextCell.nodes().to$();
              if( nextCellElement.data('editable') ){
                  // cell is xEditable field -> skip "disabled" OR check value
                  let nextCellValue = nextCellElement.editable('getValue', true);
                  if(
                      [0, null].includes(nextCellValue) &&
                      !nextCellElement.data('editable').options.disabled
                  ){
                      // xEditable value is empty
                      return nextCell;
                  }else{
                      // search next cell
                      return searchNextCell(tableApi, cell, columnSelectors);
                  }
              }else if( nextCell.index().column === tableApi.column(-1).index() ){
                  // NO xEditable cell BUT last column (=> action cell) -> OK
                  return nextCell;
              }else{
                  console.error('No cell found for activation!');
              }
          }else{
              // return origin cell
              return tableApi.cell(cell);
          }
      };
  
      /**
       * make cell active -> focus() + show xEditable
       * @param cell
       */
      let activateCell = (cell) => {
          let cellElement = cell.nodes().to$();
          // NO xEditable
          cellElement.focus();
  
          if( cellElement.data('editable') ){
              // cell is xEditable field -> show xEditable form
              cellElement.editable('show');
          }
      };
  
      /**
       * search neighboring cell (same row) and set "active" -> show editable
       * @param tableApi
       * @param cell
       * @param columnSelectors
       */
      let activateNextCell = (tableApi, cell, columnSelectors) => {
          let nextCell = searchNextCell(tableApi, cell, columnSelectors);
          activateCell(nextCell);
      };
  
      /**
       * helper function - set 'save' observer for xEditable cell
       * -> show "neighboring" xEditable field
       * @param tableApi
       * @param cell
       * @param columnSelectorsAjax - used for Ajax save (edit signature)
       * @param columnSelectorsDry - used for dry save (new signature)
       */
      let editableOnSave = (tableApi, cell, columnSelectorsAjax = [], columnSelectorsDry = []) => {
          $(cell).on('save', function(e, params){
              if(params.response){
                  // send by Ajax
                  activateNextCell(tableApi, cell, columnSelectorsAjax);
              }else{
                  // dry save - no request
                  activateNextCell(tableApi, cell, columnSelectorsDry);
              }
          });
      };
  
      /**
       * helper function - set 'hidden' observer for xEditable cell
       * -> set focus() on xEditable field
       * @param tableApi
       * @param cell
       */
      let editableOnHidden = (tableApi, cell) => {
          $(cell).on('hidden', function(e, reason){
              // re-focus element on close (keyboard navigation)
              // 'save' event handles default focus (e.g. open new xEditable)
              // 'hide' handles all the rest (experimental)
              if(reason !== 'save'){
                  this.focus();
              }
          });
      };
  
      /**
       * helper function - set 'shown' observer for xEditable type cell
       * -> enable Select2 for xEditable form
       * @param cell
       */
      let editableGroupOnShown = cell => {
          $(cell).on('shown', function(e, editable){
              let inputField = editable.input.$input;
              inputField.addClass('pf-select2').initSignatureGroupSelect();
          });
      };
  
      /**
       * helper function - set 'save' observer for xEditable group cell
       * -> update scanned signature bar
       * @param tableApi
       * @param cell
       */
      let editableGroupOnSave = (tableApi, cell) => {
          $(cell).on('save', function(e, params){
              if(params.response){
                  // send by Ajax
                  updateScannedSignaturesBar(tableApi, {showNotice: true});
              }
          });
      };
  
      /**
       * helper function - set 'init' observer for xEditable type cell
       * -> disable xEditable field if no options available
       * @param cell
       */
      let editableTypeOnInit = cell => {
          $(cell).on('init', function(e, editable){
              if(!editable.options.source().length){
                  editableDisable($(this));
              }
          });
      };
  
      /**
       * helper function - set 'shown' observer for xEditable type cell
       * -> enable Select2 for xEditable form
       * @param cell
       */
      let editableTypeOnShown = cell => {
          $(cell).on('shown', function(e, editable){
              // destroy possible open popovers (e.g. wormhole types)
              $(this).destroyPopover(true);
  
              let inputField = editable.input.$input;
              let hasOptGroups = inputField.has('optgroup').length > 0;
              inputField.addClass('pf-select2').initSignatureTypeSelect({}, hasOptGroups);
          });
      };
  
      /**
       * helper function - set 'shown' observer for xEditable description cell
       * -> change height for "new signature" table wrapper
       * @param cell
       */
      let editableDescriptionOnShown = cell => {
          $(cell).on('shown', function(e, editable){
              $(this).parents('.' + config.tableToolsActionClass).css('height', '+=35px');
          });
      };
  
      /**
       * helper function - set 'hidden' observer for xEditable description cell
       * -> change height for "new signature" table wrapper
       * @param cell
       */
      let editableDescriptionOnHidden = cell => {
          $(cell).on('hidden', function(e, editable){
              $(this).parents('.' + config.tableToolsActionClass).css('height', '-=35px');
          });
      };
  
      /**
       * helper function - set 'init' observer for xEditable connection cell
       * -> set focus() on xEditable field
       * @param cell
       */
      let editableConnectionOnInit = cell => {
          $(cell).on('init', function(e, editable){
              if(editable.value > 0){
                  // empty connection selects ON INIT don´t make a difference for conflicts
                  checkConnectionConflicts();
              }
          });
      };
  
      /**
       * helper function - set 'shown' observer for xEditable connection cell
       * -> enable Select2 for xEditable form
       * @param cell
       */
      let editableConnectionOnShown = cell => {
          $(cell).on('shown', function(e, editable){
              let inputField = editable.input.$input;
              inputField.addClass('pf-select2').initSignatureConnectionSelect();
          });
      };
  
      /**
       * helper function - set 'save' observer for xEditable connection cell
       * -> check connection conflicts
       * @param cell
       */
      let editableConnectionOnSave = cell => {
          $(cell).on('save', function(e, params){
              checkConnectionConflicts();
          });
      };
  
      /**
       * enable xEditable element
       * @param element
       */
      let editableEnable = element => {
          element.editable('enable');
          // (re)-enable focus on element by tabbing, xEditable removes "tabindex" on 'disable'
          element.attr('tabindex', 0);
      };
  
      /**
       * disable xEditable element
       * @param element
       */
      let editableDisable = element => {
          element.editable('disable');
          // xEditable sets 'tabindex = -1'
      };
  
      /**
       * en/disables xEditable element (select)
       * -> disables if there are no source options found
       * @param element
       */
      let editableSelectCheck = element => {
          if(element.data('editable')){
              let options = element.data('editable').options.source();
              if(options.length > 0){
                  editableEnable(element);
              }else{
                  editableDisable(element);
              }
          }
      };
  
      /**
       * get dataTables default options for signature tables
       * @param mapId
       * @param systemData
       * @returns {{}}
       */
      let getSignatureDataTableDefaults = (mapId, systemData) => {
  
          /**
           * add map/system specific data for each editable field in the sig-table
           * @param params
           * @returns {*}
           */
          let modifyFieldParamsOnSend = params => {
              params.systemId = systemData.id;
              return params;
          };
  
          let dataTableDefaults = {
              pfMeta: {
                  'mapId': mapId,
                  'systemId': systemData.id
              },
              order: [1, 'asc'],
              rowId: rowData => config.sigTableRowIdPrefix + rowData.id,
              language: {
                  emptyTable:     'No signatures added',
                  info:           'Showing _START_ to _END_ of _TOTAL_ signatures',
                  infoEmpty:      'Showing 0 to 0 of 0 signatures',
                  infoFiltered:   '(<i class="fas fa-fw fa-filter"></i> from _MAX_ total)',
                  lengthMenu:     'Show _MENU_',
                  zeroRecords:    'No signatures recorded'
              },
              columnDefs: [
                  {
                      targets: 0,
                      name: 'status',
                      orderable: true,
                      searchable: false,
                      title: '',
                      width: 2,
                      class: ['text-center'].join(' '),
                      data: 'updated',
                      type: 'html',
                      render: {
                          _: (cellData, type, rowData, meta) => {
                              let value = '';
                              if(cellData && cellData.character){
                                  value = Util.getStatusInfoForCharacter(cellData.character, 'class');
                              }
  
                              if(type === 'display'){
                                  value = '<i class="fas fa-fw fa-circle pf-user-status ' + value + '"></i>';
                              }
                              return value;
                          }
                      }
                  },{
                      targets: 1,
                      name: 'id',
                      orderable: true,
                      searchable: true,
                      title: 'id',
                      type: 'string',
                      width: 12,
                      class: [config.tableCellFocusClass, config.sigTableEditSigNameInput].join(' '),
                      data: 'name',
                      createdCell: function(cell, cellData, rowData, rowIndex, colIndex){
                          let tableApi = this.api();
  
                          updateTooltip(cell, cellData);
  
                          editableOnSave(tableApi, cell, [], ['group:name', 'type:name', 'action:name']);
                          editableOnHidden(tableApi, cell);
  
                          $(cell).editable($.extend({
                              mode: 'popup',
                              type: 'text',
                              title: 'signature id',
                              name: 'name',
                              pk: rowData.id || null,
                              emptytext: '? ? ?',
                              value: cellData,
                              inputclass: config.editableNameInputClass,
                              display: function(value){
                                  // change display value to first 3 letters
                                  $(this).text($.trim( value.substr(0, 3) ).toLowerCase());
                              },
                              validate: function(value){
                                  let msg = false;
                                  if($.trim(value).length < 3){
                                      msg = 'Id is less than min of "3"';
                                  }else if($.trim(value).length > 10){
                                      msg = 'Id is more than max of "10"';
                                  }
  
                                  if(msg){
                                      return {newValue: value, msg: msg, field: this};
                                  }
                              },
                              params: modifyFieldParamsOnSend,
                              success: function(response, newValue){
                                  tableApi.cell(cell).data(newValue);
  
                                  $(this).pulseBackgroundColor('changed');
                                  updateTooltip(cell, newValue);
  
                                  if(response){
                                      let newRowData = response.signatures[0];
                                      updateSignatureCell(tableApi, rowIndex, 'status:name', newRowData.updated);
                                      updateSignatureCell(tableApi, rowIndex, 'updated:name', newRowData.updated.updated);
                                  }
                                  tableApi.draw();
                              }
                          }, editableDefaults));
                      }
                  },{
                      targets: 2,
                      name: 'group',
                      orderable: true,
                      searchable: true,
                      title: 'group',
                      type: 'string',     // required for sort/filter because initial data type is numeric
                      width: 40,
                      class: [config.tableCellFocusClass].join(' '),
                      data: 'groupId',
                      render: {
                          sort: getGroupLabelById,
                          filter: getGroupLabelById
                      },
                      createdCell: function(cell, cellData, rowData, rowIndex, colIndex){
                          let tableApi = this.api();
  
                          editableOnSave(tableApi, cell, ['type:name'], ['type:name', 'action:name']);
                          editableOnHidden(tableApi, cell);
                          editableGroupOnShown(cell);
                          editableGroupOnSave(tableApi, cell);
  
                          $(cell).editable($.extend({
                              mode: 'popup',
                              type: 'select',
                              title: 'group',
                              name: 'groupId',
                              pk: rowData.id || null,
                              emptytext: 'unknown',
                              onblur: 'submit',
                              showbuttons: false,
                              value: cellData,
                              prepend: [{value: 0, text: ''}],
                              params: modifyFieldParamsOnSend,
                              source: config.signatureGroupsLabels,
                              display: function(value, sourceData){
                                  let selected = $.fn.editableutils.itemsByValue(value, sourceData);
                                  if(selected.length && selected[0].value > 0){
                                      $(this).html(selected[0].text);
                                  }else{
                                      $(this).empty();
                                  }
                              },
                              validate: function(value){
                                  // convert string to int -> important for further processing
                                  // -> on submit record (new signature) validate() is called and no error should be returned
                                  // value should already be integer
                                  if( !Number.isInteger(value) ){
                                      return {newValue: parseInt(value) || 0, msg: null};
                                  }
                              },
                              success: function(response, newValue){
                                  tableApi.cell(cell).data(newValue);
  
                                  $(this).pulseBackgroundColor('changed');
  
                                  if(response){
                                      let newRowData = response.signatures[0];
                                      updateSignatureCell(tableApi, rowIndex, 'status:name', newRowData.updated);
                                      updateSignatureCell(tableApi, rowIndex, 'updated:name', newRowData.updated.updated);
                                  }
                                  tableApi.draw();
  
                                  // find related "type" select (same row) and change options ---------------------------
                                  let signatureTypeCell = getNeighboringCell(tableApi, cell, 'type:name');
                                  let signatureTypeField = signatureTypeCell.nodes().to$();
                                  editableSelectCheck(signatureTypeField);
  
                                  signatureTypeCell.data(0);
                                  signatureTypeField.editable('setValue', 0);
  
  
                                  // find "connection" select (same row) and change "enabled" flag ----------------------
                                  let signatureConnectionCell = getNeighboringCell(tableApi, cell, 'connection:name');
                                  let signatureConnectionField = signatureConnectionCell.nodes().to$();
  
                                  if(newValue === 5){
                                      // wormhole
                                      editableEnable(signatureConnectionField);
                                  }else{
                                      checkConnectionConflicts();
                                      editableDisable(signatureConnectionField);
                                  }
                                  signatureConnectionCell.data(0);
                                  signatureConnectionField.editable('setValue', 0);
                              }
                          }, editableDefaults));
                      }
                  },{
                      targets: 3,
                      name: 'type',
                      orderable: false,
                      searchable: false,
                      title: 'type',
                      type: 'string',     // required for sort/filter because initial data type is numeric
                      width: 180,
                      class: [config.tableCellFocusClass].join(' '),
                      data: 'typeId',
                      createdCell: function(cell, cellData, rowData, rowIndex, colIndex){
                          let tableApi = this.api();
  
                          editableOnSave(tableApi, cell, ['connection:name'], ['action:name']);
                          editableOnHidden(tableApi, cell);
                          editableTypeOnInit(cell);
                          editableTypeOnShown(cell);
  
                          $(cell).editable($.extend({
                              mode: 'popup',
                              type: 'select',
                              title: 'type',
                              name: 'typeId',
                              pk: rowData.id || null,
                              emptytext: 'unknown',
                              onblur: 'submit',
                              showbuttons: false,
                              disabled: rowData.groupId <= 0,    // initial disabled if groupId not set
                              value: cellData,
                              prepend: [{value: 0, text: ''}],
                              params: modifyFieldParamsOnSend,
                              source: function(){
                                  // get current row data (important!)
                                  // -> "rowData" param is not current state, values are "on createCell()" state
                                  let rowData = tableApi.row($(cell).parents('tr')).data();
  
                                  let typeOptions = getAllSignatureNames(
                                      systemData,
                                      systemData.type.id,
                                      Util.getAreaIdBySecurity(systemData.security),
                                      rowData.groupId
                                  );
                                  return typeOptions;
                              },
                              display: function(value, sourceData){
                                  let selected = $.fn.editableutils.itemsByValue(value, sourceData);
                                  if(selected.length && selected[0].value > 0){
                                      $(this).html(FormElement.formatSignatureTypeSelectionData({text: selected[0].text}));
                                  }else{
                                      $(this).empty();
                                  }
                              },
                              validate: function(value){
                                  // convert string to int -> important for further processing
                                  // -> on submit record (new signature) validate() is called and no error should be returned
                                  // value should already be integer
                                  if( !Number.isInteger(value) ){
                                      return {newValue: parseInt(value) || 0, msg: null};
                                  }
                              },
                              success: function(response, newValue){
                                  tableApi.cell(cell).data(newValue);
  
                                  $(this).pulseBackgroundColor('changed');
  
                                  if(response){
                                      let newRowData = response.signatures[0];
                                      updateSignatureCell(tableApi, rowIndex, 'status:name', newRowData.updated);
                                      updateSignatureCell(tableApi, rowIndex, 'updated:name', newRowData.updated.updated);
                                  }
                                  tableApi.draw();
                              }
                          }, editableDefaults));
                      }
                  },{
                      targets: 4,
                      name: 'description',
                      orderable: false,
                      searchable: false,
                      title: 'description',
                      class: [config.tableCellFocusClass, config.tableCellActionClass].join(' '),
                      type: 'html',
                      data: 'description',
                      defaultContent: '',
                      createdCell: function(cell, cellData, rowData, rowIndex, colIndex){
                          let tableApi = this.api();
  
                          editableOnSave(tableApi, cell, [], ['action:name']);
                          editableOnHidden(tableApi, cell);
                          editableDescriptionOnShown(cell);
                          editableDescriptionOnHidden(cell);
  
                          $(cell).editable($.extend({
                              mode: 'inline',
                              type: 'textarea',
                              title: 'description',
                              name: 'description',
                              pk: rowData.id || null,
                              emptytext: '<i class="fas fa-fw fa-lg fa-pen"></i>',
                              onblur: 'submit',
                              showbuttons: false,
                              inputclass: config.editableDescriptionInputClass,
                              emptyclass: config.moduleHeadlineIconClass,
                              params: modifyFieldParamsOnSend,
                              success: function(response, newValue){
                                  tableApi.cell(cell).data(newValue);
  
                                  $(this).pulseBackgroundColor('changed');
  
                                  if(response){
                                      let newRowData = response.signatures[0];
                                      updateSignatureCell(tableApi, rowIndex, 'status:name', newRowData.updated);
                                      updateSignatureCell(tableApi, rowIndex, 'updated:name', newRowData.updated.updated);
                                  }
                                  tableApi.draw();
                              }
                          }, editableDefaults));
                      }
                  },{
                      targets: 5,
                      name: 'connection',
                      orderable: false,
                      searchable: false,
                      title: 'leads to',
                      type: 'string',     // required for sort/filter because initial data type is numeric
                      className: [config.tableCellFocusClass, config.tableCellConnectionClass].join(' '),
                      width: 80,
                      data: 'connection.id',
                      defaultContent: 0,
                      createdCell: function(cell, cellData, rowData, rowIndex, colIndex){
                          let tableApi = this.api();
  
                          editableOnSave(tableApi, cell, [], ['action:name']);
                          editableOnHidden(tableApi, cell);
                          editableConnectionOnInit(cell);
                          editableConnectionOnShown(cell);
                          editableConnectionOnSave(cell);
  
                          $(cell).editable($.extend({
                              mode: 'popup',
                              type: 'select',
                              title: 'system',
                              name: 'connectionId',
                              pk: rowData.id || null,
                              emptytext: 'unknown',
                              onblur: 'submit',
                              showbuttons: false,
                              disabled: rowData.groupId !== 5,    // initial disabled if NON wh
                              value: cellData,
                              prepend: [{value: 0, text: ''}],
                              params: modifyFieldParamsOnSend,
                              source: function(){
                                  let activeMap = Util.getMapModule().getActiveMap();
                                  let mapId = activeMap.data('id');
                                  let connectionOptions = getSignatureConnectionOptions(mapId, systemData);
                                  return connectionOptions;
                              },
                              display: function(value, sourceData){
                                  let selected = $.fn.editableutils.itemsByValue(value, sourceData);
                                  if(selected.length && selected[0].value > 0){
                                      let errorIcon = '<i class="fas fa-exclamation-triangle txt-color txt-color-danger hide"></i>&nbsp;';
                                      $(this).html(FormElement.formatSignatureConnectionSelectionData({text: selected[0].text})).prepend(errorIcon);
                                  }else{
                                      $(this).empty();
                                  }
                              },
                              validate: function(value, b, c){
                                  // convert string to int -> important for further processing
                                  // -> on submit record (new signature) validate() is called and no error should be returned
                                  // value should already be integer
                                  if( !Number.isInteger(value) ){
                                      return {newValue: parseInt(value) || 0, msg: null};
                                  }
                              },
                              success: function(response, newValue){
                                  tableApi.cell(cell).data(newValue);
  
                                  $(this).pulseBackgroundColor('changed');
  
                                  if(response){
                                      let newRowData = response.signatures[0];
                                      updateSignatureCell(tableApi, rowIndex, 'status:name', newRowData.updated);
                                      updateSignatureCell(tableApi, rowIndex, 'updated:name', newRowData.updated.updated);
                                  }
                                  tableApi.draw();
                              }
                          }, editableDefaults));
                      }
                  },{
                      targets: 6,
                      name: 'created',
                      title: 'created',
                      searchable: false,
                      width: 80,
                      className: ['text-right', config.tableCellCounterClass, 'min-screen-d'].join(' '),
                      data: 'created.created',
                      defaultContent: ''
                  },{
                      targets: 7,
                      name: 'updated',
                      title: 'updated',
                      searchable: false,
                      width: 80,
                      className: ['text-right', config.tableCellCounterClass, 'min-screen-d'].join(' '),
                      data: 'updated.updated',
                      defaultContent: '',
                      createdCell: function(cell, cellData, rowData, rowIndex, colIndex){
                          // highlight cell
                          let diff = Math.floor((new Date()).getTime()) - cellData * 1000;
  
                          // age > 1 day
                          if( diff > 86400000){
                              $(cell).addClass('txt-color txt-color-warning');
                          }
                      }
                  },{
                      targets: 8,
                      name: 'info',
                      title: '',
                      orderable: false,
                      searchable: false,
                      width: 10,
                      class: ['text-center', Util.config.helpClass , Util.config.popoverTriggerClass].join(' '),
                      data: 'created.created',
                      defaultContent: '',
                      render: {
                          display: (cellData, type, rowData, meta) => {
                              if(cellData){
                                  return '<i class="fas fa-question-circle"></i>';
                              }
                          }
                      }
                  },{
                      targets: 9,
                      name: 'action',
                      title: '',
                      orderable: false,
                      searchable: false,
                      width: 10,
                      class: ['text-center', config.tableCellFocusClass, config.tableCellActionClass].join(' '),
                      data: null,
                      render: {
                          display: (cellData, type, rowData, meta) => {
                              let val = '<i class="fas fa-plus"></i>';
                              if(rowData.id){
                                  val = '<i class="fas fa-times txt-color txt-color-redDarker"></i>';
                              }
                              return val;
                          }
                      },
                      createdCell: function(cell, cellData, rowData, rowIndex, colIndex){
                          let tableApi = this.api();
  
                          if(rowData.id){
                              // delete signature -----------------------------------------------------------------------
                              let confirmationSettings = {
                                  container: 'body',
                                  placement: 'left',
                                  btnCancelClass: 'btn btn-sm btn-default',
                                  btnCancelLabel: 'cancel',
                                  btnCancelIcon: 'fas fa-fw fa-ban',
                                  title: 'delete signature',
                                  btnOkClass: 'btn btn-sm btn-danger',
                                  btnOkLabel: 'delete',
                                  btnOkIcon: 'fas fa-fw fa-times',
                                  onConfirm: function(e, target){
                                      // top scroll to top
                                      e.preventDefault();
  
                                      let deleteRowElement = $(target).parents('tr');
                                      let row = tableApi.rows(deleteRowElement);
                                      deleteSignatures(tableApi, row);
                                  }
                              };
  
                              $(cell).confirmation(confirmationSettings);
                          }else{
                              // add new signature ----------------------------------------------------------------------
                              $(cell).on('click', {tableApi: tableApi, rowIndex: rowIndex}, function(e){
                                  e.stopPropagation();
                                  e.preventDefault();
  
                                  let secondaryTableApi = e.data.tableApi;
                                  let metaData = getTableMetaData(secondaryTableApi);
                                  let primaryTableApi = getDataTableInstance(metaData.mapId, metaData.systemId, 'primary');
  
                                  let formFields = secondaryTableApi.row(e.data.rowIndex).nodes().to$().find('.editable');
  
                                  // the "hide" makes sure to take care about open editable fields (e.g. description)
                                  // otherwise, changes would not be submitted in this field (not necessary)
                                  formFields.editable('hide');
  
                                  // submit all xEditable fields
                                  formFields.editable('submit', {
                                      url: Init.path.saveSignatureData,
                                      ajaxOptions: {
                                          dataType: 'json', //assuming json response
                                          beforeSend: function(xhr, settings){
                                              lockTable(primaryTableApi);
                                          },
                                          context: {
                                              primaryTableApi: primaryTableApi,
                                              secondaryTableApi: secondaryTableApi,
                                          }
                                      },
                                      data: {
                                          systemId: metaData.systemId, // additional data to submit
                                          pk: 0 // new data no primary key
                                      },
                                      error: editableDefaults.error, // user default xEditable error function
                                      success: function(data, editableConfig){
                                          let context = editableConfig.ajaxOptions.context;
                                          let primaryTableApi = context.primaryTableApi;
                                          let secondaryTableApi = context.secondaryTableApi;
  
                                          unlockTable(primaryTableApi);
  
                                          let signatureData = data.signatures[0];
                                          let row = addSignatureRow(primaryTableApi, signatureData);
                                          if(row){
                                              primaryTableApi.draw();
                                              // highlight
                                              row.nodes().to$().pulseBackgroundColor('added');
  
                                              // prepare "add signature" table for new entry -> reset -------------------
                                              secondaryTableApi.clear().row.add($.extend(true, {}, emptySignatureData)).draw();
  
                                              Util.showNotify({
                                                  title: 'Signature added',
                                                  text: 'Name: ' + signatureData.name,
                                                  type: 'success'
                                              });
  
                                              // update signature bar
                                              updateScannedSignaturesBar(primaryTableApi, {showNotice: true});
                                          }
                                      }
                                  });
                              });
                          }
                      }
                  }
              ],
              createdRow: function(row, data, dataIndex){
                  // enable tabbing for interactive cells
                  let focusCells = $(row).find('.' + config.tableCellFocusClass + ':not(.editable-disabled)').attr('tabindex', 0);
                  // enable "return" key -> click()
                  focusCells.on('keydown', function(e){
                      e.stopPropagation();
                      if(e.which === 13){
                          $(this).trigger('click');
                      }
                  });
              }
          };
  
          return dataTableDefaults;
      };
  
      /**
       * key (arrow) navigation inside a table -> set cell focus()
       * @param tableApi
       * @param e
       */
      let keyNavigation = (tableApi, e) => {
          let offset = [0, 0];
          if(e.keyCode === 37){
              offset = [-1, 0];
          }else if(e.keyCode === 38){
              offset = [0, -1];
          }else if(e.keyCode === 39){
              offset = [1, 0];
          }else if(e.keyCode === 40){
              offset = [0, 1];
          }
  
          if(offset !== [0, 0]){
              /**
               * check if cellIndex is out of table range
               * @param tableApi
               * @param cellIndex
               * @returns {*}
               */
              let checkIndex = (tableApi, cellIndex) => {
                  if(cellIndex[0] < 0){
                      cellIndex[0] = tableApi.column(':last').nodes().to$().index(); // last column
                  }
                  if(cellIndex[0] > tableApi.column(':last').nodes().to$().index()){
                      cellIndex[0] = 0; // first column
                  }
                  if(cellIndex[1] < 0){
                      cellIndex[1] = tableApi.row(':last', {search: 'applied'}).nodes().to$().index(); // last row
                  }
                  if(cellIndex[1] > tableApi.row(':last', {search: 'applied'}).nodes().to$().index()){
                      cellIndex[1] = 0; // first row
                  }
                  return cellIndex;
              };
  
              /**
               * recursive search next cell
               * @param tableApi
               * @param cellOrigin
               * @param offset
               * @returns {*}
               */
              let searchCell = (tableApi, cellOrigin, offset) => {
                  // we need to get the current cell indexes from DOM (not internal DataTables indexes)
                  let nodeOrig = cellOrigin.nodes();
                  let colIndex = nodeOrig.to$().index();
                  let rowIndex = nodeOrig.to$().closest('tr').index();
                  let currentCellIndex = [colIndex, rowIndex];
                  let newCellIndex = currentCellIndex.map((index, i) => index + offset[i]);
                  // check if cell index is inside table dimensions
                  newCellIndex = checkIndex(tableApi, newCellIndex);
  
                  let cell = tableApi.cell(':eq(' + newCellIndex[1] + ')', ':eq(' + newCellIndex[0] + ')', {search: 'applied'});
                  let node = cell.node();
  
                  if(
                      !node.hasAttribute('tabindex') ||
                      parseInt(node.getAttribute('tabindex')) < 0
                  ){
                      // cell can not be focused -> search next
                      cell = searchCell(tableApi, cell, offset);
                  }
                  return cell;
              };
  
              let cell = searchCell(tableApi, tableApi.cell(e.target), offset);
              cell.node().focus();
          }
      };
  
      /**
       * draw signature table toolbar (add signature button, scan progress bar
       * @param moduleElement
       * @param mapId
       * @param systemData
       */
      let drawSignatureTableNew = (moduleElement, mapId, systemData) => {
          let secondaryTableContainer = $('<div>', {
              class: config.tableToolsActionClass
          });
  
          // create "empty table for new signature
          let table = $('<table>', {
              id: getTableId(mapId, systemData.id, 'secondary'),
              class: ['stripe', 'row-border', 'compact', 'nowrap', config.sigTableClass, config.sigTableSecondaryClass].join(' ')
          });
  
          secondaryTableContainer.append(table);
  
          moduleElement.find('.' + config.moduleHeadClass).after(secondaryTableContainer);
  
          let dataTableOptions = {
              paging: false,
              info: false,
              searching: false,
              tabIndex: -1,
              data: [$.extend(true, {}, emptySignatureData)],
              initComplete: function(settings, json){
                  let tableApi = this.api();
  
                  $(this).on('keyup', 'td', {tableApi: tableApi}, function(e){
                      keyNavigation(tableApi, e);
                  });
              }
          };
  
          $.extend(true, dataTableOptions, getSignatureDataTableDefaults(mapId, systemData));
  
          let tableApi = table.DataTable(dataTableOptions);
  
          // "Responsive" dataTables plugin did not load automatic (because table is invisible onInit)
          // -> manually start "Responsive" extension -> see default dataTable setting for config e.g. breakpoints
          new $.fn.dataTable.Responsive(tableApi);
      };
  
      /**
       * filter table "group" column
       * @param tableApi
       * @param newValue
       * @param sourceOptions
       */
      let searchGroupColumn = (tableApi, newValue, sourceOptions) => {
          let column = tableApi.column('group:name');
          let pattern = '';
  
          if(newValue.length <= sourceOptions.length){
              // all options selected + "prepend" option
              let selected = $.fn.editableutils.itemsByValue(newValue, sourceOptions);
  
              pattern = selected.map(option => option.value !== 0 ? $.fn.dataTable.util.escapeRegex(option.text) : '^$').join('|');
          }
          column.search(pattern, true, false).draw();
      };
  
      /**
       * init table filter button "group" column
       * @param tableApi
       */
      let initGroupFilterButton = tableApi => {
          let characterId = Util.getCurrentCharacterId();
  
          let promiseStore = MapUtil.getLocaleData('character', Util.getCurrentCharacterId());
          promiseStore.then(data => {
              let filterButton = tableApi.button('tableTools', 'filterGroup:name').node();
              let prependOptions = [{value: 0, text: 'unknown'}];
              let sourceOptions = config.signatureGroupsLabels;
              let selectedValues = [];
  
              if(data && data.filterSignatureGroups && data.filterSignatureGroups.length){
                  // select local stored values
                  selectedValues = data.filterSignatureGroups;
              }else{
                  // no default group filter options -> show all
                  selectedValues = sourceOptions.map(option => option.value);
                  selectedValues.unshift(0);
              }
  
              filterButton.editable({
                  mode: 'popup',
                  container: 'body',
                  type: 'checklist',
                  showbuttons: false,
                  onblur: 'submit',
                  highlight: false,
                  title: 'filter groups',
                  value: selectedValues,
                  prepend: prependOptions,
                  source: sourceOptions,
                  inputclass: config.editableUnknownInputClass,
                  display: function(value, sourceData){
                      // update filter button label
                      let html = '<i class="fa fa-filter"></i>group';
                      let allSelected = value.length >= sourceData.length;
                      if( !allSelected ){
                          html += '&nbsp;(' + value.length + ')';
                      }
                      $(this).toggleClass('active', !allSelected).html(html);
                  },
                  validate: function(value){
                      // convert string to int -> important for further processing
                      return {newValue: value.map(num => parseInt(num)), msg: null};
                  }
              });
  
              let allOptions = prependOptions.concat(sourceOptions);
  
              filterButton.on('save', {tableApi: tableApi, sourceOptions: allOptions}, function(e, params){
                  // store values local -> IndexDB
                  MapUtil.storeLocaleCharacterData('filterSignatureGroups', params.newValue);
  
                  searchGroupColumn(e.data.tableApi, params.newValue, e.data.sourceOptions);
              });
  
              // set initial search string -> even if table ist currently empty
              searchGroupColumn(tableApi, selectedValues, allOptions);
          });
      };
  
      /**
       * draw empty signature table
       * @param moduleElement
       * @param mapId
       * @param systemData
       */
      let drawSignatureTable = (moduleElement, mapId, systemData) => {
          let table = $('<table>', {
              id: getTableId(mapId, systemData.id, 'primary'),
              class: ['display', 'compact', 'nowrap', config.sigTableClass, config.sigTablePrimaryClass].join(' ')
          });
  
          moduleElement.append(table);
  
          let dataTableOptions = {
              tabIndex: -1,
              dom: '<"row"<"col-xs-3"l><"col-xs-5"B><"col-xs-4"f>>' +
                  '<"row"<"col-xs-12"tr>>' +
                  '<"row"<"col-xs-5"i><"col-xs-7"p>>',
              buttons: {
                  name: 'tableTools',
                  buttons: [
                      {
                          name: 'filterGroup',
                          className: config.moduleHeadlineIconClass,
                          text: '' // set by js (xEditable)
                      },
                      {
                          name: 'selectAll',
                          className: config.moduleHeadlineIconClass,
                          text: '<i class="fa fa-check-double"></i>select all',
                          action: function(e, tableApi, node, conf){
                              let allRows = tableApi.rows();
                              let selectedRows = getSelectedRows(tableApi);
                              let allRowElements = allRows.nodes().to$();
  
                              if(allRows.data().length === selectedRows.data().length){
                                  allRowElements.removeClass('selected');
                              }else{
                                  allRowElements.addClass('selected');
                              }
  
                              // check delete button
                              checkDeleteSignaturesButton(tableApi);
                          }
                      },
                      {
                          name: 'delete',
                          className: [config.moduleHeadlineIconClass, config.sigTableClearButtonClass].join(' '),
                          text: '<i class="fa fa-trash"></i>delete&nbsp;(<span>0</span>)',
                          action: function(e, tableApi, node, conf){
                              let selectedRows = getSelectedRows(tableApi);
                              bootbox.confirm('Delete ' + selectedRows.data().length + ' signature?', function(result){
                                  if(result){
                                      deleteSignatures(tableApi, selectedRows);
                                  }
                              });
                          }
                      }
                  ]
              },
              initComplete: function(settings, json){
                  let tableApi = this.api();
  
                  initGroupFilterButton(tableApi);
  
                  $(this).on('keyup', 'td', {tableApi: tableApi}, function(e){
                      keyNavigation(tableApi, e);
                  });
  
                  Counter.initTableCounter(this, ['created:name', 'updated:name']);
              }
          };
  
          $.extend(true, dataTableOptions, getSignatureDataTableDefaults(mapId, systemData));
  
          let tableApi = table.DataTable(dataTableOptions);
  
          // "Responsive" dataTables plugin did not load automatic (because table is invisible onInit)
          // -> manually start "Responsive" extension -> see default dataTable setting for config e.g. breakpoints
          new $.fn.dataTable.Responsive(tableApi);
  
          // lock table until module is fully rendered
          lockTable(tableApi);
      };
  
      /**
       * open xEditable input field in "new Signature" table
       * @param moduleElement
       */
      let focusNewSignatureEditableField = moduleElement => {
          moduleElement.find('.' + config.sigTableSecondaryClass)
              .find('td.' + config.sigTableEditSigNameInput).editable('show');
      };
  
      /**
       * get all selected rows of a table
       * @param tableApi
       * @returns {*}
       */
      let getSelectedRows = tableApi => {
          return tableApi.rows('.selected');
      };
  
      /**
       * check the "delete signature" button. show/hide the button if a signature is selected
       * @param tableApi
       */
      let checkDeleteSignaturesButton = tableApi => {
          let selectedRows = getSelectedRows(tableApi);
          let selectedRowCount = selectedRows.data().length;
          let clearButton = tableApi.button('tableTools', 'delete:name').node();
  
          if(selectedRowCount > 0){
              let allRows = tableApi.rows();
              let rowCount = allRows.data().length;
  
              let countText = selectedRowCount;
              if(selectedRowCount >= rowCount){
                  countText = 'all';
              }
              clearButton.find('i+span').text(countText);
  
              // update clear signatures button text
              clearButton.velocity('stop');
  
              if( clearButton.is(':hidden') ){
                  // show button
                  clearButton.velocity('transition.expandIn', {
                      duration: 100
                  });
              }else{
                  // highlight button
                  clearButton.velocity('callout.pulse', {
                      duration: 200
                  });
              }
          }else{
              // hide button
              clearButton.velocity('transition.expandOut', {
                  duration: 100
              });
          }
      };
  
      /**
       * set module observer and look for relevant signature data to update
       * @param moduleElement
       * @param mapId
       * @param systemData
       */
      let setModuleObserver = (moduleElement, mapId, systemData) => {
          let primaryTable = moduleElement.find('.' + config.sigTablePrimaryClass);
          let primaryTableApi = getDataTableInstance(mapId, systemData.id, 'primary');
  
          // add signature toggle ---------------------------------------------------------------------------------------
          let toggleAddSignature = (show = 'auto') => {
              let button = moduleElement.find('.' + config.moduleHeadlineIconAddClass);
              let toolsElement = moduleElement.find('.' + config.tableToolsActionClass);
              button.toggleClass('active', show === 'auto' ? undefined : show);
  
              if(toolsElement.is(':visible') && (!show || show === 'auto')){
                  // hide container
                  toolsElement.velocity('stop').velocity({
                      opacity: [0, 1],
                      height: [0, '70px']
                  },{
                      duration: 150,
                      display: 'none'
                  });
              }else if(!toolsElement.is(':visible') && (show || show === 'auto')){
                  // show container
                  toolsElement.velocity('stop').velocity({
                      opacity: [1, 0],
                      height: ['70px', 0]
                  },{
                      duration: 150,
                      display: 'block',
                      complete: function(){
                          focusNewSignatureEditableField(moduleElement);
                      }
                  });
              }else if(toolsElement.is(':visible') && show){
                  // still visible -> no animation
                  focusNewSignatureEditableField(moduleElement);
              }
          };
  
          moduleElement.find('.' + config.moduleHeadlineIconAddClass).on('click', function(e){
              toggleAddSignature('auto');
          });
  
          moduleElement.on('pf:showSystemSignatureModuleAddNew', function(e){
              toggleAddSignature(true);
          });
  
          // signature reader dialog ------------------------------------------------------------------------------------
          moduleElement.find('.' + config.moduleHeadlineIconReaderClass).on('click', function(e){
              moduleElement.showSignatureReaderDialog(systemData);
          });
  
          // "lazy update" toggle ---------------------------------------------------------------------------------------
          moduleElement.find('.' + config.moduleHeadlineIconLazyClass).on('click', function(e){
              let button = $(this);
              button.toggleClass('active');
          });
  
          // set multi row select ---------------------------------------------------------------------------------------
          primaryTable.on('mousedown', 'td', {tableApi: primaryTableApi}, function(e){
              if(e.ctrlKey){
                  e.preventDefault();
                  e.stopPropagation();
                  // xEditable field should not open -> on 'click'
                  // -> therefore disable "pointer-events" on "td" for some ms -> 'click' event is not triggered
                  $(this).css('pointer-events', 'none');
                  $(e.target.parentNode).toggleClass('selected');
  
                  // check delete button
                  checkDeleteSignaturesButton(e.data.tableApi);
  
                  setTimeout(() => {
                      $(this).css('pointer-events', 'auto');
                  }, 250);
              }
          });
  
          // draw event for signature table -----------------------------------------------------------------------------
          primaryTableApi.on('draw.dt', function(e, settings){
              // check delete button
              let tableApi = $(this).dataTable().api();
              checkDeleteSignaturesButton(tableApi);
          });
  
          // event listener for global "paste" signatures into the page -------------------------------------------------
          moduleElement.on('pf:updateSystemSignatureModuleByClipboard', {tableApi: primaryTableApi}, function(e, clipboard){
              let signatureOptions = {
                  deleteOld: moduleElement.find('.' + config.moduleHeadlineIconLazyClass).hasClass('active') ? 1 : 0
              };
              updateSignatureTableByClipboard(e.data.tableApi, systemData, clipboard, signatureOptions);
          });
  
          // signature column - "type" popover --------------------------------------------------------------------------
          moduleElement.find('.' + config.sigTableClass).hoverIntent({
              over: function(e){
                  let staticWormholeElement = $(this);
                  let wormholeName = staticWormholeElement.attr('data-name');
                  let wormholeData =  Util.getObjVal(Init, 'wormholes.' + wormholeName);
                  if(wormholeData){
                      staticWormholeElement.addWormholeInfoTooltip(wormholeData, {
                          trigger: 'manual',
                          placement: 'top',
                          show: true
                      });
                  }
              },
              out: function(e){
                  $(this).destroyPopover();
              },
              selector: '.editable-click:not(.editable-open) span[class^="pf-system-sec-"]'
          });
  
          // signature column - "info" popover --------------------------------------------------------------------------
          moduleElement.find('.' + config.sigTablePrimaryClass).hoverIntent({
              over: function(e){
                  let cellElement = $(this);
                  let rowData = primaryTableApi.row(cellElement.parents('tr')).data();
                  cellElement.addCharacterInfoTooltip(rowData, {
                      trigger: 'manual',
                      placement: 'top',
                      show: true
                  });
              },
              out: function(e){
                  $(this).destroyPopover();
              },
              selector: 'td.' + Util.config.helpClass
          });
      };
  
      /**
       * add new row to signature table
       * @param tableApi
       * @param signatureData
       * @returns {*}
       */
      let addSignatureRow = (tableApi, signatureData) => {
          let row = null;
          if(tableApi){
              row = tableApi.row.add(signatureData);
          }
          return row;
      };
  
      /**
       * update signature table with new signatures
       * -> add/update/delete rows
       * @param tableApi
       * @param signaturesDataOrig
       * @param deleteOutdatedSignatures
       */
      let updateSignatureTable = (tableApi, signaturesDataOrig, deleteOutdatedSignatures = false) => {
          if(isLockedTable(tableApi)) return;
  
          // disable tableApi until update finished;
          lockTable(tableApi);
  
          // clone signature array because of further manipulation
          let signaturesData = $.extend([], signaturesDataOrig);
  
          let rowIdsExist = [];
  
          let promisesAdded = [];
          let promisesChanged = [];
          let promisesDeleted = [];
  
          let allRows = tableApi.rows();
          let updateEmptyTable = !allRows.any();
  
          let rowUpdate = function(rowIndex, colIndex, tableLoopCount, cellLoopCount){
              let cell = this;
              let node = cell.nodes().to$();
              if(node.data('editable')){
                  // xEditable is active -> should always be active!
                  // set new value even if no change -> e.g. render selected Ids as text labels
                  let oldValue = node.editable('getValue', true);
  
                  // ... some editable cells depend on each other (e.g. group->type, group->connection)
                  switch(node.data('editable').options.name){
                      case 'typeId':
                          // ... disable if no type options found
                          editableSelectCheck(node);
                          break;
                      case 'connectionId':
                          // disables if no wormhole group set
                          let groupId = cell.cell(rowIndex, 'group:name').data();
                          if(groupId === 5){
                              // wormhole
                              editableEnable(node);
                          }else{
                              editableDisable(node);
                          }
                          break;
                  }
  
                  // values should be set AFTER en/disabling of a field
                  node.editable('setValue', cell.data());
  
                  if(oldValue !== cell.data()){
                      // highlight cell on data change
                      node.pulseBackgroundColor('changed');
                  }
              }else if(node.hasClass(config.tableCellCounterClass)){
                  // "updated" timestamp always changed
                  node.pulseBackgroundColor('changed');
              }
          };
  
          // update signatures ------------------------------------------------------------------------------------------
          allRows.every(function(rowIdx, tableLoop, rowLoop){
              let row = this;
              let rowData = row.data();
              let rowElement = row.nodes().to$();
  
              for(let i = 0; i < signaturesData.length; i++){
                  if(signaturesData[i].id === rowData.id){
                      let rowId = row.id(true);
  
                      // check if row was updated
                      if(signaturesData[i].updated.updated > rowData.updated.updated){
                          // set new row data -> draw() is executed after all changes made
                          row.data(signaturesData[i]);
  
                          // bind new signature dataTable data() -> to xEditable inputs
                          row.cells(row.id(true), ['id:name', 'group:name', 'type:name', 'description:name', 'connection:name', 'updated:name'])
                              .every(rowUpdate);
  
                          promisesChanged.push(new Promise((resolve, reject) => {
                              resolve({action: 'changed', rowId: rowId});
                          }));
                      }
  
                      rowIdsExist.push(rowId);
  
                      // remove signature data -> all left signatures will be added
                      signaturesData.splice(i, 1);
                      i--;
                  }
              }
          });
  
          // delete signatures ------------------------------------------------------------------------------------------
          if(deleteOutdatedSignatures){
              let rows = tableApi.rows((rowIdx, rowData, node) => !rowIdsExist.includes('#' + config.sigTableRowIdPrefix + rowData.id));
              rows.every(function(rowIdx, tableLoop, rowLoop){
                  let row = this;
                  let rowId = row.id(true);
                  let rowElement = row.nodes().to$();
  
                  // hide open editable fields on the row before removing them
                  rowElement.find('.editable').editable('destroy');
  
                  // destroy possible open popovers (e.g. wormhole types, update popover)
                  rowElement.destroyPopover(true);
  
                  rowElement.pulseBackgroundColor('deleted');
  
                  promisesDeleted.push(new Promise((resolve, reject) => {
                      toggleTableRow(rowElement).then(payload => resolve({action: 'deleted', rowIdx: rowId}));
                  }));
              }).remove();
          }
  
          // add new signatures -----------------------------------------------------------------------------------------
          for(let signatureData of signaturesData){
              let row = addSignatureRow(tableApi, signatureData);
              let rowId = row.id(true);
              let rowElement = row.nodes().to$();
              rowElement.pulseBackgroundColor('added');
  
              promisesAdded.push(new Promise((resolve, reject) => {
                  resolve({action: 'added', rowId: rowId});
              }));
          }
  
          // done -------------------------------------------------------------------------------------------------------
          Promise.all(promisesAdded.concat(promisesChanged, promisesDeleted)).then(payloads => {
              if(payloads.length){
                  // table data changed -> draw() table changes
                  tableApi.draw();
  
                  // check for "leads to" conflicts -> important if there are just "update" (no add/delete) changes
                  checkConnectionConflicts();
  
                  if(!updateEmptyTable){
                      // no notifications if table was empty just progressbar notification is needed
                      // sum payloads by "action"
                      let notificationCounter = payloads.reduce((acc, payload) => {
                          if(!acc[payload.action]){
                              acc[payload.action] = 0;
                          }
                          acc[payload.action]++;
                          return acc;
                      }, {});
  
                      let notification = '';
                      if(notificationCounter.added > 0){
                          notification += notificationCounter.added + ' added<br>';
                      }
                      if(notificationCounter.changed > 0){
                          notification += notificationCounter.changed + ' updated<br>';
                      }
                      if(notificationCounter.deleted > 0){
                          notification += notificationCounter.deleted + ' deleted<br>';
                      }
                      if(notification.length){
                          Util.showNotify({title: 'Signatures updated', text: notification, type: 'success'});
                      }
                  }
  
                  updateScannedSignaturesBar(tableApi, {showNotice: true});
              }
  
              // unlock table
              unlockTable(tableApi);
          });
      };
  
      /**
       * update trigger function for this module
       * compare data and update module
       * @param moduleElement
       * @param systemData
       */
      let updateModule = (moduleElement, systemData) => {
  
          if(systemData.signatures){
              let mapId = moduleElement.data('mapId');
              let systemId = moduleElement.data('systemId');
              let tableApi = getDataTableInstance(mapId, systemId, 'primary');
              updateSignatureTable(tableApi, systemData.signatures, true);
          }
  
          moduleElement.hideLoadingAnimation();
      };
  
      /**
       * init callback
       * @param moduleElement
       * @param mapId
       * @param systemData
       */
      let initModule = (moduleElement, mapId, systemData) => {
          let tableApi = getDataTableInstance(mapId, systemData.id, 'primary');
          unlockTable(tableApi);
      };
  
      /**
       * get module toolbar element
       * @returns {jQuery}
       */
      let getHeadlineToolbar = () => {
          let headlineToolbar  = $('<h5>', {
              class: 'pull-right'
          }).append(
              $('<span>', {
                  class: 'progress-label-right',
                  text: '0%'
              }),
              $('<i>', {
                  class: ['fas', 'fa-fw', 'fa-plus', config.moduleHeadlineIconClass, config.moduleHeadlineIconAddClass].join(' '),
                  title: 'add'
              }).attr('data-toggle', 'tooltip'),
              $('<i>', {
                  class: ['fas', 'fa-fw', 'fa-paste', config.moduleHeadlineIconClass, config.moduleHeadlineIconReaderClass].join(' '),
                  title: 'signature reader'
              }).attr('data-toggle', 'tooltip'),
              $('<i>', {
                  class: ['fas', 'fa-fw', 'fa-exchange-alt', config.moduleHeadlineIconClass, config.moduleHeadlineIconLazyClass].join(' '),
                  title: 'lazy \'delete\' signatures'
              }).attr('data-toggle', 'tooltip')
          );
  
          headlineToolbar.find('[data-toggle="tooltip"]').tooltip({
              container: 'body'
          });
  
          return headlineToolbar;
      };
  
      /**
       * get module element
       * @param parentElement
       * @param mapId
       * @param systemData
       * @returns {jQuery}
       */
      let getModule = (parentElement, mapId, systemData) => {
          let moduleElement = $('<div>').append(
              $('<div>', {
                  class: config.moduleHeadClass
              }).append(
                  $('<h5>', {
                      class: config.moduleHandlerClass
                  }),
                  $('<h5>', {
                      text: 'Signatures'
                  }),
                  getHeadlineToolbar()
              )
          );
  
          // scanned signatures progress bar ----------------------------------------------------------------------------
          requirejs(['text!templates/form/progress.html', 'mustache'], (template, Mustache) => {
              let data = {
                  label: true,
                  wrapperClass: config.moduleHeadlineProgressBarClass,
                  class: ['progress-bar-success'].join(' '),
                  percent: 0
              };
              moduleElement.find('.' + config.moduleHeadClass).append(Mustache.render(template, data));
          });
  
          moduleElement.data('mapId', mapId);
          moduleElement.data('systemId', systemData.id);
  
          moduleElement.showLoadingAnimation();
  
          // draw "new signature" add table
          drawSignatureTableNew(moduleElement, mapId, systemData);
  
          // draw signature table
          drawSignatureTable(moduleElement, mapId, systemData);
  
          // set module observer
          setModuleObserver(moduleElement, mapId, systemData);
  
          return moduleElement;
      };
  
      /**
       * before module hide callback
       * @param moduleElement
       */
      let beforeHide = moduleElement => {
          // disable update
          let mapId = moduleElement.data('mapId');
          let systemId = moduleElement.data('systemId');
          let tableApi = getDataTableInstance(mapId, systemId, 'primary');
          lockTable(tableApi);
      };
  
      /**
       * before module destroy callback
       * @param moduleElement
       */
      let beforeDestroy = moduleElement => {
          // Destroying the data tables throws
          // -> safety remove  all dataTables
          let mapId = moduleElement.data('mapId');
          let systemId = moduleElement.data('systemId');
          let primaryTableApi = getDataTableInstance(mapId, systemId, 'primary');
          let secondaryTableApi = getDataTableInstance(mapId, systemId, 'secondary');
          primaryTableApi.destroy();
          secondaryTableApi.destroy();
      };
  
      return {
          config: config,
          getModule: getModule,
          initModule: initModule,
          updateModule: updateModule,
          beforeHide: beforeHide,
          beforeDestroy: beforeDestroy,
          getAllSignatureNamesBySystem: getAllSignatureNamesBySystem
      };
  });