/** * 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 = 'filter'; let allSelected = value.length >= sourceData.length; if( !allSelected ){ html += ' (' + 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] = '' + parts[0] + ''; parts[1] = ''; parts[2] = '' + parts[2] + ''; parts[3] = '' + parts[3] + ''; 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('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('
')).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: '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: 'delete (0)', 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: '( 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 = ''; } 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: '', 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 = ' '; $(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 ''; } } } },{ 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 = ''; if(rowData.id){ val = ''; } 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 = '' + systemNameStr + ''; 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 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 rowElement * @param rowElement */ toggleTableRow(rowElement){ let toggleTableRowExecutor = (resolve, reject) => { let cellElements = rowElement.children('td'); let duration = 350; // wrap each 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 , remove padding cellElements.addClass(Counter.config.counterStopClass) .velocity({ paddingTop: [0, '4px'], paddingBottom: [0, '4px'], opacity: [0, 1] },{ duration: duration, easing: 'linear' }).wrapInner('
') .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($('
').hide()); // show row for padding animation rowElement.show(); cellElements.velocity({ paddingTop: ['4px', 0], paddingBottom: ['4px', 0] },{ duration: duration, queue: false, complete: function(){ // animate 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: ' 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}
` : ''}`; }, ''); 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 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 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; });