From d6f72d32b802d24dcb81f6a17dbc55a089a6c411 Mon Sep 17 00:00:00 2001 From: Benjamin Wegener Date: Tue, 16 Jan 2024 20:11:59 +0000 Subject: [PATCH] imp(cetra) better plugin handling; ignore components within templates; docs --- lib/core/Cetra.js | 244 ++++++++++++++++++++++++++-------------------- 1 file changed, 137 insertions(+), 107 deletions(-) diff --git a/lib/core/Cetra.js b/lib/core/Cetra.js index 192823e..76159fc 100644 --- a/lib/core/Cetra.js +++ b/lib/core/Cetra.js @@ -15,6 +15,8 @@ class Cetra extends Emitter { INVALID_PLUGIN_TYPE: 'INVALID_PLUGIN_TYPE', INVALID_COMMAND: 'INVALID_COMMAND', COMMAND_NOT_FOUND: 'COMMAND_NOT_FOUND', + PLUGIN_NOT_FOUND: 'PLUGIN_NOT_FOUND', + REGISTERED_PLUGIN_MISSING_EXEC: 'REGISTERED_PLUGIN_MISSING_EXEC', }; // class static globals static #instances = new Map(); @@ -157,6 +159,7 @@ class Cetra extends Emitter { const entityKey = Object.keys(Cetra.#entityTypes).find((key) => { return Cetra.#entityTypes[key] == entityType; }); + if (!entityKey) { Logger.error(Cetra.#ERRORS.INVALID_ENTITY_TYPE, entityType); return false; @@ -178,7 +181,7 @@ class Cetra extends Emitter { throw new TypeError(Cetra.#ERRORS.INVALID_ENTITY); } - if (typeof fn == 'function') { + if (Cetra.#isValidPlugin(fn)) { if (!(fn instanceof CetraPlugin)) { if (fn.name) { Registry.unregister(uniqueId, this.#secretKey); @@ -209,10 +212,14 @@ class Cetra extends Emitter { if (typeof fn === 'undefined') return false; switch (entityType) { case 'Plugin': - return fn instanceof CetraPlugin || typeof fn === 'function'; + return Cetra.#isValidPlugin(fn); case 'Command': + return typeof fn === 'string'; case 'Type': - return typeof fn === 'function'; + return ( + typeof fn === 'function' && + fn.constructor.name === 'Function' + ); default: return false; } @@ -353,13 +360,20 @@ class Cetra extends Emitter { * @param {string} identifier - The identifier of the plugin. * @returns {Promise|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(Cetra.#ERRORS.INVALID_IDENTIFIER, identifier); + #getPlugin(identifier) { + if (typeof identifier != 'string') { + Logger.error( + Cetra.#ERRORS.INVALID_IDENTIFIER, + identifier, + typeof identifier + ); return undefined; } - if (!this.#internals.get('plugins').has(identifier)) return undefined; + if (!this.#internals.get('plugins').has(identifier)) { + Logger.error(Cetra.#ERRORS.PLUGIN_NOT_FOUND, identifier); + return undefined; + } const plugin = this.#internals.get('plugins').get(identifier); @@ -444,6 +458,40 @@ class Cetra extends Emitter { }); } + /** + * Checks if a plugin is a valid Cetra plugin. + * @private + * @param {Function} klass - The plugin to be checked. + * @returns {boolean} - Returns true if the plugin is a valid Cetra plugin, otherwise false. + */ + static #isValidPlugin(klass) { + let isInstance = false, + isClass = false, + isFunction = false; + + if ( + klass instanceof CetraPlugin || + klass.constructor.name != 'Function' + ) { + isInstance = true; + } else if (typeof klass === 'function') { + isFunction = true; + try { + isInstance = new klass() instanceof CetraPlugin; + } catch (error) { + return false; + } + if (!isInstance) { + isClass = Object.prototype.isPrototypeOf.call( + CetraPlugin.prototype, + klass.prototype + ); + } + } + + return isInstance || isClass || isFunction; + } + /** * Initializes a plugin by registering it with the Cetra registry. * @@ -459,16 +507,15 @@ class Cetra extends Emitter { }); 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); + if (!Cetra.#isValidPlugin(klass)) { + if (klass.plugin) { + Registry.unregister(uniqueId, this.#secretKey); + return this.#initPlugin(klass.plugin); } + if (typeof klass == 'string' && this.#isUrl(klass)) + return klass; + throw new TypeError(Cetra.#ERRORS.INVALID_PLUGIN_TYPE); } @@ -480,6 +527,9 @@ class Cetra extends Emitter { } } + if (typeof klass.register === 'function') + return klass.register(uid); + // what else could it be? if (typeof klass !== 'object') throw new TypeError(Cetra.#ERRORS.INVALID_PLUGIN_TYPE); @@ -530,7 +580,9 @@ class Cetra extends Emitter { * @param {boolean} [recursive=true] - Indicates whether to recursively process child nodes. */ #readComponents() { - let comps = this.queryElements('', { _parent: this.#root }); + let comps = this.#queryElements('', { + _parent: this.#root, + }); if (!comps || comps.length == 0) return Logger.log('no components found (yet)'); @@ -539,7 +591,7 @@ class Cetra extends Emitter { comp.matches( `[data-${this.#internals .get('settings') - .get('selector')}-id]` + .get('selector')}-id]:not(template)` ) ) continue; @@ -589,7 +641,7 @@ class Cetra extends Emitter { * @param {string} selector - The selector string to query the DOM with. * @returns {NodeList} A NodeList of matching DOM elements. */ - queryElements(attributes = '', options = {}) { + #queryElements(attributes = '', options = {}) { return Array.from( (options._parent || document).querySelectorAll( options.selector || this.#selectors(attributes) @@ -597,7 +649,18 @@ class Cetra extends Emitter { ); } - queryElement(attributes = '', options = {}) { + /** + * Finds and returns the first element that matches the specified selector. + * If no selector is provided, it uses the selectors generated from the attributes. + * + * @param {string} attributes - The attributes used to generate the selectors. + * @param {Object} options - Additional options for querying the element. + * @param {Element} options._parent - The parent element to search within. Defaults to the document. + * @param {string} options.selector - The specific selector to use for querying the element. + * + * @returns {Element|null} The first element that matches the selector, or null if no match is found. + */ + #queryElement(attributes = '', options = {}) { return (options._parent || document).querySelector( options.selector || this.#selectors(attributes) ); @@ -641,12 +704,10 @@ class Cetra extends Emitter { comp.getAttribute( `data-${this.#internals.get('settings').get('selector')}` ) ?? - comp.getAttribute(this.#internals.get('settings').get('selector')); - - if (typeof compType == 'undefined' || !compType) { - Logger.error(Cetra.#ERRORS.INVALID_COMPONENT_TYPE); - return null; - } + comp.getAttribute( + this.#internals.get('settings').get('selector') + ) ?? + 'BaseComponent'; let compData = new Map([ [ @@ -771,8 +832,6 @@ class Cetra extends Emitter { */ handleMutation(mutations, _observer) { if (_observer != this) return; - - console.log('ITS HAPPENING', mutations, _observer); } /** @@ -786,75 +845,21 @@ class Cetra extends Emitter { #registerPlugins(plugins) { if (!Array.isArray(plugins)) throw new Error('plugins is not an array'); for (const [identifier, plugin] of plugins) { - this.addPlugin(identifier, plugin); + this.#addEntity('Plugin', identifier, plugin); + // 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, - }); + registerCommands(commands, identifier) { + if (!Array.isArray(commands)) + throw new TypeError(Cetra.#ERRORS.INVALID_COMMANDS_TYPE); - try { - if (!identifier) { - Logger.error(Cetra.#ERRORS.INVALID_IDENTIFIER); - Registry.unregister(uniqueId, this.#secretKey); - return; + for (const command of commands) { + if (typeof command != 'string') { + Logger.error(Cetra.#ERRORS.INVALID_COMMAND, command); + continue; } - - if (this.#internals.get('plugins').has(identifier)) { - Registry.unregister(uniqueId, this.#secretKey); - Registry.register({ - key: this.#secretKey, - obj: plugin, - parent: this.#identifier, - }); - } - - if (!plugin) throw new TypeError(Cetra.#ERRORS.INVALID_PLUGIN_TYPE); - - if (typeof plugin === 'string') { - if (!this.#isUrl(plugin)) - throw new TypeError(Cetra.#ERRORS.INVALID_PLUGIN_URL); - - 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)) - throw new TypeError(Cetra.#ERRORS.INVALID_PLUGIN_TYPE); - - const pluginInstance = this.#initPlugin(plugin); - - if (!pluginInstance) - throw new TypeError(Cetra.#ERRORS.INVALID_PLUGIN_TYPE); - - Registry.register({ - obj: pluginInstance, - key: this.#secretKey, - parent: this.#identifier, - }); - - this.#internals.get('plugins').set(identifier, pluginInstance); - this.emit('onAfterRegisterPlugin', pluginInstance); - } catch (err) { - Logger.error(err); - Registry.unregister(uniqueId, this.#secretKey); + this.#addEntity('Command', command, identifier); } } @@ -867,7 +872,10 @@ class Cetra extends Emitter { * @param {any} e - The event object. * @throws {Error} If the component with the given ID is not found. */ - exec(componentId, update = { command, args, evt }) { + exec( + componentId, + update = { command: 'none', args: [], evt: new CustomEvent('empty') } + ) { if (Array.isArray(update.args)) update.args = []; const comp = this.#internals.get('compIndex').get(componentId); @@ -880,22 +888,44 @@ class Cetra extends Emitter { return; } - this.#internals - .get('commands') - .get(update.command) - .forEach((command) => { - if (!command.exec) { - if (typeof command != 'function' || !command.name) { - Logger.error( - Cetra.#ERRORS.INVALID_COMMAND, - update.command - ); - } - command(comp, update); - } + const command = this.#internals.get('commands').get(update.command); - command.exec(comp, update); - }); + if (!command) + return Logger.error( + Cetra.#ERRORS.COMMAND_NOT_FOUND, + update.command, + update + ); + + if (typeof command !== 'string') { + if (typeof command.constructor.name !== 'Function') { + return Logger.error( + Cetra.#ERRORS.INVALID_COMMAND, + update.command + ); + } + + return command(comp, update); + } + + const plugin = this.#getPlugin(command); + + if (!plugin) + return Logger.error( + Cetra.#ERRORS.PLUGIN_NOT_FOUND, + update.command, + update, + plugin + ); + if (!plugin.exec) + return Logger.error( + Cetra.#ERRORS.REGISTERED_PLUGIN_MISSING_EXEC, + update.command, + update, + plugin + ); + + plugin.exec(comp, update); } }