import Cetra from '../../lib/lib.js'; /** * Represents a base component class. */ export default class BaseComponent { #data = new Map([ ['type', 'BaseComponent'], ['selector', 'cetra-component'], ['variables', new Map()], ['cetraId', ''], ]); /** * Constructs a new instance of the BaseComponent class. * @param {Map} data - The initial data for the component. */ constructor(data = new Map()) { for (const key of this.#data.keys()) { if ( !data.has(key) || typeof data.get(key) != typeof this.#data.get(key) ) continue; this.#data.set(key, data.get(key)); } this.#data = data; } /** * Retrieves the value associated with the specified key from the data map. * @param {string} key - The key to retrieve the value for. * @returns {*} - The value associated with the key. */ get(key) { return this.#data.get(key); } /** * Sets a key-value pair in the component's data. * @param {string} key - The key to set. * @param {*} value - The value to set. * @returns {BaseComponent} - The updated component instance. */ set(key, value) { this.#data.set(key, value); return this; } /** * Retrieves the value of a variable by its name. * * @param {string} varName - The name of the variable. * @returns {*} - The value of the variable, or null if the variable is not found. * @throws {Error} - If no Cetra instance is found. */ getValue(varName) { if (typeof varName != 'string') return null; if (varName.startsWith('$')) varName = varName.slice(1); if (!this.get('variables').has(varName)) return null; const frameWork = Cetra.getInstance(this.get('cetraId')); if (!frameWork) throw new Error('no cetra instance found'); const varValue = frameWork.convertValue( this.get('variables').get(varName).get('type'), this.getVarNodes(varName).shift().textContent ); return varValue; } /** * Retrieves the value of a variable. * @param {string} varName - The name of the variable. * @returns {*} - The value of the variable. */ getVal(varName) { return this.getValue(varName); } /** * Retrieves the value of a variable by its name. * * @param {string} varName - The name of the variable. * @returns {*} - The value of the variable. */ getVar(varName) { return this.get('variables').get(varName); } /** * Retrieves the DOM node associated with the component. * @returns {HTMLElement} The DOM node. * @throws {Error} If no node is found. */ getNode() { let node = document.querySelector(this.get('selector')); if (!node) node = document.getElementById(this.get('id')); if (!node) throw new Error('no node found'); return node; } /** * Retrieves all DOM nodes that have a data attribute matching the specified variable name. * @param {string} varName - The name of the variable to search for. * @returns {Array} - An array of DOM nodes matching the variable name. * @throws {Error} - Throws an error if no cetra instance is found. */ getVarNodes(varName) { const frameWork = Cetra.getInstance(this.get('cetraId')); if (!frameWork) throw new Error('no cetra instance found'); const varNodeSelector = `[data-${frameWork.getOption( 'selector' )}-var^="${varName}"]`; return [...this.getNode().querySelectorAll(varNodeSelector)]; } /** * Updates the component with the given deltas. * @param {Array} deltas - An array of deltas to apply. * @throws {TypeError} If deltas is not an array. * @throws {Error} If no cetra instance is found. */ update(deltas) { if (!Array.isArray(deltas)) throw new TypeError('deltas must be an array'); const frameWork = Cetra.getInstance(this.get('cetraId')); if (!frameWork) throw new Error('no cetra instance found'); for (const delta of deltas) { const { new: value, old: oldValue, varName } = delta; if (!this.get('variables').has(varName)) continue; const variable = this.get('variables').get(varName); this.getVarNodes(varName).forEach((el) => { el.textContent = value ?? variable.get('default'); }); } } } class NewCetra { constructor(options = {}) { this.wrapper = options.wrapper || document.body; this.componentMap = new Map(); this.variableMap = new Map(); this.observer = null; } init() { this.discoverComponents(); this.setupObserver(); this.triggerHook('afterInit'); } registerComponent(name, component) { this.componentMap.set(name, component); } getComponent(name) { return this.componentMap.get(name); } mountComponent(element) { const componentName = element.getAttribute('data-ctra'); const component = this.getComponent(componentName) || this.getComponent('BaseComponent'); if (!component) { console.warn(`Component "${componentName}" not found.`); return; } const instance = new component(element); element.__ctra_instance = instance; this.triggerHook('beforeComponentMount', instance); instance.mount(); this.triggerHook('afterComponentMount', instance); } unmountComponent(element) { const instance = element.__ctra_instance; if (!instance) { console.warn('Unmounting a non-mounted component.'); return; } this.triggerHook('beforeComponentUnmount', instance); instance.unmount(); this.triggerHook('afterComponentUnmount', instance); delete element.__ctra_instance; } updateComponent(element) { const instance = element.__ctra_instance; if (!instance) { console.warn('Updating a non-mounted component.'); return; } this.triggerHook('beforeComponentUpdate', instance); instance.update(); this.triggerHook('afterComponentUpdate', instance); } getVariable(name) { return this.variableMap.get(name); } setVariable(name, value) { this.variableMap.set(name, value); this.updateVariableBindings(name); } discoverComponents() { const elements = this.wrapper.querySelectorAll('[data-ctra]'); elements.forEach(element => this.mountComponent(element)); } setupObserver() { this.observer = new MutationObserver(mutations => { mutations.forEach(mutation => { if (mutation.type === 'childList') { mutation.addedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE && node.hasAttribute('data-ctra')) { this.mountComponent(node); } }); mutation.removedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE && node.__ctra_instance) { this.unmountComponent(node); } }); } else if (mutation.type === 'attributes' && mutation.attributeName === 'data-ctra') { this.updateComponent(mutation.target); } }); }); this.observer.observe(this.wrapper, { childList: true, subtree: true, attributes: true, attributeFilter: ['data-ctra'] }); } updateVariableBindings(name) { const elements = this.wrapper.querySelectorAll(`[data-ctra-var="${name}"]`); elements.forEach(element => { const value = this.getVariable(name); element.textContent = value; }); } triggerHook(name, ...args) { // Placeholder for triggering lifecycle hooks console.log(`Triggering hook: ${name}`, ...args); } }