265 lines
7.0 KiB
JavaScript
265 lines
7.0 KiB
JavaScript
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<HTMLElement>} - 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);
|
|
}
|
|
}
|