Files
pathfinder/js/app/ui/module/system_signature.js

3289 lines
135 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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
// fonts
fontUppercaseClass: 'pf-font-uppercase', // class for "uppercase" font
// tables
tableToolsActionClass: 'pf-table-tools-action', // class for "new signature" table (hidden)
// table toolbar
tableToolbarStatusClass: 'pf-table-toolbar-status', // class for "status" DataTable 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
sigTableInfoClass: 'pf-sig-table-info', // class for info 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
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'),
// signature reader dialog
sigReaderDialogClass: 'pf-sig-reader-dialog', // class for "signature reader" dialog
sigInfoId: 'pf-sig-info', // id for "signature info" table area
sigInfoTextareaId: 'pf-sig-info-textarea', // id for signature reader "textarea"
sigReaderLazyUpdateId: 'pf-sig-reader-lazy-update', // id for "lazy update" checkbox
sigReaderConnectionDeleteId: 'pf-sig-reader-delete-connection', // id for "delete connection" checkbox
sigInfoCountSigNewId: 'pf-sig-info-count-sig-new', // id for "signature new" counter
sigInfoCountSigChangeId: 'pf-sig-info-count-sig-change', // id for "signature change" counter
sigInfoCountSigDeleteId: 'pf-sig-info-count-sig-delete', // id for "signature delete" counter
sigInfoCountConDeleteId: 'pf-sig-info-count-con-delete' // id for "connection delete" counter
};
let sigNameCache = {}; // cache signature names
let validSignatureNames = [ // allowed signature type/names
'Cosmic Anomaly',
'Cosmic Signature',
'Kosmische Anomalie', // de: "Cosmic Anomaly"
'Kosmische Signatur', // de: "Cosmic Signature"
'Космическая аномалия', // ru: "Cosmic Anomaly"
'Скрытый сигнал', // ru: "Cosmic Signature"
'Anomalie cosmique', // fr: "Cosmic Anomaly"
'Signature cosmique', // fr: "Cosmic Signature"
'宇宙の特異点', // ja: "Cosmic Anomaly"
'宇宙のシグネチャ', // ja: "Cosmic Signature"
'异常空间', // zh: "Cosmic Anomaly"
'空间信号' // zh: "Cosmic Signature"
];
let emptySignatureData = {
id: 0,
name: '',
groupId: 0,
typeId: 0
};
let emptySignatureReaderCounterData = {
added: 0,
changed: 0,
deleted: 0,
deleteCon: 0
};
let editableDefaults = { // xEditable default options for signature fields
url: function(params){
let saveExecutor = (resolve, reject) => {
// only submit params if id (pk) is set
if(params.pk){
let requestData = {};
requestData[params.name] = params.value;
Util.request('PATCH', 'signature', params.pk, requestData)
.then(payload => resolve(payload.data))
.catch(payload => reject(payload.data.jqXHR));
}else{
resolve();
}
};
return new Promise(saveExecutor);
},
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;
};
/**
* 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 = [];
/**
* get option data for a single connection
* @param type
* @param connectionData
* @param systemData
* @returns {{value: *, text: string, metaData: {type: *}}}
*/
let getOption = (type, connectionData, systemData) => {
let text = 'UNKNOWN';
if(type === 'source'){
text = connectionData.sourceAlias + ' - ' + systemData.security;
}else if(type === 'target'){
text = connectionData.targetAlias + ' - ' + systemData.security;
}
let option = {
value: connectionData.id,
text: text,
metaData: {
type: connectionData.type
}
};
return option;
};
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(getOption('target', connectionData, targetSystemData));
}
}else if(systemData.id !== connectionData.source){
let sourceSystemData = MapUtil.getSystemData(mapId, connectionData.source);
if(sourceSystemData){
// take source...
connectionOptions.push(getOption('source', connectionData, sourceSystemData));
}
}
}
}
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 parentElement = $(tableElement).parents('.' + config.moduleTypeClass + ', .' + config.sigReaderDialogClass);
let progressBar = parentElement.find('.progress-bar');
let progressBarLabel = parentElement.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'});
}
}
};
/**
* load existing (current) signature data into info table (preview)
* @param infoTableApi
* @param mapId
* @param systemId
* @param draw
*/
let initTableDataWithCurrentSignatureData = (infoTableApi, mapId, systemId, draw = false) => {
// reset/clear infoTable
infoTableApi.clear();
let primaryTableApi = getDataTableInstance(mapId, systemId, 'primary');
if(primaryTableApi){
infoTableApi.rows.add(primaryTableApi.data().toArray());
if(draw){
infoTableApi.draw();
}
}else{
console.warn('Signature table not found. mapId: %d; systemId: %d', mapId, systemId);
}
};
/**
* set "signature reader" dialog observer
* @param dialogElement
* @param mapId
* @param systemData
*/
let setSignatureReaderDialogObserver = (dialogElement, mapId, systemData) => {
let systemId = systemData.id;
let form = dialogElement.find('form').first();
let textarea = form.find('#' + config.sigInfoTextareaId);
let deleteOutdatedCheckbox = form.find('#' + config.sigReaderLazyUpdateId);
let deleteConnectionsCheckbox = form.find('#' + config.sigReaderConnectionDeleteId);
let errorClipboardValidation = 'No signatures found in scan result';
let tableStatusElement = dialogElement.find('.' + config.tableToolbarStatusClass);
form.initFormValidation({
delay: 0,
feedback: {
success: 'fa-check',
error: 'fa-times'
},
custom: {
clipboard: function(textarea){
let signatureData = parseSignatureString(systemData, textarea.val());
tableStatusElement.text(signatureData.length + ' signatures parsed');
if(signatureData.length === 0){
return errorClipboardValidation;
}
}
}
});
let updatePreviewSection = (mapId, systemId, formData) => {
let infoTableApi = getDataTableInstance(mapId, systemId, 'info');
if(infoTableApi){
// init 'infoTable' with existing signature rows
// infoTableApi.draw() not necessary at this point!
initTableDataWithCurrentSignatureData(infoTableApi, mapId, systemId);
let signatureData = parseSignatureString(systemData, formData.clipboard);
if(signatureData.length > 0){
// valid signature data parsed
// -> add some default data (e.g. currentCharacter data) to parsed signatureData
// -> not required, just for filling up some more columns
signatureData = enrichParsedSignatureData(signatureData);
updateSignatureInfoTable(infoTableApi, signatureData, Boolean(formData.deleteOld), Boolean(formData.deleteConnection));
}else{
// no signatures pasted -> draw current signature rows
infoTableApi.draw();
// reset counter elements
updateSignatureReaderCounters(emptySignatureReaderCounterData);
updateScannedSignaturesBar(infoTableApi, {showNotice: false});
console.info(errorClipboardValidation);
}
}else{
console.warn('Signature "preview" table not found. mapId: %d; systemId: %d', mapId, systemId);
}
};
// changes in 'scan result' textarea -> update preview table --------------------------------------------------
let oldValue = '';
textarea.on('change keyup paste', function(){
let formData = form.getFormValues();
let currentValue = formData.clipboard;
if(currentValue === oldValue){
return; //check to prevent multiple simultaneous triggers
}
oldValue = currentValue;
updatePreviewSection(mapId, systemId, formData);
});
textarea.on('focus', function(e){
this.select();
});
// en/disable 'lazy update' toggles dependent checkbox --------------------------------------------------------
let onDeleteOutdatedCheckboxChange = function(){
deleteConnectionsCheckbox.prop('disabled', !this.checked);
deleteConnectionsCheckbox.prop('checked', false);
}.bind(deleteOutdatedCheckbox[0]);
deleteOutdatedCheckbox.on('change', onDeleteOutdatedCheckboxChange);
onDeleteOutdatedCheckboxChange();
// en/disable checkboxes -> update preview table --------------------------------------------------------------
deleteOutdatedCheckbox.add(deleteConnectionsCheckbox).on('change', function(){
let formData = form.getFormValues();
if(formData.clipboard.length){
updatePreviewSection(mapId, systemId, form.getFormValues());
}
});
// listen 'primary' sig table updates -> update 'preview' sig table in the dialog -----------------------------
dialogElement.on('pf:updateSignatureReaderDialog', function(e){
updatePreviewSection(mapId, systemId, form.getFormValues());
});
};
/**
* open "signature reader" dialog for signature table
* @param systemData
*/
$.fn.showSignatureReaderDialog = function(systemData){
let moduleElement = $(this);
let mapId = moduleElement.data('mapId');
let systemId = moduleElement.data('systemId');
requirejs([
'text!templates/dialog/signature_reader.html',
'text!templates/form/progress.html',
'mustache'
], (TplDialog, TplProgress, Mustache) => {
let data = {
sigInfoId: config.sigInfoId,
sigReaderLazyUpdateId: config.sigReaderLazyUpdateId,
sigReaderConnectionDeleteId: config.sigReaderConnectionDeleteId,
sigInfoTextareaId: config.sigInfoTextareaId,
sigInfoLazyUpdateStatus: getLazyUpdateToggleStatus(moduleElement),
sigInfoCountSigNewId: config.sigInfoCountSigNewId,
sigInfoCountSigChangeId: config.sigInfoCountSigChangeId,
sigInfoCountSigDeleteId: config.sigInfoCountSigDeleteId,
sigInfoCountConDeleteId: config.sigInfoCountConDeleteId,
sigInfoProgressElement : Mustache.render(TplProgress, {
label: true,
wrapperClass: config.moduleHeadlineProgressBarClass,
class: ['progress-bar-success'].join(' '),
percent: 0
})
};
let signatureReaderDialog = bootbox.dialog({
className: config.sigReaderDialogClass,
title: 'Signature reader',
size: 'large',
message: Mustache.render(TplDialog, data),
show: false,
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');
// validate form
form.validator('validate');
// check whether the form is valid
if(form.isValidForm()){
// get form data
let formData = form.getFormValues();
let signatureOptions = {
deleteOld: (formData.deleteOld) ? 1 : 0,
deleteConnection: (formData.deleteConnection) ? 1 : 0
};
let tableApi = getDataTableInstance(mapId, systemId, 'primary');
updateSignatureTableByClipboard(tableApi, systemData, formData.clipboard, signatureOptions);
}else{
return false;
}
}
}
}
});
signatureReaderDialog.on('show.bs.modal', function(e) {
let dialogElement = $(this);
let infoTableApi = drawSignatureTableInfo(this, mapId, systemData);
// init 'infoTable' with existing signature rows
initTableDataWithCurrentSignatureData(infoTableApi, mapId, systemId, true);
updateScannedSignaturesBar(infoTableApi, {showNotice: false});
setSignatureReaderDialogObserver(dialogElement, mapId, systemData);
});
// dialog shown event
signatureReaderDialog.on('shown.bs.modal', function(e){
signatureReaderDialog.initTooltips();
// set focus on sig-input textarea
signatureReaderDialog.find('textarea').focus();
});
// show dialog
signatureReaderDialog.modal('show');
});
};
/**
* Parsed scan result data (from EVE client) should be enriched with some data
* -> fill up more columns in the 'preview' signature tab.e
* @param signatureData
* @returns {*}
*/
let enrichParsedSignatureData = signatureData => {
let characterData = Util.getObjVal(Util.getCurrentUserData(), 'character');
let timestamp = Math.floor((new Date()).getTime() / 1000);
for(let i = 0; i < signatureData.length; i++){
signatureData[i].created = {
created: timestamp,
character: characterData
};
signatureData[i].updated = {
updated: timestamp,
character: characterData
};
}
return signatureData;
};
/**
* 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|\s{4}/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" from description string
let sigDescriptionLowerCase = sigDescription.toLowerCase();
let typeOptions = getAllSignatureNames(
systemData,
systemData.type.id,
Util.getAreaIdBySecurity(systemData.security),
sigGroupId
);
for(let [key, name] of Object.entries(Util.flattenXEditableSelectArray(typeOptions))){
if(name.toLowerCase() === sigDescriptionLowerCase){
typeId = parseInt(key);
break;
}
}
// 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(tableApi.hasProcesses('request')){
console.info('Update signature table By clipboard locked.');
return;
}
let saveSignatureData = signatureData => {
// lock update function until request is finished
let processLockPromise = tableApi.newProcess('lock');
let processRequestPromise = tableApi.newProcess('request');
Util.request(
'POST',
'signature',
[],
{
signatures: signatureData,
deleteOld: options.deleteOld || 0,
deleteConnection: options.deleteConnection || 0,
systemId: parseInt(systemData.id)
},
{
tableApi: tableApi,
processLockPromise: processLockPromise,
processRequestPromise: processRequestPromise
},
context => {
context.tableApi.endProcess(context.processLockPromise);
context.tableApi.endProcess(context.processRequestPromise);
}).then(
payload => {
// updates table with new/updated signature information
updateSignatureTable(payload.context.tableApi, payload.data, !!options.deleteOld);
},
Util.handleAjaxErrorResponse
);
};
// 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 character is not in any system -> id === undefined -> no "confirmation required
let currentLocationData = Util.getCurrentLocationData();
if(
currentLocationData.id &&
currentLocationData.id !== systemData.systemId
){
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 is 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
* @param deleteOptions
*/
let deleteSignatures = (tableApi, rows, deleteOptions = {}) => {
// 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 metaData = getTableMetaData(tableApi);
let data = Object.assign(deleteOptions, {
systemId: metaData.systemId
});
let processRequestPromise = tableApi.newProcess('request');
Util.request('DELETE', 'signature', signatureIds, data, {
tableApi: tableApi,
processRequestPromise: processRequestPromise
},
context => {
context.tableApi.endProcess(context.processRequestPromise);
}).then(
payload => {
let tableApi = payload.context.tableApi;
// promises for all delete rows
let promisesToggleRow = [];
// get deleted rows -> match with response data
let rows = tableApi.rows((idx, rowData, node) => payload.data.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
tableApi.rows(payloads.map(payload => payload.row)).remove().draw();
// update signature bar
updateScannedSignaturesBar(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);
});
},
Util.handleAjaxErrorResponse
);
};
/**
* 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 connectionSelectsSelector = [config.sigTablePrimaryClass, config.sigTableSecondaryClass].map(
tableClass => '.' + tableClass + ' .' + config.tableCellConnectionClass + '.editable'
).join(', ');
let connectionSelects = $(connectionSelectsSelector);
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.cell(cell).index().row, 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 tableApi
* @param cell
*/
let editableConnectionOnShown = (tableApi, cell) => {
$(cell).on('shown', function(e, editable){
let inputField = editable.input.$input;
if(!$(tableApi.table().node()).hasClass(config.sigTablePrimaryClass)){
// we need the primary table API to get selected connections
let metaData = getTableMetaData(tableApi);
tableApi = getDataTableInstance(metaData.mapId, metaData.systemId, 'primary');
}
// Select2 init would work without passing select options as "data", Select2 would grap data from DOM
// -> We want to pass "meta" data for each option into Select2 for formatting
let selectOptions = Util.convertXEditableOptionsToSelect2(editable);
// for better UX, systems that are already linked to a wh signatures should be "disabled"
// -> and grouped into a new <optgroup>
let linkedConnectionIds = tableApi.column('connection:name').data().toArray();
linkedConnectionIds = linkedConnectionIds.filter(id => id > 0);
if(linkedConnectionIds.length){
let groupedSelectOptions = [];
let newSelectOptionGroupDisabled = [];
for(let selectOptionGroup of selectOptions){
if(Array.isArray(selectOptionGroup.children)){
let newSelectOptionGroup = [];
for(let option of selectOptionGroup.children){
if(!option.selected && linkedConnectionIds.includes(option.id)){
// connection already linked -> move to "disabled" group
option.disabled = true;
newSelectOptionGroupDisabled.push(option);
}else{
// connection is available for link
newSelectOptionGroup.push(option);
}
}
if(newSelectOptionGroup.length){
groupedSelectOptions.push({
text: selectOptionGroup.text,
children: newSelectOptionGroup
});
}
}else{
// option has no children -> is prepend (id = 0) option
groupedSelectOptions.push(selectOptionGroup);
}
}
if(newSelectOptionGroupDisabled.length){
groupedSelectOptions.push({
text: 'linked',
children: newSelectOptionGroupDisabled
});
}
selectOptions = groupedSelectOptions;
}
let options = {
data: selectOptions
};
inputField.addClass('pf-select2').initSignatureConnectionSelect(options);
});
};
/**
* 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 HTML for "delete connection" confirmation popover
* @returns {string}
*/
let getConfirmationContent = () => {
let checkOptions = [{
name: 'deleteConnection',
value: '1',
label: 'delete connection',
class: 'pf-editable-warn',
checked: true
}];
let getChecklist = checkOptions => {
let html = '<form class="form-inline editableform popover-content-inner">';
html += '<div class="control-group form-group">';
html += '<div class="editable-input">';
html += '<div class="editable-checklist">';
for(let option of checkOptions){
html += '<div><label>';
html += '<input type="checkbox" name="' + option.name + '" value="' + option.value + '" ';
html += 'class="' + option.class + '" ' + (option.checked ? 'checked' : '') + '>';
html += '<span>' + option.label + '</span>';
html += '</label></div>';
}
html += '</div>';
html += '</div>';
html += '</div>';
html += '</form>';
return html;
};
let html = '';
html += getChecklist(checkOptions);
return html;
};
/**
* 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, config.fontUppercaseClass].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.fontUppercaseClass,
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[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[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[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: true,
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[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(tableApi, 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,
metaData: selected[0].metaData
})).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[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 = {
title: 'Delete signature',
template: Util.getConfirmationTemplate(getConfirmationContent(), {
size: 'small',
noTitle: true
}),
onConfirm: function(e, target){
// top scroll to top
e.preventDefault();
// get form data (check if form tag is not hidden!) from confirmation popover
let tip = target.data('bs.confirmation').tip();
let form = tip.find('form:not(.hidden)').first();
let formData = form.getFormValues();
let deleteOptions = Util.getObjVal(formData, 'deleteConnection') ? formData : {};
// add "processing" state or connection that will be deleted as well
if(deleteOptions.deleteConnection){
let connectionId = tableApi.cell(rowIndex, 'connection:name').data();
if(connectionId){
let metaData = getTableMetaData(tableApi);
let connection = $().getConnectionById(metaData.mapId, connectionId);
if(connection){
connection.addType('state_process');
}
}
}
let deleteRowElement = $(target).parents('tr');
let row = tableApi.rows(deleteRowElement);
deleteSignatures(tableApi, row, deleteOptions);
},
onShow: function(e, target){
// hide "deleteConnection" checkbox if no connectionId linked
let tip = target.data('bs.confirmation').tip();
let form = tip.find('form').first();
let connectionId = tableApi.cell(rowIndex, 'connection:name').data();
form.toggleClass('hidden', !connectionId);
}
};
$(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');
let processLockPromise = null;
let processRequestPromise = null;
// submit all xEditable fields
formFields.editable('submit', {
url: Init.path.api + '/signature',
ajaxOptions: {
processData: false, // we need to "process" data in beforeSend()
type: 'PUT',
dataType: 'json', //assuming json response
contentType: 'application/json',
beforeSend: function(xhr, settings){
settings.data = JSON.stringify(settings.data);
processLockPromise = primaryTableApi.newProcess('lock');
processRequestPromise = primaryTableApi.newProcess('request');
},
context: {
primaryTableApi: primaryTableApi,
secondaryTableApi: secondaryTableApi,
}
},
data: {
systemId: metaData.systemId
},
error: editableDefaults.error, // user default xEditable error function
success: function(response, editableConfig){
let context = editableConfig.ajaxOptions.context;
let primaryTableApi = context.primaryTableApi;
let secondaryTableApi = context.secondaryTableApi;
let signatureData = response[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});
}
primaryTableApi.endProcess(processLockPromise);
primaryTableApi.endProcess(processRequestPromise);
}
});
});
}
}
}
],
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 'info' (preview) table in 'signatureReader' dialog
* @param dialogElement
* @param mapId
* @param systemData
* @returns {jQuery}
*/
let drawSignatureTableInfo = (dialogElement, mapId, systemData) => {
let infoElement = $(dialogElement).find('#' + config.sigInfoId);
let table = $('<table>', {
id: getTableId(mapId, systemData.id, 'info'),
class: ['display', 'compact', 'nowrap', config.sigTableClass, config.sigTableInfoClass].join(' ')
});
infoElement.append(table);
let dataTableOptions = {
tabIndex: -1,
dom: '<"row"<"col-xs-3"l><"col-xs-5 ' + config.tableToolbarStatusClass + '"><"col-xs-4"f>>' +
'<"row"<"col-xs-12"tr>>' +
'<"row"<"col-xs-5"i><"col-xs-7"p>>',
initComplete: function(settings, json){
let tableApi = this.api();
initCharacterInfoTooltip(this, tableApi);
tableApi.columns(['action:name']).visible(false);
Counter.initTableCounter(this, ['created:name', 'updated:name']);
}
};
$.extend(true, dataTableOptions, getSignatureDataTableDefaults(mapId, systemData));
let tableApi = table.DataTable(dataTableOptions);
tableApi.on('draw.dt', function(e, settings){
// xEditable cells should not be editable in this table
$(dialogElement).find('.' + config.sigTableInfoClass).find('td.editable').editable('disable');
});
return tableApi;
};
/**
* 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 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="fas fa-filter"></i>filter';
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);
});
};
/**
* init table undo button
* @param tableApi
*/
let initUndoButton = tableApi => {
let undoButton = tableApi.button('tableTools', 'undo:name').node();
let metaData = getTableMetaData(tableApi);
let getIconByAction = action => {
switch(action){
case 'add': return 'fa-plus txt-color-green';
case 'delete': return 'fa-times txt-color-redDarker';
case 'edit': return 'fa-pen txt-color-orangeDark';
case 'undo': return 'fa-undo txt-color-grayLight';
case 'sync': return 'fa-exchange-alt txt-color-orangeDark';
}
};
undoButton.on('shown', function(e, editable){
// check if history options loaded -> else forward to error function
if(!editable.input.$input.length){
editable.options.error.call(editable, ['No record found']);
}else{
// disable first option
editable.input.$input.first().prop('disabled', true);
// preselect second option
//editable.input.$input.eq(1).prop('checked', true);
// "fake" radio button behaviour
editable.input.$input.attr('name', 'test').attr('type', 'radio');
// preselect second option
editable.input.$input.eq(1).prop('checked', true);
let labels = editable.container.$form.find('label');
labels.addClass('radio');
for(let span of labels.find('span')){
span.style.display = 'inline-block';
span.style.width = '100%';
let parts = span.innerText.trim().split('%%');
parts[0] = '<span style="display: inline-block; width: calc(65% - 38px); text-overflow: ellipsis; overflow: hidden">' + parts[0] + '</span>';
parts[1] = '<span style="display: inline-block; width: 15px; text-align: right"><i class="fas fa-fw txt-color ' + getIconByAction(parts[1]) + '"></i></span>';
parts[2] = '<span style="display: inline-block; width: 23px; text-align: right" title="signature count"><kbd>' + parts[2] + '</kbd></span>';
parts[3] = '<span style="display: inline-block; width: 35%; text-align: right; font-size: 90%" class="txt-color txt-color-grayLight">' + parts[3] + '</span>';
span.innerHTML = parts.join('');
}
labels.initTooltips();
}
});
let processLockPromise = null;
undoButton.editable({
url: Init.path.api + '/signaturehistory',
ajaxOptions: {
processData: false,
type: 'PUT',
dataType: 'json', //assuming json response
contentType: 'application/json',
beforeSend: function(xhr, settings){
processLockPromise = tableApi.newProcess('lock');
},
},
params: function(params){
return JSON.stringify({
systemId: params.pk,
stamp: params.value[0]
});
},
mode: 'popup',
container: 'body',
type: 'checklist',
showbuttons: true,
highlight: false,
title: 'historical records',
name: 'history',
pk: metaData.systemId,
source: Init.path.api + '/signaturehistory/' + metaData.systemId,
sourceOptions: {
type: 'GET',
data: {
mapId: metaData.mapId
}
},
sourceCache: false, // always get new source options on open
display: function(value){
$(this).html('<i class="fas fa-undo"></i>undo');
},
success: function(response, newValue){
// update signature table
tableApi.endProcess(processLockPromise);
updateSignatureTable(tableApi, response, true);
},
error: function(errors){
let errorAll = [];
if(errors && errors.responseText){ //ajax error, errors = xhr object
if(errors.responseJSON && errors.responseJSON.error){
for(let error of errors.responseJSON.error){
errorAll.push(error.message);
}
}else{
//fallback -> other ajax error
errorAll.push(errors.responseText);
}
}else if(errors.length){
// manual called error
errorAll = errors;
let form = this.container.$form.addClass('has-error');
form.find('.editable-buttons').hide();
form.find('.editable-input').hide();
form.find('.editable-error-block').html(errorAll.join('<br>')).show();
}
return errorAll.join(' | ');
},
validate: function(value){
if(!Array.isArray(value) || value.length !== 1){
return {newValue: value, msg: 'No record selected', field: this};
}
}
});
};
/**
* init table selectAll button
* @param tableApi
*/
let initSelectAllButton = tableApi => {
let selectButton = tableApi.button('tableTools', 'selectAll:name').node();
selectButton.on('click', function(){
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);
});
};
/**
* init table delete button
* @param tableApi
*/
let initDeleteButton = tableApi => {
let deleteButton = tableApi.button('tableTools', 'delete:name').node();
deleteButton.on('click', function(){
let selectedRows = getSelectedRows(tableApi);
bootbox.confirm('Delete ' + selectedRows.data().length + ' signature?', function(result){
if(result){
deleteSignatures(tableApi, selectedRows);
}
});
});
};
/**
* init character info Tooltips
* -> e.g. table cell 'question mark' icon
* @param element
* @param tableApi
*/
let initCharacterInfoTooltip = (element, tableApi) => {
element.hoverIntent({
over: function(e){
let cellElement = $(this);
let rowData = tableApi.row(cellElement.parents('tr')).data();
cellElement.addCharacterInfoTooltip(rowData, {
trigger: 'manual',
placement: 'top',
show: true
});
},
out: function(e){
$(this).destroyPopover();
},
selector: 'td.' + Util.config.helpClass
});
};
/**
* 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"fS>>' +
'<"row"<"col-xs-12"tr>>' +
'<"row"<"col-xs-5"i><"col-xs-7"p>>',
buttons: {
name: 'tableTools',
buttons: [
{
name: 'filterGroup',
tag: 'a',
className: config.moduleHeadlineIconClass,
text: '' // set by js (xEditable)
},
{
name: 'undo',
tag: 'a',
className: config.moduleHeadlineIconClass,
text: '' // set by js (xEditable)
},
{
name: 'selectAll',
tag: 'a',
className: config.moduleHeadlineIconClass,
text: '<i class="fas fa-check-double"></i>select all'
},
{
name: 'delete',
tag: 'a',
className: [config.moduleHeadlineIconClass, config.sigTableClearButtonClass].join(' '),
text: '<i class="fas fa-trash"></i>delete&nbsp;(<span>0</span>)'
}
]
},
initComplete: function(settings, json){
let tableApi = this.api();
initCharacterInfoTooltip(this, tableApi);
initGroupFilterButton(tableApi);
initUndoButton(tableApi);
initSelectAllButton(tableApi);
initDeleteButton(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
moduleElement.data('lockPromise', tableApi.newProcess('lock'));
};
/**
* 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){
$(this).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).closest('tr').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: getLazyUpdateToggleStatus(moduleElement),
deleteConnection: 0
};
// "disable" lazy update icon -> prevents accidental removal for next paste #724
getLazyUpdateToggleElement(moduleElement).toggleClass('active', false);
updateSignatureTableByClipboard(e.data.tableApi, systemData, clipboard, signatureOptions);
});
// signature column - "type" popover --------------------------------------------------------------------------
MapUtil.initWormholeInfoTooltip(
moduleElement.find('.' + config.sigTableClass),
'.editable-click:not(.editable-open) span[class^="pf-system-sec-"]'
);
};
/**
* get "lazy delete" toggle element
* @param moduleElement
* @returns {*}
*/
let getLazyUpdateToggleElement = moduleElement => moduleElement.find('.' + config.moduleHeadlineIconLazyClass);
/**
* get status for "lazy delete" toggle
* @param moduleElement
* @returns {number}
*/
let getLazyUpdateToggleStatus = moduleElement => getLazyUpdateToggleElement(moduleElement).hasClass('active') ? 1 : 0;
/**
* update 'counter' UI elements in 'signature reader' dialog
* @param data
*/
let updateSignatureReaderCounters = data => {
let counterElement = $('#' + config.sigInfoCountSigNewId).text(data.added || 0);
counterElement.toggleClass(counterElement.attr('data-class'), Boolean(data.added));
counterElement = $('#' + config.sigInfoCountSigChangeId).text(data.changed || 0);
counterElement.toggleClass(counterElement.attr('data-class'), Boolean(data.changed));
counterElement = $('#' + config.sigInfoCountSigDeleteId).text(data.deleted || 0);
counterElement.toggleClass(counterElement.attr('data-class'), Boolean(data.deleted));
counterElement = $('#' + config.sigInfoCountConDeleteId).text(data.deleteCon || 0);
counterElement.toggleClass(counterElement.attr('data-class'), Boolean(data.deleteCon));
};
/**
* 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;
};
/**
* @param action
* @param rowId
* @returns {Promise<unknown>}
*/
let getPromiseForRow = (action, rowId) => {
return new Promise((resolve, reject) => {
resolve({action: action, rowId: rowId});
});
};
/**
* callback for a changed row
* @param rowIndex
* @param colIndex
* @param tableLoopCount
* @param cellLoopCount
* @param options CUSTOM parameter (not DataTables specific)!
*/
let rowUpdate = function(rowIndex, colIndex, tableLoopCount, cellLoopCount, options){
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', Util.getObjVal(options, 'keepVisible') || false);
}
}else if(node.hasClass(config.tableCellCounterClass)){
// "updated" timestamp always changed
node.pulseBackgroundColor('changed', Util.getObjVal(options, 'keepVisible') || false);
}
};
/**
* update 'info' (preview) signature table (inside 'signature reader' dialog)
* @param tableApi
* @param signaturesDataOrig
* @param deleteOutdatedSignatures
* @param deleteConnections
*/
let updateSignatureInfoTable = (tableApi, signaturesDataOrig, deleteOutdatedSignatures = false, deleteConnections = false) => {
// clone signature array because of further manipulation
let signaturesData = $.extend([], signaturesDataOrig);
let rowIdsExist = [];
let promisesAdded = [];
let promisesChanged = [];
let promisesDeleted = [];
let allRows = tableApi.rows();
let rowUpdateCallback = function(){
rowUpdate.apply(this, [...arguments, {keepVisible: true}]);
};
// update rows ------------------------------------------------------------------------------------------------
allRows.every(function(rowIdx, tableLoop, rowLoop){
let row = this;
let rowData = row.data();
for(let i = 0; i < signaturesData.length; i++){
if(signaturesData[i].name === rowData.name){
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
let newRowData = signaturesData[i];
// keep "description" must not be replaced
newRowData.description = rowData.description;
// existing "groupId" must not be removed
if(!newRowData.groupId){
newRowData.groupId = rowData.groupId;
newRowData.typeId = rowData.typeId;
}else if(newRowData.groupId === rowData.groupId){
if(!newRowData.typeId){
newRowData.typeId = rowData.typeId;
}
}
// "created" timestamp will not change -> use existing
newRowData.created = rowData.created;
row.data(newRowData);
// 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(rowUpdateCallback);
promisesChanged.push(getPromiseForRow('changed', rowId));
}
rowIdsExist.push(rowIdx);
// remove signature data -> all left signatures will be added
signaturesData.splice(i, 1);
i--;
}
}
});
// delete rows ------------------------------------------------------------------------------------------------
if(deleteOutdatedSignatures){
let rows = tableApi.rows((rowIdx, rowData, node) => !rowIdsExist.includes(rowIdx));
rows.every(function(rowIdx, tableLoop, rowLoop){
let row = this;
let rowId = row.id(true);
let rowElement = row.nodes().to$();
let rowData = row.data();
rowElement.pulseBackgroundColor('deleted', true);
promisesChanged.push(getPromiseForRow('deleted', rowId));
// check if there is a connectionId.
if(deleteConnections && Util.getObjVal(rowData, 'connection.id')){
promisesChanged.push(getPromiseForRow('deleteCon', rowId));
}
});
}
// add rows ---------------------------------------------------------------------------------------------------
for(let signatureData of signaturesData){
let row = addSignatureRow(tableApi, signatureData);
let rowElement = row.nodes().to$();
rowElement.pulseBackgroundColor('added', true);
promisesAdded.push(getPromiseForRow('added', row.index()));
}
// done -------------------------------------------------------------------------------------------------------
Promise.all(promisesAdded.concat(promisesChanged, promisesDeleted)).then(payloads => {
if(payloads.length){
// table data changed -> draw() table changes
tableApi.draw();
// no notifications if table was empty just progressbar notification is needed
// sum payloads by "action"
let notificationCounter = payloads.reduce((acc, payload) => {
acc[payload.action]++;
return acc;
}, Object.assign({}, emptySignatureReaderCounterData));
updateSignatureReaderCounters(notificationCounter);
updateScannedSignaturesBar(tableApi, {showNotice: false});
}
});
};
/**
* update signature table with new signatures
* -> add/update/delete rows
* @param tableApi
* @param signaturesDataOrig
* @param deleteOutdatedSignatures
*/
let updateSignatureTable = (tableApi, signaturesDataOrig, deleteOutdatedSignatures = false) => {
if(tableApi.hasProcesses('lock')){
console.info('Signature table locked. Skip table update');
return;
}
// disable tableApi until update finished;
let processLockPromise = tableApi.newProcess('lock');
// 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();
// update signatures ------------------------------------------------------------------------------------------
allRows.every(function(rowIdx, tableLoop, rowLoop){
let row = this;
let rowData = row.data();
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(getPromiseForRow('changed', 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(getPromiseForRow('added', 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;
}, Object.assign({}, emptySignatureReaderCounterData));
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});
// at this point the 'primary' signature table update is done
// we need to check if there is an open 'signature reader' dialog,
// that needs to update its 'preview' signature table
// -> to use DataTables "drawCallback" option or "draw.dt" event is not the *best* option:
// Both are called to frequently (e.g. after filter/sort actions)
$('.' + config.sigReaderDialogClass + '.in').trigger('pf:updateSignatureReaderDialog');
}
// unlock table
tableApi.endProcess(processLockPromise);
});
};
/**
* update signature "history" popover
* @param tableApi
* @param historyData
*/
let updateSignatureHistory = (tableApi, historyData) => {
let tableElement = tableApi.table().node();
$(tableElement).data('history', historyData);
};
/**
* update trigger function for this module
* compare data and update module
* @param moduleElement
* @param systemData
*/
let updateModule = (moduleElement, systemData) => {
if(
systemData.signatures &&
systemData.sigHistory
){
let mapId = moduleElement.data('mapId');
let systemId = moduleElement.data('systemId');
let tableApi = getDataTableInstance(mapId, systemId, 'primary');
updateSignatureTable(tableApi, systemData.signatures, true);
updateSignatureHistory(tableApi, systemData.sigHistory);
}
moduleElement.hideLoadingAnimation();
};
/**
* init callback
* @param moduleElement
* @param mapId
* @param systemData
*/
let initModule = (moduleElement, mapId, systemData) => {
let tableApi = getDataTableInstance(mapId, systemData.id, 'primary');
tableApi.endProcess(moduleElement.data('lockPromise'));
};
/**
* 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');
tableApi.newProcess('lock');
};
/**
* 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
};
});