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

3240 lines
150 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',
'module/base',
'bootbox',
'app/counter',
'app/map/map',
'app/map/util',
'app/ui/form_element'
], ($, Init, Util, BaseModule, bootbox, Counter, Map, MapUtil, FormElement) => {
'use strict';
let SystemSignatureModule = class SystemSignatureModule extends BaseModule {
constructor(config = {}) {
super(Object.assign({}, new.target.defaultConfig, config));
}
/**
* get custom "metaData" from dataTables API
* @param tableApi
* @returns {*}
*/
getTableMetaData(tableApi){
return tableApi ? tableApi.init().pfMeta : null;
}
/**
* get dataTable id
* @param {...string} parts e.g. 'tableType', 'mapId', 'systemId'
* @returns {string}
*/
getTableId(...parts){
return Util.getTableId(this._config.sigTableId, ...parts);
}
/**
* get a dataTableApi instance from global cache
* @param mapId
* @param systemId
* @param tableType
* @returns {*}
*/
getDataTableInstance(mapId, systemId, tableType){
return Util.getDataTableInstance(this._config.sigTableId, mapId, systemId, tableType);
}
/**
* Update/set tooltip for an element
* @param element
* @param title
*/
updateTooltip(element, title){
$(element).attr('title', title.toUpperCase()).tooltip('fixTitle').tooltip('setContent');
}
/**
* get progressbar
* @param progress
* @returns {HTMLDivElement}
*/
newProgressElement(progress = 0){
let progressWrapperEl = document.createElement('div');
progressWrapperEl.classList.add(this._config.moduleHeadlineProgressBarClass);
let progressEl = document.createElement('div');
progressEl.classList.add('progress', 'progress-micro');
let barEl = document.createElement('div');
barEl.classList.add('progress-bar', 'progress-bar-success');
barEl.setAttribute('role', 'progressbar');
barEl.setAttribute('aria-valuenow', progress.toString());
barEl.setAttribute('aria-valuemin', '0');
barEl.setAttribute('aria-valuemax', '100');
barEl.style.width = progress + 'px';
barEl.style.willChange = 'width';
progressEl.append(barEl);
progressWrapperEl.append(progressEl);
return progressWrapperEl;
}
/**
* module header
* @returns {HTMLDivElement}
*/
newHeaderElement(text){
let headEl = super.newHeaderElement(text);
let progressEl = this.newProgressElement();
headEl.append(progressEl);
let progressLabelEl = this.newHeadlineElement('0%');
progressLabelEl.classList.add('progress-label-right');
headEl.append(progressLabelEl);
return headEl;
}
/**
* render module
* @param mapId
* @param systemData
* @returns {HTMLElement}
*/
render(mapId, systemData){
this._systemData = systemData;
this._bodyEl = Object.assign(document.createElement('div'), {
className: this._config.bodyClassName
});
this.moduleElement.append(this._bodyEl);
$(this.moduleElement).showLoadingAnimation();
// draw "new signature" add table
this.drawSignatureTableNew();
// draw signature table
this.drawSignatureTable();
this.setModuleObserver();
return this.moduleElement;
}
/**
* draw signature 'info' (preview) table in 'signatureReader' dialog
* @param dialogElement
* @returns {jQuery}
*/
drawSignatureTableInfo(dialogElement){
let module = this;
let infoElement = $(dialogElement).find('#' + module._config.sigInfoId);
let infoTableEl = document.createElement('table');
infoTableEl.id = module.getTableId('info', module._systemData.mapId, module._systemData.id);
infoTableEl.classList.add('display', 'compact', 'nowrap', module._config.sigTableClass, module._config.sigTableInfoClass);
infoElement.append(infoTableEl);
let dataTableOptions = {
tabIndex: -1,
dom: '<"flex-row flex-between"<"flex-col"l><"flex-col flex-grow ' + module._config.tableToolbarStatusClass + '"><"flex-col"fS>>' +
'<"flex-row"<"flex-col flex-grow"tr>>' +
'<"flex-row flex-between"<"flex-col"i><"flex-col"p>>',
initComplete: function(settings, json){
let tableApi = this.api();
module.initCharacterInfoTooltip(this, tableApi);
tableApi.columns(['action:name']).visible(false);
Counter.initTableCounter(this, ['created:name', 'updated:name']);
}
};
let tableApi = $(infoTableEl).DataTable($.extend(true, dataTableOptions, module.getSignatureDataTableDefaults(module._systemData.mapId, module._systemData)));
tableApi.on('draw.dt', function(e, settings){
// xEditable cells should not be editable in this table
$(dialogElement).find('.' + module._config.sigTableInfoClass).find('td.editable').editable('disable');
});
return tableApi;
}
/**
* draw signature table toolbar (add signature button, scan progress bar
*/
drawSignatureTableNew(){
let module = this;
let secondaryTableWrapperEl = document.createElement('div');
secondaryTableWrapperEl.classList.add(module._config.tableToolsActionClass);
// create "empty table for new signature
let secondaryTableEl = document.createElement('table');
secondaryTableEl.id = module.getTableId('secondary', module._systemData.mapId, module._systemData.id);
secondaryTableEl.classList.add('compact', 'stripe', 'row-border', 'nowrap', module._config.sigTableClass, module._config.sigTableSecondaryClass);
secondaryTableWrapperEl.append(secondaryTableEl);
this._bodyEl.append(secondaryTableWrapperEl);
let dataTableOptions = {
paging: false,
info: false,
searching: false,
tabIndex: -1,
data: [$.extend(true, {}, SystemSignatureModule.emptySignatureData)],
initComplete: function(settings, json){
let tableApi = this.api();
$(this).on('keyup', 'td', {tableApi: tableApi}, function(e){
module.keyNavigation(tableApi, e);
});
}
};
let tableApi = $(secondaryTableEl).DataTable($.extend(true, dataTableOptions, module.getSignatureDataTableDefaults(module._systemData.mapId, module._systemData)));
// "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);
}
/**
* draw empty signature table
*/
drawSignatureTable(){
let module = this;
let primaryTableEl = document.createElement('table');
primaryTableEl.id = module.getTableId('primary', module._systemData.mapId, module._systemData.id);
primaryTableEl.classList.add('display', 'compact', 'nowrap', module._config.sigTableClass, module._config.sigTablePrimaryClass);
this._bodyEl.append(primaryTableEl);
let dataTableOptions = {
select: {
style: 'os',
selector: 'td:not(.' + module._config.tableCellActionClass + ')'
},
tabIndex: -1,
dom: '<"flex-row flex-between"<"flex-col"l><"flex-col flex-grow"B><"flex-col"fS>>' +
'<"flex-row"<"flex-col flex-grow"tr>>' +
'<"flex-row flex-between"<"flex-col"i><"flex-col"p>>',
buttons: {
name: 'tableTools',
buttons: [
{
name: 'filterGroup',
tag: 'a',
className: module._config.moduleHeadlineIconClass,
text: '', // set by js (xEditable)
init: function(tableApi, node, config){
Util.getLocalStore('character').getItem(Util.getCurrentCharacterId()).then(data => {
let prependOptions = [{value: 0, text: 'unknown'}];
let sourceOptions = module._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);
}
node.editable({
mode: 'popup',
type: 'checklist',
showbuttons: false,
onblur: 'submit',
highlight: false,
title: 'filter groups',
value: selectedValues,
prepend: prependOptions,
source: sourceOptions,
inputclass: module._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);
node.on('save', {tableApi: tableApi, sourceOptions: allOptions}, (e, params) => {
// store values local -> IndexDB
Util.getLocalStore('character').setItem(`${Util.getCurrentCharacterId()}.filterSignatureGroups`, params.newValue);
module.searchGroupColumn(e.data.tableApi, params.newValue, e.data.sourceOptions);
});
// set initial search string -> even if table ist currently empty
module.searchGroupColumn(tableApi, selectedValues, allOptions);
});
}
},
{
name: 'undo',
tag: 'a',
className: module._config.moduleHeadlineIconClass,
text: '', // set by js (xEditable)
init: function(tableApi, node, config){
let getIconByAction = action => {
switch(action){
case 'add': return 'fa-plus txt-color-green';
case 'delete': return 'fa-times txt-color-redDark';
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';
}
};
node.on('shown', (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;
node.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',
type: 'checklist',
showbuttons: true,
highlight: false,
title: 'historical records',
name: 'history',
pk: module._systemData.id,
source: Init.path.api + '/SignatureHistory/' + module._systemData.id,
sourceOptions: {
type: 'GET',
data: {
mapId: module._systemData.mapId
}
},
sourceCache: false, // always get new source options on open
display: function(value){
$(this).html('<i class="fas fa-undo"></i>undo');
},
success: (response, newValue) => {
// update signature table
tableApi.endProcess(processLockPromise);
module.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};
}
}
});
}
},
{
name: 'selectAll',
tag: 'a',
className: module._config.moduleHeadlineIconClass,
text: '<i class="fas fa-check-double"></i>select all',
action: function(e, tableApi, node, config){
let rows = tableApi.rows();
let rowCountAll = rows.count();
let rowCountSelected = tableApi.rows({selected: true}).count();
if(rowCountSelected && (rowCountSelected >= rowCountAll)){
rows.deselect();
node.removeClass('active');
}else{
rows.select();
node.addClass('active');
}
}
},
{
extend: 'selected',
name: 'delete',
tag: 'a',
className: [module._config.moduleHeadlineIconClass, module._config.sigTableClearButtonClass].join(' '),
text: '<i class="fas fa-trash"></i>delete&nbsp;(<span>0</span>)',
init: function(tableApi, node, config){
// call `super` init() for "extend: 'selected'" button
$.fn.dataTable.ext.buttons.selected.init.call(this, tableApi, node, config);
tableApi.on('select deselect', (e, tableApi, type, indexes) => {
let rowCountAll = tableApi.rows().count();
let rowCountSelected = tableApi.rows({selected: true}).count();
let countText = (rowCountSelected >= rowCountAll) ? 'all' : rowCountSelected;
node.find('i+span').text(countText);
});
},
action: function(e, tableApi, node, config){
let selectedRows = tableApi.rows({selected: true});
if(selectedRows.count()){
bootbox.confirm('Delete ' + selectedRows.count() + ' signature?', result => {
if(result){
// for some reason using 'tableApi' as first param in deleteSignature()
// does not work because my custom plugin fkt 'newProcess()' is missing here...
// -> using 'this' seems to work...
module.deleteSignatures(this, selectedRows);
}
});
}
}
}
]
},
initComplete: function(settings, json){
let tableApi = this.api();
module.initCharacterInfoTooltip(this, tableApi);
$(this).on('keyup', 'td', {tableApi: tableApi}, function(e){
module.keyNavigation(tableApi, e);
});
Counter.initTableCounter(this, ['created:name', 'updated:name']);
}
};
let tableApi = $(primaryTableEl).DataTable($.extend(true, dataTableOptions, module.getSignatureDataTableDefaults(module._systemData.mapId, module._systemData)));
// "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);
// "Select" Datatables Plugin
tableApi.select();
let buttons = new $.fn.dataTable.Buttons(tableApi, {
dom: {
container: {
tag: 'h5',
className: 'pull-right'
},
button: {
tag: 'i',
className: ['fas', 'fa-fw', module._config.moduleHeadlineIconClass].join(' '),
},
buttonLiner: {
tag: null
}
},
name: 'moduleTools',
buttons: [
{
name: 'add',
className: ['fa-plus', module._config.moduleHeadlineIconAddClass].join(' '),
titleAttr: 'add',
attr: {
'data-toggle': 'tooltip',
'data-html': true
},
action: function(e, tableApi, node, config){
module.toggleAddSignature('auto');
}
},
{
name: 'reader',
className: ['fa-paste', module._config.moduleHeadlineIconReaderClass].join(' '),
titleAttr: 'signature reader',
attr: {
'data-toggle': 'tooltip',
'data-html': true
},
action: function(e, tableApi, node, config){
module.showSignatureReaderDialog(tableApi);
}
},
{
name: 'lazy',
className: ['fa-exchange-alt', module._config.moduleHeadlineIconLazyClass].join(' '),
titleAttr: 'lazy \'delete\' signatures',
attr: {
'data-toggle': 'tooltip',
'data-html': true
},
action: function(e, tableApi, node, config){
$(node).toggleClass('active');
}
}
]
});
tableApi.buttons('moduleTools', null).container().appendTo(module.moduleElement.querySelector('.' + module._config.headClassName));
// lock table until module is fully rendered
$(module.moduleElement).data('lockPromise', tableApi.newProcess('lock'));
}
/**
* get dataTables default options for signature tables
* @param mapId
* @param systemData
* @returns {{}}
*/
getSignatureDataTableDefaults(mapId, systemData){
let module = this;
/**
* 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 => module._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: [module._config.tableCellFocusClass, module._config.sigTableEditSigNameInput, module._config.fontUppercaseClass].join(' '),
data: 'name',
createdCell: function(cell, cellData, rowData, rowIndex, colIndex){
let tableApi = this.api();
module.updateTooltip(cell, cellData);
module.editableOnSave(tableApi, cell, [], ['group:name', 'type:name', 'action:name']);
module.editableOnHidden(tableApi, cell);
$(cell).editable($.extend({
mode: 'popup',
type: 'text',
title: 'signature id',
name: 'name',
pk: rowData.id || null,
emptytext: '? ? ?',
value: cellData,
inputclass: module._config.fontUppercaseClass,
display: function(value){
// change display value to first 3 chars -> unicode beware
$(this).text([...$.trim(value)].slice(0, 3).join('').toLowerCase());
},
validate: function(value){
let msg = false;
//let mbLength = [...$.trim(value)].length; // unicode beware
if(! value.trimChars().match(/^[a-zA-Z]{3}-\d{3}$/)){
msg = 'ID format invalid. E.g.: ABC-123';
}
if(msg){
return {newValue: value, msg: msg, field: this};
}
},
params: modifyFieldParamsOnSend,
success: function(response, newValue){
tableApi.cell(cell).data(newValue);
$(this).pulseBackgroundColor('changed');
module.updateTooltip(cell, newValue);
if(response){
let newRowData = response[0];
module.updateSignatureCell(tableApi, rowIndex, 'status:name', newRowData.updated);
module.updateSignatureCell(tableApi, rowIndex, 'updated:name', newRowData.updated.updated);
}
tableApi.draw();
}
}, SystemSignatureModule.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: [module._config.tableCellFocusClass].join(' '),
data: 'groupId',
render: {
sort: module.getGroupLabelById.bind(module),
filter: module.getGroupLabelById.bind(module)
},
createdCell: function(cell, cellData, rowData, rowIndex, colIndex){
let tableApi = this.api();
module.editableOnSave(tableApi, cell, ['type:name'], ['type:name', 'action:name']);
module.editableOnHidden(tableApi, cell);
module.editableGroupOnShown(cell);
module.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: module._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];
module.updateSignatureCell(tableApi, rowIndex, 'status:name', newRowData.updated);
module.updateSignatureCell(tableApi, rowIndex, 'updated:name', newRowData.updated.updated);
}
tableApi.draw();
// find related "type" select (same row) and change options ---------------------------
let signatureTypeCell = module.getNeighboringCell(tableApi, cell, 'type:name');
let signatureTypeField = signatureTypeCell.nodes().to$();
module.editableSelectCheck(signatureTypeField);
signatureTypeCell.data(0);
signatureTypeField.editable('setValue', 0);
// find "connection" select (same row) and change "enabled" flag ----------------------
let signatureConnectionCell = module.getNeighboringCell(tableApi, cell, 'connection:name');
let signatureConnectionField = signatureConnectionCell.nodes().to$();
if(newValue === 5){
// wormhole
module.editableEnable(signatureConnectionField);
}else{
module.checkConnectionConflicts();
module.editableDisable(signatureConnectionField);
}
signatureConnectionCell.data(0);
signatureConnectionField.editable('setValue', 0);
}
}, SystemSignatureModule.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: [module._config.tableCellFocusClass, module._config.tableCellTypeClass].join(' '),
data: 'typeId',
createdCell: function(cell, cellData, rowData, rowIndex, colIndex){
let tableApi = this.api();
module.editableOnSave(tableApi, cell, ['connection:name'], ['action:name']);
module.editableOnHidden(tableApi, cell);
module.editableTypeOnInit(cell);
module.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();
return SystemSignatureModule.getSignatureTypeOptions(
systemData.type.id,
Util.getAreaIdBySecurity(systemData.security),
rowData.groupId,
systemData
);
},
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}, undefined, {showWhSizeLabel: true}));
}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];
module.updateSignatureCell(tableApi, rowIndex, 'status:name', newRowData.updated);
module.updateSignatureCell(tableApi, rowIndex, 'updated:name', newRowData.updated.updated);
}
tableApi.draw();
}
}, SystemSignatureModule.editableDefaults));
}
},{
targets: 4,
name: 'description',
orderable: false,
searchable: true,
title: 'description',
class: [module._config.tableCellFocusClass, module._config.tableCellActionClass].join(' '),
type: 'html',
data: 'description',
defaultContent: '',
createdCell: function(cell, cellData, rowData, rowIndex, colIndex){
let tableApi = this.api();
module.editableOnSave(tableApi, cell, [], ['action:name']);
module.editableOnHidden(tableApi, cell);
module.editableDescriptionOnShown(cell);
module.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: module._config.editableDescriptionInputClass,
emptyclass: module._config.moduleHeadlineIconClass,
params: modifyFieldParamsOnSend,
success: function(response, newValue){
tableApi.cell(cell).data(newValue);
$(this).pulseBackgroundColor('changed');
if(response){
let newRowData = response[0];
module.updateSignatureCell(tableApi, rowIndex, 'status:name', newRowData.updated);
module.updateSignatureCell(tableApi, rowIndex, 'updated:name', newRowData.updated.updated);
}
tableApi.draw();
}
}, SystemSignatureModule.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: [module._config.tableCellFocusClass, module._config.tableCellConnectionClass].join(' '),
width: 80,
data: 'connection.id',
defaultContent: 0,
createdCell: function(cell, cellData, rowData, rowIndex, colIndex){
let tableApi = this.api();
module.editableOnSave(tableApi, cell, [], ['action:name']);
module.editableOnHidden(tableApi, cell);
module.editableConnectionOnInit(cell);
module.editableConnectionOnShown(tableApi, cell);
module.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');
return SystemSignatureModule.getSignatureConnectionOptions(mapId, systemData);
},
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){
// 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];
module.updateSignatureCell(tableApi, rowIndex, 'status:name', newRowData.updated);
module.updateSignatureCell(tableApi, rowIndex, 'updated:name', newRowData.updated.updated);
}
tableApi.draw();
}
}, SystemSignatureModule.editableDefaults));
}
},{
targets: 6,
name: 'created',
title: 'created',
searchable: false,
width: 80,
className: ['text-right', module._config.tableCellCounterClass, 'min-screen-d'].join(' '),
data: 'created.created',
defaultContent: '',
},{
targets: 7,
name: 'updated',
title: 'updated',
searchable: false,
width: 80,
className: ['text-right', module._config.tableCellCounterClass, 'min-screen-d'].join(' '),
data: 'updated.updated',
defaultContent: ''
},{
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', module._config.tableCellFocusClass, module._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-redDark"></i>';
}
return val;
}
},
createdCell: function(cell, cellData, rowData, rowIndex, colIndex){
let tableApi = this.api();
if(rowData.id){
// delete signature -----------------------------------------------------------------------
let confirmationSettings = {
title: '---',
template: Util.getConfirmationTemplate(Util.getConfirmationContent([{
name: 'deleteConnection',
value: '1',
label: 'delete connection',
class: 'pf-editable-warn',
checked: true
}]), {
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 = module.getTableMetaData(tableApi);
let connection = $().getConnectionById(metaData.mapId, connectionId);
if(connection){
connection.addType('state_process');
}
}
}
let deleteRowElement = $(target).parents('tr');
let row = tableApi.rows(deleteRowElement);
module.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 = module.getTableMetaData(secondaryTableApi);
let primaryTableApi = module.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: SystemSignatureModule.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 = module.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, {}, SystemSignatureModule.emptySignatureData)).draw();
Util.showNotify({
title: 'Signature added',
text: 'Name: ' + signatureData.name,
type: 'success'
});
// update signature bar
module.updateScannedSignaturesBar(primaryTableApi, {showNotice: true});
}
primaryTableApi.endProcess(processLockPromise);
primaryTableApi.endProcess(processRequestPromise);
}
});
});
}
}
}
],
createdRow: function(row, data, dataIndex){
// enable tabbing for interactive cells
let focusCells = $(row).find('.' + module._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');
}
});
},
rowCallback: function(){
let tableApi = this.api();
let time = Math.floor((new Date()).getTime());
tableApi.cells(null, ['updated:name']).every(function(rowIndex, colIndex, tableLoopCount, cellLoopCount){
let cell = this;
let node = cell.node();
let cellData = cell.data();
let diff = time - cellData * 1000;
// highlight cell: age > 1 day
$(node).toggleClass('txt-color txt-color-warning', diff > 86400000);
});
}
};
return dataTableDefaults;
}
/**
* toggle primary table visibility
* @param show
*/
toggleAddSignature(show = 'auto'){
let button = $(this.moduleElement).find('.' + this._config.moduleHeadlineIconAddClass);
let toolsElement = $(this.moduleElement).find('.' + this._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: () => {
this.focusNewSignatureEditableField();
}
});
}else if(toolsElement.is(':visible') && show){
// still visible -> no animation
this.focusNewSignatureEditableField();
}
}
/**
* filter table "group" column
* @param tableApi
* @param newValue
* @param sourceOptions
*/
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 character info tooltips
* -> e.g. table cell 'question mark' icon
* @param element
* @param tableApi
*/
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
});
}
/**
* 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 {*}
*/
enrichParsedSignatureData(signatureData){
let characterData = Util.getCurrentCharacter();
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 clipboard
* @returns {Array}
*/
parseSignatureString(clipboard){
let signatureData = [];
if(clipboard.length){
let signatureRows = clipboard.split(/\r\n|\r|\n/g);
let signatureGroupOptions = this._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(SystemSignatureModule.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 = SystemSignatureModule.getSignatureTypeOptions(
this._systemData.type.id,
Util.getAreaIdBySecurity(this._systemData.security),
sigGroupId,
this._systemData
);
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: this._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 clipboard data stream
* @param options
*/
updateSignatureTableByClipboard(tableApi, 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(this._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
this.updateSignatureTable(payload.context.tableApi, payload.data, !!options.deleteOld);
},
Util.handleAjaxErrorResponse
);
};
// parse input stream
let signatureData = this.parseSignatureString(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 !== this._systemData.systemId
){
let systemNameStr = (this._systemData.name === this._systemData.alias) ?
'"' + this._systemData.name + '"' :
'"' + this._systemData.alias + '" (' + this._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, 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
*/
deleteSignatures(tableApi, rows, deleteOptions = {}){
let module = this;
// 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 = module.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(module.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
module.updateScannedSignaturesBar(tableApi, {showNotice: false});
// update connection conflicts
module.checkConnectionConflicts();
let notificationOptions = {
type: 'success'
};
if (payloads.length === 1) {
notificationOptions.title = 'Signature deleted';
} else {
notificationOptions.title = payloads.length + ' Signatures deleted ';
}
module.showNotify(notificationOptions);
});
},
Util.handleAjaxErrorResponse
);
}
/**
* updates a single cell with new data (e.g. "updated" cell)
* @param tableApi
* @param rowIndex
* @param columnSelector
* @param data
*/
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
*/
checkConnectionConflicts(){
setTimeout(() => {
let connectionSelectsSelector = [this._config.sigTablePrimaryClass, this._config.sigTableSecondaryClass].map(
tableClass => '.' + tableClass + ' .' + this._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}
*/
getGroupLabelById(groupId){
let options = this._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 {*}
*/
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 {*}
*/
searchNextCell(tableApi, cell, columnSelectors){
if(columnSelectors.length){
// copy selectors -> .shift() modifies the orig array, important!
columnSelectors = columnSelectors.slice(0);
let nextCell = this.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 this.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
*/
activateCell(cell){
let cellElement = cell.nodes().to$();
// check if cell is visible and not e.g. immediately filtered out by a search filter
// -> https://github.com/exodus4d/pathfinder/issues/865
if(cellElement.is(':visible')){
// 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
*/
activateNextCell(tableApi, cell, columnSelectors){
let nextCell = this.searchNextCell(tableApi, cell, columnSelectors);
this.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)
*/
editableOnSave(tableApi, cell, columnSelectorsAjax = [], columnSelectorsDry = []){
$(cell).on('save', (e, params) => {
if(params.response){
// send by Ajax
this.activateNextCell(tableApi, cell, columnSelectorsAjax);
}else{
// dry save - no request
this.activateNextCell(tableApi, cell, columnSelectorsDry);
}
});
}
/**
* helper function - set 'hidden' observer for xEditable cell
* -> set focus() on xEditable field
* @param tableApi
* @param cell
*/
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
*/
editableGroupOnShown(cell){
$(cell).on('shown', (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
*/
editableGroupOnSave(tableApi, cell){
$(cell).on('save', (e, params) => {
if(params.response){
// send by Ajax
this.updateScannedSignaturesBar(tableApi, {showNotice: true});
}
});
}
/**
* helper function - set 'init' observer for xEditable type cell
* -> disable xEditable field if no options available
* @param cell
*/
editableTypeOnInit(cell){
$(cell).on('init', (e, editable) => {
if(!editable.options.source().length){
this.editableDisable($(e.target));
}
});
}
/**
* helper function - set 'shown' observer for xEditable type cell
* -> enable Select2 for xEditable form
* @param cell
*/
editableTypeOnShown(cell){
$(cell).on('shown', (e, editable) => {
// destroy possible open popovers (e.g. wormhole types)
$(e.target).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
*/
editableDescriptionOnShown(cell){
$(cell).on('shown', (e, editable) => {
$(e.target).parents('.' + this._config.tableToolsActionClass).css('height', '+=35px');
});
}
/**
* helper function - set 'hidden' observer for xEditable description cell
* -> change height for "new signature" table wrapper
* @param cell
*/
editableDescriptionOnHidden(cell){
$(cell).on('hidden', (e, editable) => {
$(cell).parents('.' + this._config.tableToolsActionClass).css('height', '-=35px');
});
}
/**
* helper function - set 'init' observer for xEditable connection cell
* -> set focus() on xEditable field
* @param cell
*/
editableConnectionOnInit(cell){
$(cell).on('init', (e, editable) => {
if(editable.value > 0){
// empty connection selects ON INIT don´t make a difference for conflicts
this.checkConnectionConflicts();
}
});
}
/**
* helper function - set 'shown' observer for xEditable connection cell
* -> enable Select2 for xEditable form
* @param tableApi
* @param cell
*/
editableConnectionOnShown(tableApi, cell){
$(cell).on('shown', (e, editable) => {
let inputField = editable.input.$input;
if(!$(tableApi.table().node()).hasClass(this._config.sigTablePrimaryClass)){
// we need the primary table API to get selected connections
let metaData = this.getTableMetaData(tableApi);
tableApi = this.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
*/
editableConnectionOnSave(cell){
$(cell).on('save', (e, params) => {
this.checkConnectionConflicts();
});
}
/**
* enable xEditable element
* @param element
*/
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
*/
editableDisable(element){
element.editable('disable');
// xEditable sets 'tabindex = -1'
}
/**
* en/disables xEditable element (select)
* -> disables if there are no source options found
* @param element
*/
editableSelectCheck(element){
if(element.data('editable')){
let options = element.data('editable').options.source();
if(options.length > 0){
this.editableEnable(element);
}else{
this.editableDisable(element);
}
}
}
/**
* open xEditable input field in "new Signature" table
*/
focusNewSignatureEditableField(){
$(this.moduleElement).find('.' + this._config.sigTableSecondaryClass)
.find('td.' + this._config.sigTableEditSigNameInput).editable('show');
}
/**
* set module observer
*/
setModuleObserver(){
// add signature toggle
$(this.moduleElement).on('pf:showSystemSignatureModuleAddNew', e => {
this.toggleAddSignature(true);
});
// event listener for global "paste" signatures into the page
$(this.moduleElement).on('pf:updateSystemSignatureModuleByClipboard', (e, clipboard) => {
let signatureOptions = {
deleteOld: this.getLazyUpdateToggleStatus(),
deleteConnection: 0
};
// "disable" lazy update icon -> prevents accidental removal for next paste #724
$(this.getLazyUpdateToggleElement()).toggleClass('active', false);
this.updateSignatureTableByClipboard(
this.getDataTableInstance(this._systemData.mapId, this._systemData.id, 'primary'),
clipboard,
signatureOptions
);
});
// signature column - "type" popover
MapUtil.initWormholeInfoTooltip(
$(this.moduleElement).find('.' + this._config.sigTableClass),
'.editable-click:not(.editable-open) span[class^="pf-system-sec-"]'
);
// init tooltips
$(this.moduleElement).initTooltips();
}
/**
* key (arrow) navigation inside a table -> set cell focus()
* @param tableApi
* @param e
*/
keyNavigation(tableApi, e){
let offset;
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(Array.isArray(offset)){
/**
* 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();
}
}
/**
* show/hides a table <tr> rowElement
* @param rowElement
*/
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(Counter.config.counterStopClass)
.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
*/
updateScannedSignaturesBar(tableApi, options){
let tableElement = tableApi.table().node();
let parentElement = $(tableElement).parents('.' + this._config.className + ', .' + this._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){
this.showNotify({title: 'Unscanned signatures', text: notification, type: 'info'});
}else{
this.showNotify({title: 'System is scanned', text: notification, type: 'success'});
}
}
}
/**
* load existing (current) signature data into info table (preview)
* @param infoTableApi
* @param draw
*/
initTableDataWithCurrentSignatureData(infoTableApi, draw = false){
// reset/clear infoTable
infoTableApi.clear();
let primaryTableApi = this.getDataTableInstance(this._systemData.mapId, this._systemData.id, 'primary');
if(primaryTableApi){
infoTableApi.rows.add(primaryTableApi.data().toArray());
if(draw){
infoTableApi.draw();
}
}else{
console.warn('Signature table not found. mapId: %d; systemId: %d',this._systemData.mapId, this._systemData.id);
}
}
/**
* set "signature reader" dialog observer
* @param dialogElement
*/
setSignatureReaderDialogObserver(dialogElement){
dialogElement = $(dialogElement);
let form = dialogElement.find('form').first();
let textarea = form.find('#' + this._config.sigInfoTextareaId);
let deleteOutdatedCheckbox = form.find('#' + this._config.sigReaderLazyUpdateId);
let deleteConnectionsCheckbox = form.find('#' + this._config.sigReaderConnectionDeleteId);
let errorClipboardValidation = 'No signatures found in scan result';
let tableStatusElement = dialogElement.find('.' + this._config.tableToolbarStatusClass);
form.initFormValidation({
delay: 0,
feedback: {
success: 'fa-check',
error: 'fa-times'
},
custom: {
clipboard: textarea => {
let signatureData = this.parseSignatureString(textarea.val());
tableStatusElement.text(signatureData.length + ' signatures parsed');
if(signatureData.length === 0){
return errorClipboardValidation;
}
}
}
});
let updatePreviewSection = (formData) => {
let infoTableApi = this.getDataTableInstance(this._systemData.mapId, this._systemData.id, 'info');
if(infoTableApi){
// init 'infoTable' with existing signature rows
// infoTableApi.draw() not necessary at this point!
this.initTableDataWithCurrentSignatureData(infoTableApi);
let signatureData = this.parseSignatureString(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 = this.enrichParsedSignatureData(signatureData);
this.updateSignatureInfoTable(infoTableApi, signatureData, Boolean(formData.deleteOld), Boolean(formData.deleteConnection));
}else{
// no signatures pasted -> draw current signature rows
infoTableApi.draw();
// reset counter elements
this.updateSignatureReaderCounters(SystemSignatureModule.emptySignatureReaderCounterData);
this.updateScannedSignaturesBar(infoTableApi, {showNotice: false});
console.info(errorClipboardValidation);
}
}else{
console.warn('Signature "preview" table not found. mapId: %d; systemId: %d', this._systemData.mapId, this._systemData.id);
}
};
// changes in 'scan result' textarea -> update preview table --------------------------------------------------
let oldValue = '';
textarea.on('change keyup paste', () => {
let formData = form.getFormValues();
let currentValue = formData.clipboard;
if(currentValue === oldValue){
return; //check to prevent multiple simultaneous triggers
}
oldValue = currentValue;
updatePreviewSection(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', () => {
let formData = form.getFormValues();
if(formData.clipboard.length){
updatePreviewSection(form.getFormValues());
}
});
// listen 'primary' sig table updates -> update 'preview' sig table in the dialog -----------------------------
dialogElement.on('pf:updateSignatureReaderDialog', () => {
updatePreviewSection(form.getFormValues());
});
}
/**
* open "signature reader" dialog for signature table
* @param tableApi
*/
showSignatureReaderDialog(tableApi){
requirejs([
'text!templates/dialog/signature_reader.html',
'mustache'
], (TplDialog, Mustache) => {
let data = {
sigInfoId: this._config.sigInfoId,
sigReaderLazyUpdateId: this._config.sigReaderLazyUpdateId,
sigReaderConnectionDeleteId: this._config.sigReaderConnectionDeleteId,
sigInfoTextareaId: this._config.sigInfoTextareaId,
sigInfoLazyUpdateStatus: this.getLazyUpdateToggleStatus(),
sigInfoCountSigNewId: this._config.sigInfoCountSigNewId,
sigInfoCountSigChangeId: this._config.sigInfoCountSigChangeId,
sigInfoCountSigDeleteId: this._config.sigInfoCountSigDeleteId,
sigInfoCountConDeleteId: this._config.sigInfoCountConDeleteId,
sigInfoProgressElement: this.newProgressElement().outerHTML
};
let signatureReaderDialog = bootbox.dialog({
className: this._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: e => {
let form = $(e.delegateTarget).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
};
this.updateSignatureTableByClipboard(tableApi, formData.clipboard, signatureOptions);
}else{
return false;
}
}
}
}
});
signatureReaderDialog.on('show.bs.modal', e => {
let infoTableApi = this.drawSignatureTableInfo(e.target);
// init 'infoTable' with existing signature rows
this.initTableDataWithCurrentSignatureData(infoTableApi, true);
this.updateScannedSignaturesBar(infoTableApi, {showNotice: false});
this.setSignatureReaderDialogObserver(e.target);
});
// dialog shown event
signatureReaderDialog.on('shown.bs.modal', e => {
signatureReaderDialog.initTooltips();
// set focus on sig-input textarea
signatureReaderDialog.find('textarea').focus();
});
// show dialog
signatureReaderDialog.modal('show');
});
}
/**
* get "lazy delete" toggle element
* @returns {*}
*/
getLazyUpdateToggleElement(){
return this.moduleElement.querySelector('.' + this._config.moduleHeadlineIconLazyClass);
}
/**
* get status for "lazy delete" toggle
* @returns {number}
*/
getLazyUpdateToggleStatus(){
return this.getLazyUpdateToggleElement().classList.contains('active') ? 1 : 0;
}
/**
* update 'counter' UI elements in 'signature reader' dialog
* @param data
*/
updateSignatureReaderCounters(data){
let counterElement = $('#' + this._config.sigInfoCountSigNewId).text(data.added || 0);
counterElement.toggleClass(counterElement.attr('data-class'), Boolean(data.added));
counterElement = $('#' + this._config.sigInfoCountSigChangeId).text(data.changed || 0);
counterElement.toggleClass(counterElement.attr('data-class'), Boolean(data.changed));
counterElement = $('#' + this._config.sigInfoCountSigDeleteId).text(data.deleted || 0);
counterElement.toggleClass(counterElement.attr('data-class'), Boolean(data.deleted));
counterElement = $('#' + this._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 {*}
*/
addSignatureRow(tableApi, signatureData){
return tableApi ? tableApi.row.add(signatureData) : null;
}
/**
* @param action
* @param rowId
* @returns {Promise}
*/
getPromiseForRow(action, rowId){
return new Promise(resolve => {
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)!
*/
rowUpdate(rowIndex, colIndex, tableLoopCount, cellLoopCount, options){
let cell = Util.getObjVal(options, 'cell');
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
this.editableSelectCheck(node);
break;
case 'connectionId':
// disables if no wormhole group set
let groupId = cell.cell(rowIndex, 'group:name').data();
if(groupId === 5){
// wormhole
this.editableEnable(node);
}else{
this.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(this._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
*/
updateSignatureInfoTable(tableApi, signaturesDataOrig, deleteOutdatedSignatures = false, deleteConnections = false){
let module = this;
// 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(){
module.rowUpdate(...arguments, {cell: this, 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(module.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(module.getPromiseForRow('deleted', rowId));
// check if there is a connectionId.
if(deleteConnections && Util.getObjVal(rowData, 'connection.id')){
promisesChanged.push(module.getPromiseForRow('deleteCon', rowId));
}
});
}
// add rows ---------------------------------------------------------------------------------------------------
for(let signatureData of signaturesData){
let row = module.addSignatureRow(tableApi, signatureData);
let rowElement = row.nodes().to$();
rowElement.pulseBackgroundColor('added', true);
promisesAdded.push(module.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({}, SystemSignatureModule.emptySignatureReaderCounterData));
module.updateSignatureReaderCounters(notificationCounter);
module.updateScannedSignaturesBar(tableApi, {showNotice: false});
}
});
}
/**
* update signature table with new signatures
* -> add/update/delete rows
* @param tableApi
* @param signaturesDataOrig
* @param deleteOutdatedSignatures
*/
updateSignatureTable(tableApi, signaturesDataOrig, deleteOutdatedSignatures = false){
let module = this;
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();
let rowUpdateCallback = function(){
module.rowUpdate(...arguments, {cell: this});
};
// 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(rowUpdateCallback);
promisesChanged.push(module.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('#' + module._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) => {
module.toggleTableRow(rowElement).then(payload => resolve({action: 'deleted', rowIdx: rowId}));
}));
}).remove();
}
// add new signatures -----------------------------------------------------------------------------------------
for(let signatureData of signaturesData){
let row = module.addSignatureRow(tableApi, signatureData);
let rowId = row.id(true);
let rowElement = row.nodes().to$();
rowElement.pulseBackgroundColor('added');
promisesAdded.push(module.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
module.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({}, SystemSignatureModule.emptySignatureReaderCounterData));
let notification = Object.keys(notificationCounter).reduce((acc, key) => {
return `${acc}${notificationCounter[key] ? `${notificationCounter[key]} ${key}<br>` : ''}`;
}, '');
if(notification.length){
Util.showNotify({title: 'Signatures updated', text: notification, type: 'success', textTrusted: true});
}
}
module.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)
$('.' + module._config.sigReaderDialogClass + '.in').trigger('pf:updateSignatureReaderDialog');
}
// unlock table
tableApi.endProcess(processLockPromise);
});
}
/**
* update signature "history" popover
* @param tableApi
* @param historyData
*/
updateSignatureHistory(tableApi, historyData){
let tableElement = tableApi.table().node();
$(tableElement).data('history', historyData);
}
/**
* update module
* compare data and update module
* @param systemData
* @returns {Promise}
*/
update(systemData){
return super.update(systemData).then(systemData => new Promise(resolve => {
if(
Util.getObjVal(systemData, 'id') === this._systemData.id &&
Util.getObjVal(systemData, 'mapId') === this._systemData.mapId &&
Util.getObjVal(systemData, 'signatures') &&
Util.getObjVal(systemData, 'sigHistory')
){
let tableApi = this.getDataTableInstance(systemData.mapId, systemData.id, 'primary');
this.updateSignatureTable(tableApi, systemData.signatures, true);
this.updateSignatureHistory(tableApi, systemData.sigHistory);
}
$(this.moduleElement).hideLoadingAnimation();
resolve({
action: 'update',
data: {
module: this
}
});
}));
}
/**
* init module
*/
init(){
super.init();
let tableApi = this.getDataTableInstance(this._systemData.mapId, this._systemData.id, 'primary');
tableApi.endProcess($(this.moduleElement).data('lockPromise'));
}
/**
* before module hide callback
*/
beforeHide(){
super.beforeHide();
// disable update
this.getDataTableInstance(this._systemData.mapId, this._systemData.id, 'primary').newProcess('lock');
}
/**
* get all signature types that can exist for a system (jQuery obj)
* @param systemElement
* @param groupId
* @returns {*[]|*}
*/
static getSignatureTypeOptionsBySystem(systemElement, groupId){
let systemTypeId = systemElement.data('typeId');
let areaId = Util.getAreaIdBySecurity(systemElement.data('security'));
let systemData = {statics: systemElement.data('statics')};
return SystemSignatureModule.getSignatureTypeOptions(systemTypeId, areaId, groupId, systemData);
}
/**
* get all connection select options
* @param mapId
* @param systemData
* @returns {[]}
*/
static 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;
}
return {
value: connectionData.id,
text: text,
metaData: {
type: connectionData.type
}
};
};
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;
}
/**
* get all signature types that can exist for a given system
* -> result is partially cached
* @param systemTypeId
* @param areaId
* @param groupId
* @param statics
* @param shattered
* @returns {[]|*}
*/
static getSignatureTypeOptions(systemTypeId, areaId, groupId, {statics = null, shattered = false} = {}){
systemTypeId = parseInt(systemTypeId || 0);
areaId = parseInt(areaId || 0);
groupId = parseInt(groupId || 0);
let newSelectOptionsCount = 0;
if(!systemTypeId || !areaId || !groupId){
return [];
}
// check if sig types require more than one 'areaId' to be checked
let areaIds = SystemSignatureModule.getAreaIdsForSignatureTypeOptions(systemTypeId, areaId, groupId, shattered);
let cacheKey = [systemTypeId, ...areaIds, groupId].join('_');
let newSelectOptions = SystemSignatureModule.getCache('sigTypeOptions').get(cacheKey);
// check for cached signature names
if(Array.isArray(newSelectOptions)){
// hint: Cached signatures do not include static WHs!
// -> ".slice(0)" creates copy
newSelectOptions = newSelectOptions.slice(0);
newSelectOptionsCount = SystemSignatureModule.getOptionsCount('children', newSelectOptions);
}else{
newSelectOptions = [];
// get new Options ----------
// get all possible "static" signature names by the selected groupId
// -> for groupId == 5 (wormholes) this give you "wandering" whs
let tempSelectOptions = Util.getSignatureTypeNames(systemTypeId, areaIds, 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 = SystemSignatureModule.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 potential drifter holes (k-space only)
if([30, 31, 32].includes(areaId)){
let drifterWHData = [];
for(let drifterKey in Init.drifterWormholes){
if(
drifterKey > 0 &&
Init.drifterWormholes.hasOwnProperty(drifterKey)
){
newSelectOptionsCount++;
drifterWHData.push({value: newSelectOptionsCount, text: Init.drifterWormholes[drifterKey]});
}
}
if(drifterWHData.length > 0){
newSelectOptions.push({text: 'Drifter', children: drifterWHData});
}
}
// add potential 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
SystemSignatureModule.getCache('sigTypeOptions').set(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(statics){
let staticWHData = [];
let filterOptionCallback = text => option => option.text !== text;
for(let wormholeName of statics){
let wormholeData = Object.assign({}, Init.wormholes[wormholeName]);
let staticWHName = wormholeData.name + ' - ' + wormholeData.security;
// filter staticWHName from existing options -> prevent duplicates in <optgroup>
SystemSignatureModule.filterGroupedOptions(newSelectOptions, filterOptionCallback(staticWHName));
newSelectOptionsCount++;
staticWHData.push({value: newSelectOptionsCount, text: staticWHName});
}
if(staticWHData.length > 0){
newSelectOptions.unshift({text: 'Static', children: staticWHData});
}
}
}
return newSelectOptions;
}
/**
* filter out some options from nested select options
* @param obj
* @param callback
* @param key
*/
static filterGroupedOptions(obj, callback = () => true, key = 'children'){
for(let [i, val] of Object.entries(obj)){
// pre-check if filter callback will some, prevents unnecessary cloning
if(
typeof val === 'object' &&
val.hasOwnProperty(key) &&
val[key].not(callback).length
){
// clone object, apply filter to key prop
obj[i] = Object.assign({}, obj[i], {[key]: val[key].filter(callback)});
}
}
}
/**
* get possible frig holes that could spawn in a system
* filtered by "systemTypeId"
* @param systemTypeId
* @returns {{}}
*/
static getFrigateHolesBySystem(systemTypeId){
let signatureNames = {};
if(Init.frigateWormholes[systemTypeId]){
signatureNames = Init.frigateWormholes[systemTypeId];
}
return signatureNames;
}
/**
* 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}
*/
static getOptionsCount(key, obj){
let sum = 0;
for(let entry of obj){
if(entry.hasOwnProperty(key)){
sum += entry[key].length;
}else{
sum++;
}
}
return sum;
}
/**
* Some signatures types can spawn in more than one 'areaId' for a 'groupId'
* -> e.g. a 'shattered' C3 WHs have Combat/Relic/.. sites from C2, C3, c4!
* https://github.com/exodus4d/pathfinder/issues/875
* @param systemTypeId
* @param areaId
* @param groupId
* @param shattered
* @returns {[*]}
*/
static getAreaIdsForSignatureTypeOptions(systemTypeId, areaId, groupId, shattered = false){
let areaIds = [areaId];
if(
systemTypeId === 1 && shattered &&
[1, 2, 3, 4, 5, 6].includes(areaId) &&
[1, 2, 3].includes(groupId) // Combat, Relic, Data
){
areaIds = [areaId - 1, areaId, areaId + 1].filter(areaId => areaId >= 1 && areaId <= 6);
}else if(
systemTypeId === 1 && shattered &&
[1, 2, 3, 4, 5, 6].includes(areaId) &&
[4, 6].includes(groupId) // Gas, Ore
){
areaIds = [1, 2, 3, 4, 5, 6, 13];
}
return areaIds;
}
};
SystemSignatureModule.isPlugin = false; // module is defined as 'plugin'
SystemSignatureModule.scope = 'system'; // module scope controls how module gets updated and what type of data is injected
SystemSignatureModule.sortArea = 'a'; // default sortable area
SystemSignatureModule.position = 4; // default sort/order position within sortable area
SystemSignatureModule.label = 'Signatures'; // static module label (e.g. description)
SystemSignatureModule.fullDataUpdate = true; // static module requires additional data (e.g. system description,...)
SystemSignatureModule.cacheConfig = {
sigTypeOptions: { // cache signature names
ttl: 60 * 5,
maxSize: 100
}
};
SystemSignatureModule.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"
];
SystemSignatureModule.emptySignatureData = {
id: 0,
name: '',
groupId: 0,
typeId: 0
};
SystemSignatureModule.emptySignatureReaderCounterData = {
added: 0,
changed: 0,
deleted: 0,
deleteCon: 0
};
SystemSignatureModule.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',
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.msg;
// 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;
}
};
SystemSignatureModule.defaultConfig = {
className: 'pf-system-signature-module', // class for module
sortTargetAreas: ['a', 'b', 'c'], // sortable areas where module can be dragged into
headline: 'Signatures',
// headline toolbar
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)
tableCellTypeClass: 'pf-table-type-cell', // class for "type" cells
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
};
return SystemSignatureModule;
});