jsOMS/UI/ActionManager.js
Dennis Eichhorn 7b75ec58f7
Some checks failed
CodeQL / Analyze (javascript) (push) Has been cancelled
CI / general_module_workflow_js (push) Has been cancelled
fix permissions
2025-04-02 14:15:07 +00:00

220 lines
7.1 KiB
JavaScript

import { jsOMS } from '../Utils/oLib.js';
import { Logger } from '../Log/Logger.js';
/**
* Action manager class.
*
* @copyright Dennis Eichhorn
* @license OMS License 2.2
* @version 1.0.0
* @since 1.0.0
*
* @todo Karaka/jsOMS#26
* Sync/Async events
* Events so far can be created sync and async depending on the implementation.
* It would be better to make it sync/async depending on a option flag.
*
* @todo Karaka/jsOMS#35
* Template actions cannot be overwritten
* Templates by nature get added and removed from a page (often in order to reuse existing html to minimize the html load).
* The problem with templates is that they need to register in the ActionManager.js.
* A listener currently is only registered once per id.
* Since templates often keep the same id for some elements this results in a problem because the new template will not register a new listener.
* Possible solutions:
* 1. Force unique ids for templates during setup (pro: fast and easy initial solution, con: action event pollution not solved)
* 2. Whenever a dom element with action elements is removed, also unregister the listeners (pro: clean solution, con: difficult to implement)
* Solution 2 will be implemented.
* Maybe this can be performed in the dom removing action events or a dom listener would be required to listen for these dom elements.
*/
export class ActionManager
{
/**
* @constructor
*
* @param {Object} app Application
*
* @since 1.0.0
*/
constructor (app)
{
this.logger = Logger.getInstance();
this.app = app;
this.actions = {};
};
/**
* Bind element.
*
* @param {null|string} [id] Element id (optional)
*
* @return {void}
*
* @since 1.0.0
*/
bind (id = null)
{
const uiElements = id == null ? document.querySelectorAll('[data-action]') : (typeof id.length !== 'undefined' ? id : [id]);
const length = uiElements.length;
for (let i = 0; i < length; ++i) {
if (uiElements[i] !== null && uiElements[i].hasAttribute('data-action')) {
this.bindElement(uiElements[i]);
}
}
};
/**
* Bind element.
*
* @param {Element} e Element to bind
*
* @return {void}
*
* @since 1.0.0
*/
bindElement (e)
{
if (!jsOMS.isValidJson(e.getAttribute('data-action'))) {
this.logger.error('Invalid json string: \'' + e.getAttribute('data-action') + '\'');
return;
}
const listeners = JSON.parse(e.getAttribute('data-action'));
const listenerLength = listeners.length;
const self = this;
// For every action an event is registered
for (let i = 0; i < listenerLength; ++i) {
let c = [e];
let hasSelector = false;
// the selector must be a child of e!!!
if (Object.prototype.hasOwnProperty.call(listeners[i], 'selector')) {
c = document.querySelectorAll(listeners[i].selector);
hasSelector = true;
}
const childLength = c.length;
for (let j = 0; j < childLength; ++j) {
this.bindListener(c[j].id, listeners[i]);
}
/**
* @todo Karaka/jsOMS#69
* If a element has a selector defined this means the action is defined for all child elements of this selector.
* This usually is done in order to avoid defining the same behavior multiple times for similar elements (e.g. elements in a list).
* However, in this case it's not unusual that the child elements get changed dynamically (added, changed, removed).
* In this case the child element listeners need to be updated on change/addition/removal.
*/
const observeConfig = { childList: false, attributes: true, subtree: false };
if (hasSelector) {
this.app.eventManager.attach(e.id + '-childList', function (data) {
const length = data.addedNodes.length;
for (let j = 0; j < length; ++j) {
self.bindListener(
data.addedNodes[j].id,
listeners[i],
typeof listeners[i].autoremove !== 'undefined' ? listeners[i].autoremove : false
);
}
});
observeConfig.childList = true;
observeConfig.subtree = true;
}
this.app.eventManager.attach(e.id + '-attributes', function (data) {});
this.app.uiManager.getDOMObserver().observe(e, observeConfig);
}
};
/**
* Bind listener for object
*
* @param {string} id Element to bind
* @param {Object} listener Listener object
*
* @return {void}
*
* @since 1.0.0
*/
bindListener (id, listener, removable = false)
{
const self = this;
const actionLength = listener.action.length;
for (let j = 1; j < actionLength; ++j) {
if (typeof id === 'undefined' || typeof listener.key === 'undefined') {
this.logger.error('Invalid element id/key: ' + id + '/' + listener.key);
return;
}
if (this.app.eventManager.isAttached(id + '-' + listener.key + '-' + listener.action[j - 1].key)) {
return;
}
this.app.eventManager.attach(id + '-' + listener.key + '-' + listener.action[j - 1].key, function (data)
{
self.runAction(id, listener, listener.action[j], data);
}, removable, true);
}
// Register event for first action
document.getElementById(id).addEventListener(listener.listener, function (event)
{
if (listener.preventDefault) {
jsOMS.preventAll(event);
}
self.runAction(this.id, listener, listener.action[0], event);
}, false);
};
/**
* Run event action.
*
* @param {string} id Element
* @param {Object} listener Listener
* @param {Object} action Action
* @param {Object} data Data for the next action
*
* @return {void}
*
* @since 1.0.0
*/
runAction (id, listener, action, data)
{
const self = this;
if (!Object.prototype.hasOwnProperty.call(this.actions, action.type)) {
this.logger.warning('Undefined action ' + action.type);
return;
}
action.data = data;
this.actions[action.type](action, function (data)
{
self.app.eventManager.trigger(id + '-' + listener.key + '-' + action.key, id, data);
}, id);
};
/**
* Add action callback.
*
* @param {string} name Action identifier
* @param {function} callback Action callback
*
* @return {void}
*
* @since 1.0.0
*/
add (name, callback)
{
this.actions[name] = callback;
};
};