- NEW "plugin API" for custom UI modules, closed #913 - NEW live "Killstream" for killboard module, closed #909 - NEW "custom layout" UI settings, closed #470
285 lines
9.2 KiB
JavaScript
285 lines
9.2 KiB
JavaScript
define([
|
|
'jquery',
|
|
'app/init',
|
|
'app/util',
|
|
'app/promises/promise.deferred',
|
|
'app/promises/promise.queue'
|
|
], ($, Init, Util, 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(' ')
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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');
|
|
}
|
|
}
|
|
|
|
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.handler = [
|
|
'render',
|
|
'init',
|
|
'update',
|
|
'beforeHide',
|
|
'beforeDestroy',
|
|
'onSortableEvent'
|
|
];
|
|
|
|
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]
|
|
|
|
moduleHeadlineIconClass: 'pf-module-icon-button' // class for toolbar icons in the head
|
|
};
|
|
|
|
return BaseModule;
|
|
});
|