import Emitter from './Emitter'; import Logger from './Logger'; class Cetra extends Emitter { // class static globals static #instances = new Map(); static #uidIndex = new Set(); static #defaultSettings = new Map([ ['selector', 'ctra'], ['root', document.body], ]); #identifier; #root; #internals = new Map([ ['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(); this.#identifier = this.uid(); if (!(options instanceof Map) && typeof options == 'object') { options = new Map(Object.entries(options)); } for (const key of Cetra.#defaultSettings.keys()) { let value = options.get(key); if (typeof value == 'undefined') { value = Cetra.#defaultSettings.get(key); } this.#internals.get('settings').set(key, value); } for (const [key, cb] of options) { if (!key.startsWith('on') || typeof cb != 'function') continue; this.on(key, cb); } this.#init(); } /** * 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() { 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(); this.#root.setAttribute( `data-${this.#internals.get('settings').get('selector')}-id`, this.#identifier ); Cetra.#instances.set(this.#identifier, this); this.emit('onAfterInit', this); } /** * Registers default properties, transformer or anything related to the functionality * @returns {void} */ #registerDefaults() { } /** * 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; } } /** * Generates a (internally)unique identifier. * @param {number} [numGroups=1] - The number of groups to generate. * @param {number} [groupLength=6] - The length of each group. * @memberof Cetra * @method * @returns {string} The unique identifier. */ uid(numGroups = 1, groupLength = 6) { const usedIds = new Set(Cetra.#uidIndex.values()); let uid = undefined; while (!uid || usedIds.has(uid)) { uid = Array.from(Array(numGroups), () => Math.random() .toString(36) .substring(2, groupLength + 2) ).join(''); } Cetra.#uidIndex.add(uid); return uid; } /** * 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()); } } export default Cetra;