TimeTrex/interface/html5/services/TTEventBus.js

192 lines
8.6 KiB
JavaScript

import mitt from 'mitt';
const EventBus = mitt();
//Issue #3049 - Moved class static fields outside of main TTEventBus class as Safari v14.1 and older do not support class field declarations.
window.TTEventBusStatics = { AUTO_CLEAR_ON_EXIT: true, mitt: EventBus, _events_by_listener_scope: {} }; //Constants and external libraries
//_events_by_listener_scope - Internal data element, only to be accessed/changed via functions in this class. Static as its shared across scope_id's.
/**
* How to understand the ID's used in this class.
* this.scope_id: is tied to the owner of the instance of TTEventBus.
* so that they can be removed when the owner of that scope is unloaded/unmounted.
* mitt_event_id: is passed to the mitt event library. It uses the event scope id rather than stored instance scope_id,
* because listeners within a view/component might listen to different scope_id's depending on event.
* unique_listener_id: is only needed for debugging currently, to be able to differentiate between two different listeners
* listening to the same event on the same scope, but using different event handler functions.
*
*
*/
class TTEventBus {
// Standard mitt calls we want to expose for backwards compatibility to our old code.
// static on = TTEventBusStatics.mitt.on;
// static off = TTEventBusStatics.mitt.off;
constructor( options = {} ) {
// TTEventBus will happily works for both Views and Vue Components using a single id variable, but tracking them both might be more useful in the future.
this._options = options; // Unlikely to use directly, but will store here for debugging and future options.
this.scope_id = null; //scope_id of the listening view or component. Not neccessarily the scope of an event. When this scope unloads, we want to clear listener events related to that scope.
this._setInstanceScopeId( this.generateScopeIdFromOptions( options ) );
Debug.Text( 'constructor called ('+ this.scope_id +').', 'TTEventBus.js', 'TTEventBus', 'constructor', 11 );
}
/**
* Scope is created depending on availability of view id and component id. In most cases, the standard is to either use the view_id or component_id, to use both would overcomplicate storage of the components id's in the views..
* @param options
* @returns {string|boolean}
*/
generateScopeIdFromOptions( options ) {
// E.g. Schedule.vue-schedule-control-bar
// var scope_string = '';
// if( options?.view_id ) {
// scope_string += options.view_id;
// }
// if( options?.component_id ) {
// if( scope_string !== '' ) { scope_string += '.' }
// scope_string += options.component_id;
// }
//
// return scope_string;
if ( options && options.view_id && options.component_id ) {
Debug.Warn( 'Are you sure you want to set both view and component id? This complicates things.', 'TTEventBus.js', 'TTEventBus', 'generateScopeIdFromOptions', 2 );
return options.view_id + '.' + options.component_id;
} else if ( options && options.view_id ) {
return options.view_id;
} else if ( options && options.component_id ) {
return options.component_id;
} else {
return false;
}
}
/**
* Don't call this directly, as we need to generate the scope_id via generateScopeFromOptions first.
* @param scope_id
* @private
*/
_setInstanceScopeId( scope_id ) {
return this.scope_id = scope_id;
}
getInstanceScopeId() {
return this.scope_id;
}
generateMittId( scope_id, event_id ) {
// E.g. Schedule.vue-schedule-control-bar.scheduleModeOnChange
return scope_id + '.' + event_id;
}
/**
* Event that should only last for that view/vue component and be removed when scope is destroyed/unloaded.
* @param event_scope The scope_id related to the event.
* @param event_id id of the event, should be unique within the provided scope.
* @param event_handler Function to call when event is triggered.
* @param auto_clear_on_exit Specifies if this event should not be auto cleared when the vue/component is unloaded. Set using TTEventBusStatics.AUTO_CLEAR_ON_EXIT
*/
on( event_scope, event_id, event_handler, auto_clear_on_exit ) {
TTEventBusStatics._events_by_listener_scope[ this.scope_id ] = TTEventBusStatics._events_by_listener_scope[ this.scope_id ] || [];
// If we want unique ID's then use TTUUID.generateUUID(), but we want unique to a scope, so that duplicates can be prevented.
var mitt_event_id = this.generateMittId( event_scope, event_id);
var unique_listener_id = this.scope_id + ':' + mitt_event_id + ':' + TTUUID.generateUUID();
TTEventBusStatics._events_by_listener_scope[ this.scope_id ].push( {
unique_listener_id: unique_listener_id,
mitt_event_id: mitt_event_id,
event_scope: event_scope,
event_id: event_id,
event_handler: event_handler,
auto_clear_on_exit: auto_clear_on_exit
} );
TTEventBusStatics.mitt.on( mitt_event_id, event_handler );
Debug.Text( this.scope_id + ': Listener created for ('+ unique_listener_id +').', 'TTEventBus.js', 'TTEventBus', 'on', 11 );
return unique_listener_id;
}
/**
* Trigger EventBus event, but converts the scope_id and event_id into the mitt event id that the event is registered with.
* @param event_scope The scope_id related to the event.
* @param event_id id of the event, should be unique within the provided scope.
* @param event_data Object containing event data as parameters.
*/
emit( event_scope, event_id, event_data ) {
var mitt_event_id = this.generateMittId( event_scope, event_id);
Debug.Text( this.scope_id + ': Event emitted for ('+ mitt_event_id +').', 'TTEventBus.js', 'TTEventBus', 'emit', 11 );
return TTEventBusStatics.mitt.emit( mitt_event_id, event_data );
}
/**
* TODO: UNFINISHED.
* TODO: Improve this by adding ability to remove by scope and name, or scope, name and callback, or by unique ID.
* Warning: This will remove all events that match the scope_id and event_id, even if there are multiple.
* @param scope_id the scope of the event that needs to be switched off.
* @param event_id the event_id of the event tyhat needs to be switched off.
* @returns {void|number}
*/
off( scope_id, event_id ) {
var scope_array = TTEventBusStatics._events_by_listener_scope[ scope_id ];
if( scope_array === undefined ) {
// scope_id not found.
Debug.Error( 'Error: invalid params passed. scope_id not found.', 'TTEventBus.js', 'EventBus', 'off', 1 );
return -1;
}
var removeIndex = scope_array.map( item => item.event_id ).indexOf( event_id ); // TODO: Will only match the FIRST found, problem for multiple listeners like in Schedule.scheduleModeOnChange
if( removeIndex >= 0 ) {
var stored_event = scope_array[ removeIndex ];
scope_array.splice(removeIndex, 1);
Debug.Text( this.scope_id + ': Listener removed for ('+ stored_event.mitt_event_id +').', 'TTEventBus.js', 'EventBus', 'off', 11 );
return TTEventBusStatics.mitt.off( stored_event.mitt_event_id, stored_event.event_handler );
} else {
// event_id not found in array.
Debug.Error( 'Error: invalid params passed. event_id not found.', 'TTEventBus.js', 'EventBus', 'off', 1 );
return -1;
}
}
/**
* Used to trigger allOff() when unloading a view/component, using stored scope_id.
* @returns {number|boolean}
*/
autoClear() {
Debug.Text( 'Auto off triggered for ('+ this.scope_id +').', 'TTEventBus.js', 'EventBus', 'autoClear', 11 );
return this.allOff( this.scope_id );
}
/**
* This removes all events registered on the given scope. This will only apply to events that have the AUTO_CLEAR_ON_EXIT flag.
* @param scope_id
*/
allOff( scope_id ) {
var scope_array = TTEventBusStatics._events_by_listener_scope[ scope_id ];
if( scope_array === undefined ) {
// scope_id not found.
Debug.Text( 'Scope not found. But could be normal if this is a global function triggered on a scope with no events.', 'TTEventBus.js', 'EventBus', 'allOff', 2 );
return -1;
}
//Loop in reverse to easily remove array values.
for ( let i = scope_array.length - 1; i >= 0; i-- ) {
if ( scope_array[i].auto_clear_on_exit ) {
// Remove event
TTEventBusStatics.mitt.off( scope_array[i].mitt_event_id, scope_array[i].event_handler );
Debug.Text( 'Auto removed ' + scope_array[i].mitt_event_id + ' event on scope close.', 'TTEventBus.js', 'EventBus', 'allOff', 2 );
scope_array.splice( i, 1 );
} else {
Debug.Text( 'Event does not have AUTO_CLEAR_ON_EXIT. Skipping ' + scope_array[i].mitt_event_id, 'TTEventBus.js', 'EventBus', 'allOff', 2 );
}
}
if ( !TTEventBusStatics._events_by_listener_scope[ scope_id ] || TTEventBusStatics._events_by_listener_scope[ scope_id ].length === 0 ) {
//Remove empty scope array.
delete TTEventBusStatics._events_by_listener_scope[ scope_id ];
}
return true;
}
}
export default TTEventBus;