2023-11-19 16:32:20 +00:00
|
|
|
import Emitter from './Emitter';
|
|
|
|
|
import Logger from './Logger';
|
2024-01-11 23:27:10 +00:00
|
|
|
import Registry from './Registry';
|
|
|
|
|
import CetraPlugin from '../plugins/CetraPlugin';
|
|
|
|
|
import BaseComponent from './BaseComponent';
|
|
|
|
|
|
2023-11-19 16:32:20 +00:00
|
|
|
class Cetra extends Emitter {
|
|
|
|
|
// class static globals
|
|
|
|
|
static #instances = new Map();
|
2024-01-11 23:27:10 +00:00
|
|
|
static #entityTypes = {
|
|
|
|
|
plugins: 'Plugin',
|
|
|
|
|
commands: 'Command',
|
|
|
|
|
types: 'Type',
|
|
|
|
|
}; // prevents us from defining several `registerSomething` methods
|
2023-11-19 16:32:20 +00:00
|
|
|
static #defaultSettings = new Map([
|
|
|
|
|
['selector', 'ctra'],
|
|
|
|
|
['root', document.body],
|
2024-01-11 23:27:10 +00:00
|
|
|
[
|
|
|
|
|
'observeOptions',
|
|
|
|
|
{
|
|
|
|
|
childList: true,
|
|
|
|
|
subtree: true,
|
|
|
|
|
attributes: true,
|
|
|
|
|
attributeOldValue: false,
|
|
|
|
|
characterData: true,
|
|
|
|
|
characterDataOldValue: false,
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
['types', new Map()],
|
|
|
|
|
['plugins', new Map()],
|
|
|
|
|
['enforceSecureUrls', true],
|
|
|
|
|
['noSandboxing', false],
|
|
|
|
|
['varSeparator', ':'],
|
|
|
|
|
['defaultPlugins', new Map()],
|
2023-11-19 17:30:24 +00:00
|
|
|
]);
|
|
|
|
|
|
2023-11-19 16:32:20 +00:00
|
|
|
#identifier;
|
2024-01-11 23:27:10 +00:00
|
|
|
#secretKey;
|
2023-11-19 17:30:24 +00:00
|
|
|
#root;
|
2023-11-19 16:32:20 +00:00
|
|
|
#internals = new Map([
|
2024-01-11 23:27:10 +00:00
|
|
|
['compIndex', new Map()],
|
2023-11-19 16:32:20 +00:00
|
|
|
['settings', new Map()],
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Constructs a new instance of the Cetra framework.
|
|
|
|
|
* @class
|
|
|
|
|
* @classdesc The Cetra framework provides a reactive approach to DOM manipulation and component management.
|
|
|
|
|
* @param {Map} settings - A map object containing the framework settings.
|
|
|
|
|
*/
|
|
|
|
|
constructor(options = new Map()) {
|
|
|
|
|
super();
|
2024-01-11 23:27:10 +00:00
|
|
|
this.#secretKey = Math.random().toString(36).substring(2, 8);
|
|
|
|
|
this.#identifier = Registry.register({
|
|
|
|
|
key: this.#secretKey,
|
|
|
|
|
obj: this,
|
|
|
|
|
parent: '$root',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.#initEntityTypes(); // generic entity specific methods registration
|
2023-11-19 17:15:17 +00:00
|
|
|
|
|
|
|
|
if (!(options instanceof Map) && typeof options == 'object') {
|
|
|
|
|
options = new Map(Object.entries(options));
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-11 23:27:10 +00:00
|
|
|
for (let key of Cetra.#defaultSettings.keys()) {
|
|
|
|
|
if (Object.keys(Cetra.#entityTypes).includes(key)) continue;
|
2023-11-19 17:15:17 +00:00
|
|
|
let value = options.get(key);
|
|
|
|
|
if (typeof value == 'undefined') {
|
|
|
|
|
value = Cetra.#defaultSettings.get(key);
|
|
|
|
|
}
|
2024-01-11 17:12:24 +00:00
|
|
|
this.#internals.get('settings').set(key, value);
|
2023-11-19 17:15:17 +00:00
|
|
|
}
|
|
|
|
|
|
2024-01-11 23:27:10 +00:00
|
|
|
if (options.get('plugins') instanceof Map) {
|
|
|
|
|
this.#registerPlugins([...options.get('plugins')]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this.#internals.get('settings').get('defaultPlugins').size > 0) {
|
|
|
|
|
this.#registerPlugins([
|
|
|
|
|
...this.#internals.get('settings').get('defaultPlugins'),
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-19 17:15:17 +00:00
|
|
|
for (const [key, cb] of options) {
|
|
|
|
|
if (!key.startsWith('on') || typeof cb != 'function') continue;
|
|
|
|
|
this.on(key, cb);
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-19 16:32:20 +00:00
|
|
|
this.#init();
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-11 23:27:10 +00:00
|
|
|
/**
|
|
|
|
|
* Registers entity types.
|
|
|
|
|
*/
|
|
|
|
|
#initEntityTypes() {
|
|
|
|
|
for (const key of Object.keys(Cetra.#entityTypes)) {
|
|
|
|
|
const entityType = Cetra.#entityTypes[key];
|
|
|
|
|
this.#internals.set(key, new Map());
|
|
|
|
|
if (typeof this[`add${entityType}`] === 'undefined') {
|
|
|
|
|
this[`add${entityType}`] = (identifier, fn) =>
|
|
|
|
|
this.#addEntity(entityType, identifier, fn);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (typeof this[`add${entityType}s`] === 'undefined') {
|
|
|
|
|
this[`add${entityType}s`] = (entities) => {
|
|
|
|
|
if (!Array.isArray(entities)) {
|
|
|
|
|
if (typeof entities != 'string')
|
|
|
|
|
throw new TypeError(
|
|
|
|
|
`${entityType} is not of type array or string`
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
throw new TypeError(
|
|
|
|
|
`${entityType} is not of type array`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
entities.forEach(({ identifier, _obj }) => {
|
|
|
|
|
this[`register${entityType}`](identifier, _obj);
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (typeof this[`remove${entityType}`] === 'undefined') {
|
|
|
|
|
this[`remove${entityType}`] = (identifier) =>
|
|
|
|
|
this.#removeEntity(entityType, identifier);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (typeof this[`get${entityType}`] === 'undefined') {
|
|
|
|
|
this[`get${entityType}`] = (identifier) =>
|
|
|
|
|
this.#getEntity(entityType, identifier);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Registers an entity with the specified entity type and identifier.
|
|
|
|
|
*
|
|
|
|
|
* @param {string} entityType - The type of the entity.
|
|
|
|
|
* @param {string} identifier - The identifier of the entity.
|
|
|
|
|
* @param {Function} fn - The constructor function for the entity.
|
|
|
|
|
* @throws {TypeError} If the entity type is invalid.
|
|
|
|
|
*/
|
|
|
|
|
#addEntity(entityType, identifier, fn) {
|
|
|
|
|
if (typeof identifier != 'string') {
|
|
|
|
|
Logger.error(`invalid identifier for ${entityType} ${identifier}`);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
// get entityKey by its value
|
|
|
|
|
const entityKey = Object.keys(Cetra.#entityTypes).find((key) => {
|
|
|
|
|
return Cetra.#entityTypes[key] == entityType;
|
|
|
|
|
});
|
|
|
|
|
if (!entityKey) {
|
|
|
|
|
Logger.error(`invalid entity type ${entityType}`);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let uniqueId = Registry.register({
|
|
|
|
|
obj: fn,
|
|
|
|
|
key: this.#secretKey,
|
|
|
|
|
parent: this.#identifier,
|
|
|
|
|
});
|
|
|
|
|
try {
|
|
|
|
|
if (typeof fn === 'undefined') {
|
|
|
|
|
Registry.unregister(uniqueId, this.#secretKey);
|
|
|
|
|
throw new TypeError(
|
|
|
|
|
`invalid entity constructor for ${entityType} ${identifier}`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!this.#isValidEntity(entityType, fn)) {
|
|
|
|
|
Registry.unregister(uniqueId, this.#secretKey);
|
|
|
|
|
throw new TypeError(
|
|
|
|
|
`invalid entity constructor for ${entityType} ${identifier}`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (typeof fn == 'function') {
|
|
|
|
|
if (!(fn instanceof CetraPlugin)) {
|
|
|
|
|
if (fn.name) {
|
|
|
|
|
Registry.unregister(uniqueId, this.#secretKey);
|
|
|
|
|
fn = new fn(uniqueId);
|
|
|
|
|
uniqueId = Registry.register({
|
|
|
|
|
obj: fn,
|
|
|
|
|
key: this.#secretKey,
|
|
|
|
|
parent: this.#identifier,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (typeof fn.register === 'function') {
|
|
|
|
|
fn = fn.register(uniqueId);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
this.#internals.get(entityKey).set(identifier, fn);
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
} catch (err) {
|
|
|
|
|
Logger.error(err);
|
|
|
|
|
Registry.unregister(uniqueId);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#isValidEntity(entityType, fn) {
|
|
|
|
|
if (typeof fn === 'undefined') return false;
|
|
|
|
|
switch (entityType) {
|
|
|
|
|
case 'Plugin':
|
|
|
|
|
return fn instanceof CetraPlugin || typeof fn === 'function';
|
|
|
|
|
case 'Command':
|
|
|
|
|
case 'Type':
|
|
|
|
|
return typeof fn === 'function';
|
|
|
|
|
default:
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Unregisters an entity of the specified type and identifier.
|
|
|
|
|
* @param {string} entityType - The type of the entity.
|
|
|
|
|
* @param {string} identifier - The identifier of the entity.
|
|
|
|
|
* @returns {boolean|undefined} - Returns `true` if the entity was successfully unregistered, `false` if the entity was not found, or `undefined` if the entity type does not exist.
|
|
|
|
|
*/
|
|
|
|
|
#removeEntity(entityType, identifier) {
|
|
|
|
|
if (this.#internals.get(entityType).has(identifier))
|
|
|
|
|
return this.#internals.get(entityType).delete(identifier);
|
|
|
|
|
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Retrieves an entity of the specified type and identifier.
|
|
|
|
|
* @param {string} entityType - The type of the entity.
|
|
|
|
|
* @param {string} identifier - The identifier of the entity.
|
|
|
|
|
* @returns {any} The retrieved entity, or undefined if not found.
|
|
|
|
|
*/
|
|
|
|
|
#getEntity(entityType, identifier) {
|
|
|
|
|
return this.#internals.get(entityType).get(identifier) ?? undefined;
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-19 16:32:20 +00:00
|
|
|
/**
|
|
|
|
|
* Retrieves an instance of Cetra by its ID.
|
|
|
|
|
* @param {string} id - The ID of the Cetra instance to retrieve.
|
|
|
|
|
* @returns {Cetra} The Cetra instance with the specified ID.
|
|
|
|
|
*/
|
|
|
|
|
static getInstance(id) {
|
|
|
|
|
return Cetra.#instances.get(id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Initializes the Cetra framework by setting up the mutation observer and processing existing components.
|
|
|
|
|
* @memberof Cetra
|
|
|
|
|
* @instance
|
|
|
|
|
* @method
|
|
|
|
|
*/
|
|
|
|
|
#init() {
|
2024-01-11 23:27:10 +00:00
|
|
|
if (!MutationObserver || typeof MutationObserver == 'undefined')
|
|
|
|
|
throw new Error('Please update your browser.');
|
|
|
|
|
|
2023-11-19 16:32:20 +00:00
|
|
|
this.#root =
|
|
|
|
|
this.#internals.get('settings').get('root') ||
|
|
|
|
|
document.querySelector(this.#internals.get('settings').get('root'));
|
|
|
|
|
|
|
|
|
|
if (typeof this.#root == 'string') {
|
|
|
|
|
this.#root = document.querySelector(this.#root);
|
|
|
|
|
|
|
|
|
|
if (!this.#root) {
|
|
|
|
|
Logger.error('No root found, defaulting to body');
|
|
|
|
|
this.#root = document.body;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.#registerDefaults();
|
2024-01-11 23:27:10 +00:00
|
|
|
this.#readComponents();
|
|
|
|
|
this.#initObserver();
|
2023-11-19 16:32:20 +00:00
|
|
|
|
|
|
|
|
this.#root.setAttribute(
|
|
|
|
|
`data-${this.#internals.get('settings').get('selector')}-id`,
|
|
|
|
|
this.#identifier
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
Cetra.#instances.set(this.#identifier, this);
|
2023-11-19 17:15:17 +00:00
|
|
|
this.emit('onAfterInit', this);
|
2023-11-19 16:32:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Registers default properties, transformer or anything related to the functionality
|
|
|
|
|
* @returns {void}
|
|
|
|
|
*/
|
|
|
|
|
#registerDefaults() {
|
2024-01-11 23:27:10 +00:00
|
|
|
this.addType('string', (val) => {
|
|
|
|
|
return String(val);
|
|
|
|
|
});
|
|
|
|
|
this.addType('number', (val) => {
|
|
|
|
|
return Number(val);
|
|
|
|
|
});
|
|
|
|
|
this.addType('int', (val) => {
|
|
|
|
|
return isNaN(val) ? 0 : parseInt(val, 10);
|
|
|
|
|
});
|
|
|
|
|
this.addType('integer', (val) => {
|
|
|
|
|
return isNaN(val) ? 0 : parseInt(val, 10);
|
|
|
|
|
});
|
|
|
|
|
this.addType('float', (val) => {
|
|
|
|
|
return isNaN(val) ? 0 : parseFloat(val);
|
|
|
|
|
});
|
|
|
|
|
this.addType('bool', (str) => {
|
|
|
|
|
return ['true', 'yes', 'on', '+', '1'].includes(str.toLowerCase());
|
|
|
|
|
});
|
|
|
|
|
this.addType('boolean', (str) => {
|
|
|
|
|
return ['true', 'yes', 'on', '+', '1'].includes(str.toLowerCase());
|
|
|
|
|
});
|
|
|
|
|
this.addType('json', (val) => {
|
|
|
|
|
let data;
|
|
|
|
|
try {
|
|
|
|
|
data = JSON.parse(val);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
//Logger.error(err);
|
|
|
|
|
return val;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return data;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Initializes the mutation observer to listen for changes in the DOM and triggers the appropriate actions.
|
|
|
|
|
* @memberof Cetra
|
|
|
|
|
* @instance
|
|
|
|
|
* @method
|
|
|
|
|
* @private
|
|
|
|
|
*/
|
|
|
|
|
#initObserver() {
|
|
|
|
|
try {
|
|
|
|
|
this.observer = new MutationObserver(
|
|
|
|
|
this.handleMutation.bind(this)
|
|
|
|
|
);
|
|
|
|
|
this.observer.observe(
|
|
|
|
|
this.#root,
|
|
|
|
|
this.#internals.get('settings').get('observeOptions')
|
|
|
|
|
);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
Logger.error('an error occured with the observer!', err);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Retrieves a plugin by its identifier.
|
|
|
|
|
* @note even though there is `getEntity` this method is required for the plugin loading process
|
|
|
|
|
*
|
|
|
|
|
* @param {string} identifier - The identifier of the plugin.
|
|
|
|
|
* @returns {Promise<CetraPlugin>|Function|Object|undefined} - The plugin object or a promise that resolves to the plugin object. Returns undefined if the plugin is not found or if the identifier is not a string.
|
|
|
|
|
*/
|
|
|
|
|
getPlugin(identifier) {
|
|
|
|
|
if (!(identifier instanceof String)) {
|
|
|
|
|
Logger.error('Plugin identifier is not a string');
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!this.#internals.get('plugins').has(identifier)) return undefined;
|
|
|
|
|
|
|
|
|
|
const plugin = this.#internals.get('plugins').get(identifier);
|
|
|
|
|
|
|
|
|
|
if (!plugin) return undefined;
|
|
|
|
|
|
|
|
|
|
if (typeof plugin.url == 'string') {
|
|
|
|
|
return this.#loadPlugin(plugin.url, identifier); // returns a promise
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!['function', 'object'].includes(typeof plugin)) return undefined;
|
|
|
|
|
|
|
|
|
|
return plugin;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* requests a plugin from the given url and returns a promise which is resolved after the plugin is loaded.
|
|
|
|
|
* @param {String} url the url to load
|
|
|
|
|
* @returns {Promise} the promise of the plugin loading process
|
|
|
|
|
*/
|
|
|
|
|
#loadPlugin(url, identifier) {
|
|
|
|
|
if (!this.#internals.get('plugins').has(identifier))
|
|
|
|
|
return Promise.reject(
|
|
|
|
|
new Error(`plugin ${identifier} is not registered`)
|
|
|
|
|
);
|
|
|
|
|
const _framework = this;
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
fetch(url)
|
|
|
|
|
.then((response) => {
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error(
|
|
|
|
|
`Cetra: Plugin ${url} could not be loaded`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return response.text();
|
|
|
|
|
})
|
|
|
|
|
.then((text) => {
|
|
|
|
|
if (this.#internals.get('settings').get('noSandboxing')) {
|
|
|
|
|
const pluginScriptTag =
|
|
|
|
|
document.createElement('script');
|
|
|
|
|
pluginScriptTag.textContent = text;
|
|
|
|
|
|
|
|
|
|
document.body.appendChild(pluginScriptTag);
|
|
|
|
|
return resolve();
|
|
|
|
|
} else if (this.#internals.get('plugins').get('sandbox')) {
|
|
|
|
|
return this.#internals
|
|
|
|
|
.get('plugins')
|
|
|
|
|
.get('sandbox')
|
|
|
|
|
.execute(text, (integrityId) => {
|
|
|
|
|
if (
|
|
|
|
|
!_framework.#identifier
|
|
|
|
|
.get('plugins')
|
|
|
|
|
.has(identifier)
|
|
|
|
|
)
|
|
|
|
|
return reject(
|
|
|
|
|
new Error(
|
|
|
|
|
`plugin ${identifier} is not registered`
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const pluginData = _framework.#internals
|
|
|
|
|
.get('plugins')
|
|
|
|
|
.get(identifier);
|
|
|
|
|
_framework.#internals
|
|
|
|
|
.get('plugins')
|
|
|
|
|
.set(identifier, {
|
|
|
|
|
...pluginData,
|
|
|
|
|
integrityId,
|
|
|
|
|
});
|
|
|
|
|
resolve(
|
|
|
|
|
_framework.#internals
|
|
|
|
|
.get('plugins')
|
|
|
|
|
.get(identifier)
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
return reject(
|
|
|
|
|
new Error(`plugin ${identifier} is not registered`)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.catch(reject);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Initializes a plugin by registering it with the Cetra registry.
|
|
|
|
|
*
|
|
|
|
|
* @param {Function|Object} klass - The plugin class or object to be initialized.
|
|
|
|
|
* @returns {Function|Object} - The initialized plugin.
|
|
|
|
|
* @throws {TypeError} - If the plugin is null, not of a valid type, or has an invalid URL.
|
|
|
|
|
*/
|
|
|
|
|
#initPlugin(klass) {
|
|
|
|
|
const uid = Registry.register({
|
|
|
|
|
key: this.#secretKey,
|
|
|
|
|
obj: klass,
|
|
|
|
|
parent: this.#identifier,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
if (!['function', 'object'].includes(typeof klass))
|
|
|
|
|
throw new TypeError(
|
|
|
|
|
'plugin is not of type function or object or instance of a CetraPlugin (child) class'
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (klass instanceof CetraPlugin) {
|
|
|
|
|
if (typeof klass.register === 'function') {
|
|
|
|
|
return klass.register(uid);
|
|
|
|
|
}
|
|
|
|
|
return klass;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (typeof klass === 'function') {
|
|
|
|
|
if (!klass.name) {
|
|
|
|
|
klass = new klass(uid); // simple function
|
|
|
|
|
} else {
|
|
|
|
|
klass = new klass(uid); // a class with its own name
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// what else could it be?
|
|
|
|
|
if (typeof klass !== 'object')
|
|
|
|
|
throw new TypeError(
|
|
|
|
|
'plugin is not of a valid plugin type (`CetraPlugin` or `object` or `function` or `url`)'
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
klass.url &&
|
|
|
|
|
(typeof klass.url !== 'string' || !this.#isUrl(klass.url))
|
|
|
|
|
)
|
|
|
|
|
throw new TypeError(
|
|
|
|
|
'plugin url is not of type string and/or no valid url'
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (klass.plugin) {
|
|
|
|
|
Logger.info('registering plugin 5', klass);
|
|
|
|
|
return this.#initPlugin(klass.plugin);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return klass;
|
|
|
|
|
} catch (err) {
|
|
|
|
|
Logger.error(err);
|
|
|
|
|
Registry.unregister(uid, this.#secretKey);
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
2023-11-19 16:32:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* checks if the given url is valid
|
|
|
|
|
* @param {String} url the url to check
|
|
|
|
|
* @returns {Boolean} true if the url is valid, false otherwise
|
|
|
|
|
*/
|
|
|
|
|
#isUrl(url) {
|
|
|
|
|
if (typeof url != 'string') return false;
|
|
|
|
|
try {
|
|
|
|
|
// if there is no error this should be a valid url
|
|
|
|
|
const urlObj = new URL(url, window.location.href);
|
|
|
|
|
return this.#internals.get('settings').get('enforceSecureUrls')
|
|
|
|
|
? urlObj.protocol === 'https:'
|
|
|
|
|
: true;
|
|
|
|
|
} catch (err) {
|
|
|
|
|
// any error is a bad url
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2024-01-11 23:27:10 +00:00
|
|
|
* Reads and initializes components based on the defined component selector.
|
2023-11-19 16:32:20 +00:00
|
|
|
* @memberof Cetra
|
2024-01-11 23:27:10 +00:00
|
|
|
* @instance
|
2023-11-19 16:32:20 +00:00
|
|
|
* @method
|
2024-01-11 23:27:10 +00:00
|
|
|
* @param {NodeList} nodes - The NodeList of DOM nodes to process.
|
|
|
|
|
* @param {boolean} [recursive=true] - Indicates whether to recursively process child nodes.
|
2023-11-19 16:32:20 +00:00
|
|
|
*/
|
2024-01-11 23:27:10 +00:00
|
|
|
#readComponents() {
|
|
|
|
|
let comps = this.queryElements('', { _parent: this.#root });
|
|
|
|
|
if (!comps || comps.length == 0)
|
|
|
|
|
return Logger.log('no components found (yet)');
|
|
|
|
|
|
|
|
|
|
for (const [index, comp] of [...comps.entries()]) {
|
|
|
|
|
if (
|
|
|
|
|
comp.matches(
|
|
|
|
|
`[data-${this.#internals
|
|
|
|
|
.get('settings')
|
|
|
|
|
.get('selector')}-id]`
|
|
|
|
|
)
|
|
|
|
|
) {
|
|
|
|
|
Logger.info('skipped element', comp);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.#initComponent(comp);
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-11-19 16:32:20 +00:00
|
|
|
|
2024-01-11 23:27:10 +00:00
|
|
|
/**
|
|
|
|
|
* Generates a comma-separated string of selectors based on the component selector defined in the framework's settings.
|
|
|
|
|
* @memberof Cetra
|
|
|
|
|
* @instance
|
|
|
|
|
* @method
|
|
|
|
|
* @param {string | string[]} [suffixes=[]] - The suffixes to append to the component selector.
|
|
|
|
|
* @returns {string} The generated selector string.
|
|
|
|
|
*/
|
|
|
|
|
#selectors(suffixes = []) {
|
|
|
|
|
if (!Array.isArray(suffixes)) {
|
|
|
|
|
if (typeof suffixes != 'string')
|
|
|
|
|
throw new Error('suffixes is not of type array or string');
|
2023-11-19 16:32:20 +00:00
|
|
|
|
2024-01-11 23:27:10 +00:00
|
|
|
suffixes = [suffixes];
|
2023-11-19 16:32:20 +00:00
|
|
|
}
|
2024-01-11 23:27:10 +00:00
|
|
|
const selector = this.#internals.get('settings').get('selector');
|
2023-11-19 16:32:20 +00:00
|
|
|
|
2024-01-11 23:27:10 +00:00
|
|
|
if (suffixes.length == 0) suffixes = [''];
|
2023-11-19 16:32:20 +00:00
|
|
|
|
2024-01-11 23:27:10 +00:00
|
|
|
return [...suffixes]
|
|
|
|
|
.map((suffix) => {
|
|
|
|
|
const selectorString = [`${selector}`, suffix || null]
|
|
|
|
|
.filter((v) => !!v)
|
|
|
|
|
.join('-');
|
|
|
|
|
return [
|
|
|
|
|
`[data-${selectorString}]`,
|
|
|
|
|
`.${selectorString}`,
|
|
|
|
|
`[${selectorString}]`,
|
|
|
|
|
].join(',');
|
|
|
|
|
})
|
|
|
|
|
.join(',');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Queries the DOM for elements that match the given selector.
|
|
|
|
|
* @memberof Cetra
|
|
|
|
|
* @instance
|
|
|
|
|
* @method
|
|
|
|
|
* @param {string} selector - The selector string to query the DOM with.
|
|
|
|
|
* @returns {NodeList} A NodeList of matching DOM elements.
|
|
|
|
|
*/
|
|
|
|
|
queryElements(attributes = '', options = {}) {
|
|
|
|
|
return Array.from(
|
|
|
|
|
(options._parent || document).querySelectorAll(
|
|
|
|
|
options.selector || this.#selectors(attributes)
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
queryElement(attributes = '', options = {}) {
|
|
|
|
|
return (options._parent || document).querySelector(
|
|
|
|
|
options.selector || this.#selectors(attributes)
|
|
|
|
|
);
|
2023-11-19 16:32:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Retrieves the value of the specified option from the framework's settings.
|
|
|
|
|
* @memberof Cetra
|
|
|
|
|
* @instance
|
|
|
|
|
* @method
|
|
|
|
|
* @param {string} option - The name of the option to retrieve.
|
|
|
|
|
* @returns {*} The value of the specified option.
|
|
|
|
|
*/
|
|
|
|
|
getOption(key) {
|
|
|
|
|
if (typeof key != 'string')
|
|
|
|
|
throw new Error('given key is not a string!');
|
|
|
|
|
return this.#internals
|
|
|
|
|
.get('settings')
|
|
|
|
|
.get(key.toString().toLowerCase());
|
|
|
|
|
}
|
2024-01-11 23:27:10 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Initializes a component with the specified name and options.
|
|
|
|
|
* @memberof Cetra
|
|
|
|
|
* @instance
|
|
|
|
|
* @method
|
|
|
|
|
* @param {string} name - The name of the component to initialize.
|
|
|
|
|
* @param {object} options - The options for the component.
|
|
|
|
|
* @returns {boolean} Returns true if the component was successfully initialized, otherwise false.
|
|
|
|
|
*/
|
|
|
|
|
#initComponent(comp) {
|
|
|
|
|
if (!comp.matches(this.#selectors())) return null;
|
|
|
|
|
if (
|
|
|
|
|
comp.getAttribute(
|
|
|
|
|
`data-${this.#internals.get('settings').get('selector')}-id`
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
return null;
|
|
|
|
|
|
|
|
|
|
const compType =
|
|
|
|
|
comp.getAttribute(
|
|
|
|
|
`data-${this.#internals.get('settings').get('selector')}`
|
|
|
|
|
) ??
|
|
|
|
|
comp.getAttribute(this.#internals.get('settings').get('selector'));
|
|
|
|
|
|
|
|
|
|
if (typeof compType == 'undefined' || !compType) {
|
|
|
|
|
Logger.error(`no component type given`);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let compData = new Map([
|
|
|
|
|
[
|
|
|
|
|
'id',
|
|
|
|
|
Registry.register({
|
|
|
|
|
key: this.#secretKey,
|
|
|
|
|
parent: this.#identifier,
|
|
|
|
|
type: compType,
|
|
|
|
|
obj: comp,
|
|
|
|
|
}),
|
|
|
|
|
],
|
|
|
|
|
['cetraId', this.#identifier],
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// add id to the component
|
|
|
|
|
comp.setAttribute(this.#dataIdAttribute(), compData.get('id'));
|
|
|
|
|
|
|
|
|
|
compData.set(
|
|
|
|
|
'selector',
|
|
|
|
|
`[${this.#dataIdAttribute()}="${compData.get('id')}"]`
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (typeof compType != 'undefined' && compType) {
|
|
|
|
|
compData.set('type', compType);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
compData.set('variables', this.#initVariables(comp));
|
|
|
|
|
|
|
|
|
|
for (const [_id, plugin] of this.#internals.get('plugins')) {
|
|
|
|
|
if (!plugin || typeof plugin.initComponent !== 'function') continue;
|
|
|
|
|
plugin.initComponent(comp, compData);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const compInstance = new BaseComponent(compData);
|
|
|
|
|
this.#internals
|
|
|
|
|
.get('compIndex')
|
|
|
|
|
.set(compInstance.get('id'), compInstance);
|
|
|
|
|
this.emit('onAfterInitComponent', compInstance);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#dataIdAttribute() {
|
|
|
|
|
return `data-${this.#internals.get('settings').get('selector')}-id`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Initializes variables with the specified data from the component.
|
|
|
|
|
* @memberof Cetra
|
|
|
|
|
* @instance
|
|
|
|
|
* @method
|
|
|
|
|
* @param {HTMLElement} component - The component element containing the variables and their initial values.
|
|
|
|
|
* @returns {Map} The Map object containing the initialized variables.
|
|
|
|
|
*/
|
|
|
|
|
#initVariables(component) {
|
|
|
|
|
const variables = new Map();
|
|
|
|
|
|
|
|
|
|
// Find elements within the component that have the 'data-ctra-var' attribute
|
|
|
|
|
const varElements = component.querySelectorAll(this.#selectors('var'));
|
|
|
|
|
|
|
|
|
|
varElements.forEach((element) => {
|
|
|
|
|
const varData = element.getAttribute('data-ctra-var');
|
|
|
|
|
const [varName, varType, varDefault] = varData.split(
|
|
|
|
|
this.#internals.get('settings').get('varSeparator')
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Check if the variable is already set
|
|
|
|
|
if (variables.has(varName)) return;
|
|
|
|
|
|
|
|
|
|
let defaultValue = this.convertValue(varType, varDefault); // typeConversion[varType](varDefault); // Initialize with the default value from the attribute
|
|
|
|
|
|
|
|
|
|
// Check if the textContent of the element exists and differs from the given default value
|
|
|
|
|
if (
|
|
|
|
|
typeof varDefault == 'undefined' &&
|
|
|
|
|
element.textContent.trim() !== ''
|
|
|
|
|
) {
|
|
|
|
|
defaultValue = this.convertValue(
|
|
|
|
|
varType,
|
|
|
|
|
element.textContent.trim()
|
|
|
|
|
); // Convert the parsed value based on the specified type
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Set the variable with the determined initial value
|
|
|
|
|
variables.set(
|
|
|
|
|
varName,
|
|
|
|
|
new Map([
|
|
|
|
|
['default', defaultValue],
|
|
|
|
|
['type', varType],
|
|
|
|
|
])
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return variables;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* takes the value and converts it based on the given type definition
|
|
|
|
|
* @memberof Cetra
|
|
|
|
|
* @instance
|
|
|
|
|
* @method
|
|
|
|
|
* @param {string} type - the variable's type
|
|
|
|
|
* @param {mixed} value - the value we need to parse
|
|
|
|
|
* @returns {mixed} the converted (otherwise the same) value
|
|
|
|
|
*/
|
|
|
|
|
convertValue(type, value) {
|
|
|
|
|
const convertFn = this.#internals.get('types').get(type ?? 'string');
|
|
|
|
|
if (!convertFn) {
|
|
|
|
|
Logger.warn(`No conversion function found for type "${type}".`);
|
|
|
|
|
return value;
|
|
|
|
|
}
|
|
|
|
|
return convertFn(value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Observer related */
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* handler for mutation events
|
|
|
|
|
* @note when MutationObserver registers a mutation within the root element
|
|
|
|
|
* @memberof Cetra
|
|
|
|
|
* @instance
|
|
|
|
|
* @method
|
|
|
|
|
* @param {Array} mutations - the list of registered mutations
|
|
|
|
|
* @param {MutationObserver} _observer - the observer which registered the mutation(s)
|
|
|
|
|
*/
|
|
|
|
|
handleMutation(mutations, _observer) {
|
|
|
|
|
if (_observer != this) return;
|
|
|
|
|
|
|
|
|
|
console.log('ITS HAPPENING', mutations, _observer);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* iterates over a list of plugins and registers them within the framework
|
|
|
|
|
* @memberof Cetra
|
|
|
|
|
* @instance
|
|
|
|
|
* @method
|
|
|
|
|
* @private
|
|
|
|
|
* @param {Array} plugins - The plugins to register.
|
|
|
|
|
*/
|
|
|
|
|
#registerPlugins(plugins) {
|
|
|
|
|
if (!Array.isArray(plugins)) throw new Error('plugins is not an array');
|
|
|
|
|
for (const [identifier, plugin] of plugins) {
|
|
|
|
|
this.addPlugin(identifier, plugin);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Registers a plugin with the specified name and options.
|
|
|
|
|
* @memberof Cetra
|
|
|
|
|
* @instance
|
|
|
|
|
* @method
|
|
|
|
|
* @param {string} name - The name of the plugin.
|
|
|
|
|
* @param {object} options - The options for the plugin.
|
|
|
|
|
*/
|
|
|
|
|
addPlugin(identifier, plugin) {
|
|
|
|
|
const uniqueId = Registry.register({
|
|
|
|
|
key: this.#secretKey,
|
|
|
|
|
obj: plugin,
|
|
|
|
|
parent: this.#identifier,
|
|
|
|
|
});
|
|
|
|
|
if (!identifier) {
|
|
|
|
|
Logger.error('Plugin identifier is required');
|
|
|
|
|
Registry.unregister(uniqueId, this.#secretKey);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this.#internals.get('plugins').has(identifier)) {
|
|
|
|
|
Logger.log(`Overwrite plugin ${identifier}`);
|
|
|
|
|
Registry.unregister(uniqueId, this.#secretKey);
|
|
|
|
|
Registry.register({
|
|
|
|
|
key: this.#secretKey,
|
|
|
|
|
obj: plugin,
|
|
|
|
|
parent: this.#identifier,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!plugin) {
|
|
|
|
|
Logger.error(
|
|
|
|
|
`Plugin ${identifier} is not an url or a plugin instance or class`
|
|
|
|
|
);
|
|
|
|
|
Registry.unregister(uniqueId, this.#secretKey);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (typeof plugin === 'string') {
|
|
|
|
|
if (!this.#isUrl(plugin)) {
|
|
|
|
|
Logger.error(`Plugin ${identifier} has an invalid url: ${url}`);
|
|
|
|
|
Registry.unregister(uniqueId, this.#secretKey);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
Logger.info(`Plugin ${identifier} is loaded from ${plugin}`);
|
|
|
|
|
this.#internals.get('plugins').set(identifier, {
|
|
|
|
|
plugin,
|
|
|
|
|
key: uniqueId,
|
|
|
|
|
parent: this.#identifier,
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!['function', 'object'].includes(typeof plugin)) {
|
|
|
|
|
Logger.error('Plugin is not of type function or object');
|
|
|
|
|
Registry.unregister(uniqueId, this.#secretKey);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const pluginInstance = this.#initPlugin(plugin);
|
|
|
|
|
Registry.unregister(uniqueId, this.#secretKey);
|
|
|
|
|
if (!pluginInstance)
|
|
|
|
|
throw new TypeError('plugin is not of a valid plugin type');
|
|
|
|
|
|
|
|
|
|
Registry.register({
|
|
|
|
|
key: this.#secretKey,
|
|
|
|
|
obj: pluginInstance,
|
|
|
|
|
parent: this.#identifier,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.#internals.get('plugins').set(identifier, pluginInstance);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Executes an action on a component.
|
|
|
|
|
*
|
|
|
|
|
* @param {string} action - The action to be executed.
|
|
|
|
|
* @param {string} varName - The name of the variable to be updated.
|
|
|
|
|
* @param {string} componentId - The ID of the component.
|
|
|
|
|
* @param {any} e - The event object.
|
|
|
|
|
* @throws {Error} If the component with the given ID is not found.
|
|
|
|
|
*/
|
|
|
|
|
exec(action, varName, componentId, e) {
|
|
|
|
|
const comp = this.#internals.get('compIndex').get(componentId);
|
|
|
|
|
|
|
|
|
|
if (!comp) {
|
|
|
|
|
throw new Error(`component ${componentId} not found`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this.#internals.get('plugins').get('utils')[action]) {
|
|
|
|
|
const value = comp.getValue(varName);
|
|
|
|
|
comp.update([
|
|
|
|
|
{
|
|
|
|
|
old: value,
|
|
|
|
|
new: this.#internals
|
|
|
|
|
.get('plugins')
|
|
|
|
|
.get('utils')
|
|
|
|
|
[action](comp.getValue(varName)),
|
|
|
|
|
varName,
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-11-19 16:32:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default Cetra;
|