Files
pathfinder/js/app/ui/module/base.js
Mark Friedrich a5f29ee2eb - NEW "Thera connections" UI module, closed #829
- Upgraded "[_pathfinder_esi_](https://github.com/exodus4d/pathfinder_esi)" Web API client`v1.3.2` → `v2.0.0`
- Fixed a js bug where current active(selected) system becomes deselected after system was dragged on map
- Fixed a js bug where new auto mapped systems (e.g. after jump) were positioned outside current map scroll viewport
- Fixed a js bug where map sync failed after map tabs switch
- Fixed blurry map when map zoom was changed
- Fixed multiple minor JS bugs where map render/update failed
2020-03-02 16:42:36 +01:00

514 lines
18 KiB
JavaScript

define([
'jquery',
'app/init',
'app/util',
'app/map/util',
'app/lib/cache',
'app/promises/promise.deferred',
'app/promises/promise.queue'
], ($, Init, Util, MapUtil, Cache, DeferredPromise, PromiseQueue) => {
'use strict';
/**
* abstract BaseModel class
* -> custom/plugin modules must extend from it
* @type {BaseModule}
*/
let BaseModule = class BaseModule {
constructor(config= {}){
if(new.target === BaseModule){
throw new TypeError('Cannot construct ' + this.constructor.name + ' instances directly');
}
// check for abstract methods to be implemented in child
if(this.render === undefined){
throw new TypeError('Abstract method render() missing in ' + new.target.name + ' class');
}
this._config = Object.assign({}, BaseModule.defaultConfig, config);
this._updateQueue = new PromiseQueue();
}
/**
* get current module configuration
* @returns {*}
*/
get config(){
return this._config;
}
/**
* get root node for this module
* -> parent container for custom body HTML
* @returns {HTMLElement}
*/
get moduleElement(){
if(!this._moduleEl){
// init new moduleElement
this._moduleEl = Object.assign(document.createElement('div'), {
className: `${BaseModule.className} ${this._config.className}`,
style: {
opacity: '0'
}
}).setData('module', this);
this._moduleEl.dataset.position = this._config.position;
this._moduleEl.dataset.module = this.constructor.name;
// module header
this._moduleEl.append(this.newHeaderElement());
}
return this._moduleEl;
}
/**
* module header element
* -> dragHandler + headline
* @param text
* @returns {HTMLDivElement}
*/
newHeaderElement(text){
let headEl = this.newHeadElement();
headEl.append(
this.newHandlerElement(),
this.newHeadlineElement(text || this._config.headline)
);
return headEl;
}
/**
* module head element
* @returns {HTMLDivElement}
*/
newHeadElement(){
return Object.assign(document.createElement('div'), {
className: this._config.headClassName
});
}
/**
* module dragHandler element
* @returns {HTMLHeadingElement}
*/
newHandlerElement(){
return Object.assign(document.createElement('h5'), {
className: this._config.handlerClassName
});
}
/**
* module headline element
* @param text
* @returns {HTMLHeadingElement}
*/
newHeadlineElement(text){
return Object.assign(document.createElement('h5'), {
textContent: typeof text === 'string' ? text : ''
});
}
/**
* module toolbar element (wrapper)
* @returns {HTMLHeadingElement}
*/
newHeadlineToolbarElement(){
return Object.assign(document.createElement('h5'), {
className: 'pull-right'
});
}
/**
* icon element
* @param cls
* @returns {HTMLElement}
*/
newIconElement(cls = []){
return Object.assign(document.createElement('i'), {
className: ['fas', ...cls].join(' ')
});
}
/**
* label element
* @param text
* @param cls
* @returns {HTMLSpanElement}
*/
newLabelElement(text, cls = []){
let labelEl = document.createElement('span');
labelEl.classList.add('label', 'center-block', ...cls);
labelEl.textContent = text || '';
return labelEl;
}
/**
* control button element
* @param text
* @param cls
* @param iconCls
* @returns {HTMLDivElement}
*/
newControlElement(text, cls = [], iconCls = ['fa-sync']){
let controlEl = document.createElement('div');
controlEl.classList.add(...[BaseModule.Util.config.dynamicAreaClass, this._config.controlAreaClass, ...cls]);
controlEl.insertAdjacentHTML('beforeend', `  ${text}`);
controlEl.prepend(this.newIconElement(iconCls));
return controlEl;
}
/**
* HTTP request handler for internal (Pathfinder) ajax calls
* @param args
* @returns {Promise}
*/
request(...args){
return BaseModule.Util.request(...args);
}
/**
* scoped instance for LocalStore for current module
* @returns {LocalStore}
*/
getLocalStore(){
if(!this._localStore){
// make accessible -> scope Store keys!
this._localStore = BaseModule.Util.getLocalStore('module');
this._localStore.scope = this.constructor.name;
}
return this._localStore;
}
/**
* visual notification handler (UI popover)
* -> can be used for info/error on-screen messages
* @param args
*/
showNotify(...args){
return BaseModule.Util.showNotify(...args);
}
/**
* responsible for dispatching all incoming method calls
* @param handler
* @param data
* @returns {*}
*/
handle(handler, ...data){
try{
if(BaseModule.handler.includes(handler)){
// .. run module handler
let returnData = this[handler].apply(this, data);
if(returnData instanceof Promise){
// log returned Promise from handler call resolved
returnData.then(() => { this.logHandler(handler, 0);});
}
// log handler call
this.logHandler(handler);
return returnData;
}else{
console.error('Error in module %o. Invalid handler %o', this.constructor.name, handler);
}
}catch(e){
console.error('Error in module %o in handler %s() %o', this.constructor.name, handler, e);
}
}
/**
* log handler calls for this instance
* -> can be helpful for debugging
* @param handler
* @param increment
*/
logHandler(handler, increment = 1){
if(increment){
if(!this._config.logHandler){
this._config.logHandler = {};
}
this._config.logHandler[handler] = (this._config.logHandler[handler] || 0) + increment;
}
}
/**
* init module
*/
init(){}
/**
* update module
* @param data
* @returns {Promise}
*/
update(data){
return this._updateQueue.enqueue(() => Promise.resolve(data), 'end', 'upd');
}
beforeHide(){}
beforeDestroy(){
$(this.moduleElement).destroyPopover(true);
// destroy DataTable instances
for(let table of $(this.moduleElement).find('table.dataTable')){
$(table).DataTable().destroy(true);
}
}
/**
* events from 'Sortable' lib
* @see https://github.com/SortableJS/Sortable
* @param name
* @param e
*/
onSortableEvent(name, e){
if(name === 'onUnchoose' && this._sortableChoosePromise){
this._sortableChoosePromise.resolve();
}
if(name === 'onChoose' && !this._sortableChoosePromise){
this._sortableChoosePromise = BaseModule.newDeferredPromise();
this._updateQueue.enqueue(() => this._sortableChoosePromise.then(() => {
this._sortableChoosePromise = null;
}), 'start');
}
}
/**
* get a unique cache key name for "source"/"target"-name
* @param sourceName
* @param targetName
* @returns {string|boolean}
*/
static getConnectionDataCacheKey(sourceName, targetName){
let key = false;
if(sourceName && targetName){
// names can be "undefined" in case system is currently in drag/drop state
// sort() is important -> ignore direction
key = `con_` + `${ [String(sourceName).toLowerCase(), String(targetName).toLowerCase()].sort() }`.hashCode();
}
return key;
}
/**
* get a connectionsData object that holds all connections for given mapIds (used as cache for route search)
* @param mapIds
* @returns {{}}
*/
static getConnectionsDataFromMaps(mapIds){
let data = {};
for(let mapId of mapIds){
let map = MapUtil.getMapInstance(mapId);
if(map){
let cacheKey = `map_${mapId}`;
let cache = BaseModule.getCache('mapConnections');
let mapConnectionsData = cache.get(cacheKey);
if(!mapConnectionsData){
mapConnectionsData = this.getConnectionsDataFromConnections(mapId, map.getAllConnections());
// update cache
cache.set(cacheKey, mapConnectionsData);
}
Object.assign(data, mapConnectionsData);
}
}
return data;
}
/**
* get a connectionsData object for all connections
* @param mapId
* @param connections
* @returns {{}}
*/
static getConnectionsDataFromConnections(mapId = 0, connections = []){
let data = {};
if(connections.length){
let connectionsData = MapUtil.getDataByConnections(connections);
for(let connectionData of connectionsData){
let connectionDataCacheKey = BaseModule.getConnectionDataCacheKey(connectionData.sourceName, connectionData.targetName);
// skip double connections between same systems
if(connectionDataCacheKey && !Object.keys(data).includes(connectionDataCacheKey)){
data[connectionDataCacheKey] = {
map: {
id: mapId
},
connection: {
id: connectionData.id,
type: connectionData.type,
scope: connectionData.scope,
updated: connectionData.updated
},
source: {
id: connectionData.source,
name: connectionData.sourceName,
alias: connectionData.sourceAlias
},
target: {
id: connectionData.target,
name: connectionData.targetName,
alias: connectionData.targetAlias
}
};
}
}
}
return data;
}
/**
* search for a specific connection by "source"/"target"-name inside connectionsData cache
* @param connectionsData
* @param sourceName
* @param targetName
* @returns {*}
*/
static findConnectionsData(connectionsData, sourceName, targetName){
return this.Util.getObjVal(connectionsData, this.getConnectionDataCacheKey(sourceName, targetName));
}
/**
* get fake connection data (default connection type in case connection was not found on a map)
* @param sourceSystemData
* @param targetSystemData
* @param scope
* @param types
* @returns {{connection: {scope: string, id: number, type: [*]}, source: {name: number, alias: number, id: number}, target: {name: number, alias: number, id: number}}}
*/
static getFakeConnectionData(sourceSystemData, targetSystemData, scope = 'stargate', types = []){
return {
connection: {
id: 0,
scope: scope,
type: types.length ? types : [MapUtil.getDefaultConnectionTypeByScope(scope)],
updated: 0
},
source: {
id: 0,
name: sourceSystemData.system,
alias: sourceSystemData.system
},
target: {
id: 0,
name: targetSystemData.system,
alias: targetSystemData.system
}
};
}
/**
* get fake connection Element
* @param connectionData
* @returns {string}
*/
static getFakeConnectionElement(connectionData){
let mapId = this.Util.getObjVal(connectionData, 'map.id') || 0;
let connectionId = this.Util.getObjVal(connectionData, 'connection.id') || 0;
let scope = this.Util.getObjVal(connectionData, 'connection.scope') || '';
let classes = MapUtil.getConnectionFakeClassesByTypes(this.Util.getObjVal(connectionData, 'connection.type') || []);
let disabled = !mapId || !connectionId;
let connectionElement = '<div data-mapId="' + mapId + '" data-connectionId="' + connectionId + '" ';
connectionElement += (disabled ? 'data-disabled' : '');
connectionElement += ' class="' + classes.join(' ') + '" ';
connectionElement += ' title="' + scope + '" data-placement="bottom"></div>';
return connectionElement;
}
/**
* get static instance of in-memory Cache() store by 'name'
* -> not persistent across page reloads
* -> persistent across module instances (different and same maps)
* @param name
* @returns {Cache}
*/
static getCache(name){
let key = `CACHE-${name}`;
if(!this.Util.getObjVal(this, key)){
let configKey = `cacheConfig.${name}`;
let cacheConfig = this.Util.getObjVal(this, configKey);
if(!cacheConfig){
console.warn('Missing Cache config for %o. Expected at %o. Default config loaded…',
name, `${this.name}.${configKey}`
);
cacheConfig = {};
}else{
// set cache name
cacheConfig.name = name;
}
this[key] = new Cache(cacheConfig);
}
return this[key];
}
static now(){
return new Date().getTime() / 1000;
}
static getOrderPrio(){
return this.isPlugin ?
this.scopeOrder.indexOf('plugin') :
(this.scopeOrder.indexOf(this.scope) !== -1 ?
this.scopeOrder.indexOf(this.scope) :
this.scopeOrder.length - 1
);
}
static newDeferredPromise(){
return new DeferredPromise();
}
};
BaseModule.isPlugin = true; // module is defined as 'plugin'
BaseModule.scope = 'system'; // static module scope controls how module gets updated and what type of data is injected
BaseModule.sortArea = 'a'; // static default sortable area
BaseModule.position = 0; // static default sort/order position within sortable area
BaseModule.label = '???'; // static module label (e.g. description)
BaseModule.className = 'pf-module'; // static CSS class name
BaseModule.fullDataUpdate = false; // static module requires additional data (e.g. system description,...)
BaseModule.Util = Util; // static access to Pathfinders Util object
BaseModule.scopeOrder = [
'system',
'connection',
'global',
'plugin',
'undefined'
];
BaseModule.handler = [
'render',
'init',
'update',
'beforeHide',
'beforeDestroy',
'onSortableEvent'
];
BaseModule.cacheConfig = {
mapConnections: {
ttl: 5,
maxSize: 600,
debug: false
}
};
BaseModule.defaultConfig = {
position: 1,
className: 'pf-base-module', // class for module
headClassName: 'pf-module-head', // class for module header
handlerClassName: 'pf-sortable-handle', // class for "drag" handler
sortTargetAreas: ['a', 'b', 'c'], // sortable areas where module can be dragged into
headline: 'Base headline', // module headline
bodyClassName: 'pf-module-body', // class for module body [optional: can be used]
controlAreaClass: 'pf-module-control-area', // class for "control" areas
moduleHeadlineIconClass: 'pf-module-icon-button' // class for toolbar icons in the head
};
return BaseModule;
});