Files
pathfinder/js/app/ui/module/base.js
Mark Friedrich 0c3d57e833 - BC Break: Required _PHP_ version changed >=7.1>=7.2
- NEW "plugin API" for custom UI modules, closed #913
- NEW live "Killstream" for killboard module, closed #909
- NEW "custom layout" UI settings, closed #470
2020-02-01 12:40:17 +01:00

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;
});