From 7e7c2a76a0cca59746faf63936852c50bc5073db Mon Sep 17 00:00:00 2001 From: levizwannah Date: Sun, 23 Jul 2023 19:51:49 +0300 Subject: [PATCH] fixed events bugs, added Mediators, EventData, Listener, and MediatorManager, and Broker. Ensured that the DOM the components always bind and proper exporting of component fuctions --- OpenScript.js | 599 ++++++++++++++++++++++++++++++++++++++++++++++---- ojs-config.js | 100 +++++++++ 2 files changed, 655 insertions(+), 44 deletions(-) create mode 100644 ojs-config.js diff --git a/OpenScript.js b/OpenScript.js index f5d800d..82a377e 100644 --- a/OpenScript.js +++ b/OpenScript.js @@ -323,7 +323,7 @@ var OpenScript = { /** * Event Emitter Class */ - Emitter: class { + Emitter: class Emitter { listeners = {} /** @@ -372,7 +372,7 @@ var OpenScript = { // Fire the event emit(eventName, ...args) { - this.emitted[eventName] = true; + this.emitted[eventName] = args; let fns = this.listeners[eventName]; if (!fns) return false; @@ -401,10 +401,318 @@ var OpenScript = { } }, + + /** + * The Broker Class + */ + Broker: class Broker { + + /** + * TIME DIFFERENCE BEFORE GARBAGE + * COLLECTION + */ + CLEAR_LOGS_AFTER = 10000; + + /** + * TIME TO GARBAGE COLLECTION + */ + TIME_TO_GC = 30000; + + /** + * The event listeners + * event: {time:xxx, args: xxx} + */ + #logs = {}; + + /** + * The emitter + */ + #emitter = new OpenScript.Emitter(); + + /** + * Add Event Listeners + * @param {string} event + * @param {function} listener - asynchronous function + */ + on(event, listener) { + + if(this.#logs[event]){ + let emitted = this.#logs[event]; + + for(let i = 0 ; i < emitted.length; i++){ + listener(...emitted[i].args); + } + } + + return this.#emitter.on(event, listener); + } + + /** + * Emits an event + * @param {string} event + * @param {...any} args + * @returns + */ + async send(event, ...args) { + return this.emit(event, ...args); + } + + /** + * Emits Events + * @param {string} event + * @param {...any} args + * @returns + */ + async emit(event, ...args){ + const currentTime = () => (new Date()).getTime(); + + this.#logs[event] = this.#logs[event] ?? []; + this.#logs[event].push({timestamp: currentTime(), args: args}); + + return this.#emitter.emit(event, ...args); + } + + /** + * Clear the logs + */ + clearLogs(){ + + for(let event in this.#logs){ + let d = new Date(); + let k = -1; + + for(let i in this.#logs[event]) { + if( + (d.getTime() - this.#logs[event][i].timestamp) + >= this.TIME_TO_GC + ) { k = i; } + } + + if(k !== -1){ + this.#logs[event] = this.#logs[event].slice(k + 1); + } + + if(this.#logs[event].length < 1) delete this.#logs[event]; + } + } + + /** + * Do Events Garbage Collection + */ + removeStaleEvents(){ + setInterval(this.clearLogs.bind(this), this.CLEAR_LOGS_AFTER); + } + + }, + + /** + * The Mediator Manager + */ + MediatorManager: class MediatorManager { + + static directory = "./mediators"; + static version = "1.0.0"; + mediators = new Map(); + + /** + * Fetch Mediators from the Backend + * @param {string} qualifiedName + */ + async fetchMediators(qualifiedName){ + + let Mediator = await (new OpenScript + .AutoLoader( + OpenScript. + MediatorManager. + directory, + OpenScript + .MediatorManager + .version )).include(qualifiedName); + + + if(!Mediator) { + Mediator = new Map([qualifiedName, ['_', OpenScript.Mediator]]); + } + + for(let [k, v] of Mediator) { + + try{ + if(this.mediators.has(k)) continue; + + let mediator = new v[1](); + mediator.register(); + + this.mediators.set(k, mediator); + } + catch(e) { + console.error(`Unable to load '${k}' Mediator.`, e); + } + } + } + }, + + /** + * The Mediator Class + */ + Mediator: class Mediator { + + async register(){ + + let obj = this; + let seen = new Set(); + + do { + if(!(obj instanceof OpenScript.Mediator)) break; + + for(let method of Object.getOwnPropertyNames(obj)){ + + if(seen.has(method)) continue; + + if(typeof this[method] !== "function") continue; + if(method.length < 3) continue; + + if(method.substring(0, 2) !== "$$") continue; + + let events = method.substring(2).split(/_/g).filter(a => a.length > 0); + + for(let ev of events){ + if(ev.length === 0) continue; + + broker.on(ev, this[method].bind(this)); + } + + seen.add(method); + } + } + while (obj = Object.getPrototypeOf(obj)); + } + + /** + * Emits an event through the broker + * @param {string} event + * @param {...string} args data to send + */ + send(event, ...args) { + broker.send(event, ...args); + return this; + } + + /** + * parses a JSON string + * `JSON.parse` + * @param {string} JSONString + * @returns + */ + parse(JSONString){ + return JSON.parse(JSONString); + } + + /** + * Stringifies a JSON Object + * `JSON.stringify` + * @param {object} object + * @returns + */ + stringify(object){ + return JSON.stringify(object) + } + }, + + /** + * A Broker Listener + */ + Listener: class Listener { + + /** + * Registers with the broker + */ + async register(){ + let obj = this; + let seen = new Set(); + + do { + if(!(obj instanceof OpenScript.Listener)) break; + + for(let method of Object.getOwnPropertyNames(obj)){ + if(seen.has(method)) continue; + + if(typeof this[method] !== "function") continue; + if(method.length < 3) continue; + + if(method.substring(0, 2) !== "$$") continue; + + let events = method.substring(2).split(/_/g).filter(a => a.length > 0); + + for(let ev of events){ + if(ev.length === 0) continue; + + broker.on(ev, this[method].bind(this)); + } + + seen.add(method); + } + } + while (obj = Object.getPrototypeOf(obj)); + } + }, + + /** + * The Event Data class + */ + EventData: class EventData { + + /** + * The Meta Data + */ + _meta = {}; + + /** + * Message containing the args + */ + _message = {}; + + meta(data) { + this._meta = data; + return this; + } + + message(data) { + this._message = data; + return this; + } + + /** + * Convert the Event Schema to string + * @returns {string} + */ + encode(){ + return JSON.stringify(this); + } + + /** + * JSON.parse + * @param {string} string + * @returns {EventData} + */ + static decode(string){ + return JSON.parse(string); + } + /** + * Parse and Event Data + * @param {string} eventData + * @returns + */ + static parse(eventData){ + let ed = OpenScript.EventData.decode(eventData); + + return {meta: ed._meta, message: ed._message}; + } + }, + /** * Base Component Class */ - Component: class { + Component: class Component { /** * List of events that the component emits @@ -467,6 +775,11 @@ var OpenScript = { */ visible = true; + /** + * Keeps track of why the + * component is made visible or hidden + */ + visibleBy = 'parent'; /** * Anonymous component ID @@ -489,6 +802,7 @@ var OpenScript = { emitter = new OpenScript.Emitter(); constructor(name = null) { + this.name = name ?? this.constructor.name; this.emitter.once(this.EVENTS.rendered, (th) => th.rendered = true); @@ -496,7 +810,39 @@ var OpenScript = { this.emitter.on(this.EVENTS.rerendered, (th) => th.rerendered = true); this.emitter.on(this.EVENTS.bound, (th) => th.bound = true); this.emitter.on(this.EVENTS.mounted, (th) => th.mounted = true); - this.emitter.on(this.EVENTS.visible, (th) => th.visible = true); + this.emitter.on(this.EVENTS.visible, (th) => th.visible = true); + + this.getDeclaredListeners(); + } + + /** + * Make the component's method accessible from the + * global window + * @param {string} methodName - the method name + * @param {[*]} args - arguments to pass to the method + * To pass a literal string param use '${param}' in the args. + * For example ['${this}'] this will reference the DOM element. + */ + method(name, args){ + if(!Array.isArray(args)) args = [args]; + + return h.func(this, name, ...args); + } + + /** + * Get an external Component's method + * to add it to a DOM Element + * @param {string} componentMethod `Component.method` e.g. 'MainNav.notify' + * @param {[*]} args + */ + xMethod(componentMethod, args){ + let splitted = componentMethod.trim().split(/\./).map(a => a.trim()); + + if(splitted.length < 2) { + console.error(`${componentMethod} has syntax error. Please use ComponentName.methodName`); + } + + return component(splitted[0]).method(splitted[1], args); } /** @@ -581,21 +927,10 @@ var OpenScript = { component.doNotListenTo(this, event); } - /** - * Initializes the component and adds it to - * the component map of the markup engine - * @emits mounted - * @emits pre-mount + * Get all Emitters declared in the component */ - async mount() { - h.component(this.name, this); - - this.claimListeners(); - this.emit(this.EVENTS.premount); - await this.bind(); - this.emit(this.EVENTS.mounted); - + getDeclaredListeners(){ let obj = this; let seen = new Set(); @@ -608,7 +943,19 @@ var OpenScript = { if(typeof this[method] !== "function") continue; if(method.length < 3) continue; - if(method[0] !== '$' && method[1] !== "_") continue; + if(method[0] !== '$') continue; + + if(method[1] !== "$" && method[1] !== "_") continue; + + if(method[1] === "$"){ + let events = method.substring(2).split(/_/g).filter((a) => a.length); + + for(let i = 0; i < events.length; i++){ + broker.on(events[i], this[method].bind(this)); + } + + continue; + } let meta = method.substring(1).split(/\$/g); @@ -648,16 +995,14 @@ var OpenScript = { let ev = events[j]; if(!ev.length) continue; - + h[m](cmp, ev, (component, event, ...args) => { - try{ - h.getComponent(cmpName)[method](h.getComponent(cmpName), component, event, ...args); + h.getComponent(cmpName)[method]?.bind(h.getComponent(cmpName))(component, event, ...args); } catch(e){ console.error(e); } - }); } } @@ -666,7 +1011,21 @@ var OpenScript = { seen.add(method); } } - while (obj = Object.getPrototypeOf(obj)); + while (obj = Object.getPrototypeOf(obj)); + } + /** + * Initializes the component and adds it to + * the component map of the markup engine + * @emits mounted + * @emits pre-mount + */ + async mount() { + h.component(this.name, this); + + this.claimListeners(); + this.emit(this.EVENTS.premount); + await this.bind(); + this.emit(this.EVENTS.mounted); } /** @@ -688,6 +1047,44 @@ var OpenScript = { return true; } + /** + * Checks if this component has + * elements on the dom and if they are + * visible + */ + checkVisibility(){ + + let elem = h.dom.querySelector(`ojs-${this.kebab(this.name)}`); + + if(elem && elem.parentElement?.style.display !== 'none' && !this.visible){ + return this.show(); + } + + if(elem && elem.parentElement?.style.display === 'none' && this.visible){ + return this.hide(); + } + + if(elem && + ( + elem.style.display !== 'none' && + elem.style.visibility !== 'hidden' + ) && + !this.visible + ) { + this.show(); + } + + if(( + !elem || + ( + elem.style.display === 'none' || + elem.style.visibility === 'hidden' + ) + ) && this.visible) { + this.hide(); + } + } + /** * Emits an event * @param {string} event @@ -709,6 +1106,11 @@ var OpenScript = { let all = h.dom.querySelectorAll(`ojs-${this.kebab(this.name)}-tmp--`); + if(all.length < 1) { + setTimeout(this.bind.bind(this), 500); + return; + } + for(let elem of all) { let hId = elem.getAttribute('ojs-key'); @@ -802,7 +1204,7 @@ var OpenScript = { // check if we have previously emitted this event listeners.forEach(a => { - if(this.emitter.emitted[event]) a(this, event); + if(event in this.emitter.emitted) a(...this.emitter.emitted[event]); this.emitter.on(event, a); }); @@ -929,6 +1331,8 @@ var OpenScript = { let current = h.dom.querySelectorAll(`ojs-${this.kebab(this.name)}[s-${state.id}="${state.id}"]`) ?? []; current.forEach(e => { + if(!this.visible) e.style.display = 'none'; + else e.style.display = ''; e.textContent = ""; let arg = this.argsMap.get(e.getAttribute("uuid")); @@ -966,12 +1370,14 @@ var OpenScript = { if(!this.visible) attr.style = 'display: none;'; const markup = this.render(...args); - - return h[`ojs-${this.kebab(this.name)}`](attr, markup, { + + attr = {...attr, component: this, event, eventParams: [markup] - }); + }; + + return h[`ojs-${this.kebab(this.name)}`](attr, markup); } /** @@ -1079,7 +1485,7 @@ var OpenScript = { this.put(referenceName, qualifiedName, fetch); - return referenceName.length == 1 ? this.map.get(referenceName[0]) : this.map; + return referenceName.length === 1 ? this.map.get(referenceName[0]) : this.map; } /** @@ -1131,14 +1537,14 @@ var OpenScript = { this.map.set(key, cxt); } catch(e) { - console.error(`Unable to load ${referenceName} because it already exists in the window. Please ensure that you are loading your contexts before your components`, e); + console.error(`Unable to load '${referenceName}' context because it already exists in the window. Please ensure that you are loading your contexts before your components`, e); } counter++; } } else { - console.log(`${referenceName[0]} already exists. If you have multiple contexts in the file in ${qualifiedName}, then you can use context('[contextName]Context') to access them.`) + console.warn(`[${referenceName}] context already exists. If you have multiple contexts in the file in ${qualifiedName}, then you can use context('[contextName]Context') or the aliases you give them to access them.`) } return this.context(referenceName); @@ -1697,6 +2103,15 @@ var OpenScript = { return ojsUtils.camel(name, true); } + const checkComponentsVisibility = async () => { + + for(let [k, comp] of this.compMap) { + comp.checkVisibility(); + } + + return true; + } + /** * @type {DocumentFragment|HTMLElement} */ @@ -1789,7 +2204,7 @@ var OpenScript = { if(Array.isArray(arg)) { if(isComponent) continue; arg.forEach(e => { - rootFrag.append(this.toElement(e)); + if(e) rootFrag.append(this.toElement(e)); }); continue; } @@ -1830,6 +2245,9 @@ var OpenScript = { else { parent.append(root); } + + checkComponentsVisibility(); + if(component){ component.emit(event, eventParams); @@ -1837,18 +2255,21 @@ var OpenScript = { sc.forEach(c => { if(!isComponentName(c.tagName.toLowerCase())) return; - + let cmpName = getComponentName(c.tagName); + let cmp = h.getComponent(getComponentName(c.tagName)); - if(!cmp || cmp.listensTo(component, event)) return; + if(cmp && cmp.listensTo(component, event)) return; h.onAll(component.name, event, () => { - cmp.emit(event, eventParams); + h.getComponent(cmpName)?.emit(event, eventParams); }); + if(!cmp) return; component.addListeningComponent(cmp, event); }); } + return root; } @@ -1878,16 +2299,24 @@ var OpenScript = { } /** + * * adds quotes to string arguments * and serializes objects for * param passing + * @note To escape adding quotes use ${string} */ _escape = (args) => { let final = []; for(let e of args) { if(typeof e === "number") final.push(e); - else if(typeof e === "string") final.push(`'${e}'`); + else if(typeof e === "string") { + if(e.length && e.substring(0, 2) === '${'){ + let length = e[e.length - 1] === '}' ? e.length - 1 : e.length; + final.push(e.substring(2, length)); + } + else final.push(`'${e}'`); + } else if(typeof e === "object") final.push(JSON.stringify(e)); } @@ -2066,7 +2495,7 @@ var OpenScript = { /** * Keeps track of the files that have been loaded */ - history = new Map(); + static history = new Map(); /** * The Directory or URL in which all JS files are located @@ -2248,7 +2677,7 @@ var OpenScript = { let names = fileName.split(/\./); - if(this.history.has(`${this.dir}.${fileName}`)) return this.history.get(`${this.dir}.${fileName}`); + if(OpenScript.AutoLoader.history.has(`${this.dir}.${fileName}`)) return OpenScript.AutoLoader.history.get(`${this.dir}.${fileName}`); let response = await fetch(`${this.dir}/${this.normalize(fileName)}${this.extension}?v=${this.version}`); @@ -2303,7 +2732,6 @@ var OpenScript = { let signature = classes.get(arr[0]).signature; let replacement = signature.replace(original, parent); - //console.log(k, arr[1]); let c = arr[1].replace(signature, replacement); arr[1] = c; @@ -2321,7 +2749,7 @@ var OpenScript = { } } - this.history.set(`${this.dir}.${fileName}`, codeMap); + OpenScript.AutoLoader.history.set(`${this.dir}.${fileName}`, codeMap); return codeMap; } @@ -2361,7 +2789,7 @@ var OpenScript = { if(h.has(c.name)) return; await c.mount(); - } + } return content; } @@ -2408,7 +2836,7 @@ var OpenScript = { /** * Initializes the OpenScript */ - Initializer: class { + Initializer: class Initializer { /** * Wrapper to write OJS codes in @@ -2478,6 +2906,11 @@ var OpenScript = { */ Router = OpenScript.Router; + /** + * The mediator manager + */ + mediatorManager = new OpenScript.MediatorManager(); + /** * The router object */ @@ -2486,7 +2919,8 @@ var OpenScript = { constructor( configs = { directories: { components: "./components", - contexts: "./contexts" + contexts: "./contexts", + mediators: "./mediators" }, version: "1.0.0" @@ -2500,6 +2934,7 @@ var OpenScript = { OpenScript.ContextProvider.version = configs.version; this.contextProvider = this.createContextProvider(); + /** * * @param {string} name @@ -2507,6 +2942,9 @@ var OpenScript = { */ this.context = (name) => this.contextProvider.context(name); + OpenScript.MediatorManager.directory = configs.directories.mediators; + OpenScript.MediatorManager.version = configs.version; + } /** @@ -2573,6 +3011,47 @@ var OpenScript = { fetchContext = (referenceName, qualifiedName) => { return this.contextProvider.load(referenceName, qualifiedName, true); } + + /** + * Gets a component + * @returns {OpenScript.Component} + */ + component = (name) => h.getComponent(name); + + /** + * Loads mediators + * @param {Array} names + */ + mediators = async (names) => { + for(let qn of names) { + this.mediatorManager.fetchMediators(qn); + } + } + + /** + * The Broker Object + */ + broker = new OpenScript.Broker(); + + /** + * The Mediator Manager Class + */ + MediatorManager = OpenScript.MediatorManager; + + /** + * The Event Data Class + */ + EventData = OpenScript.EventData; + + /** + * Creates an event data + * @param {object} meta + * @param {object} message + * @returns {string} encoded EventData + */ + eData = (meta = {}, message = {}) => { + return new OpenScript.EventData().meta(meta).message(message).encode(); + } } } @@ -2675,8 +3154,40 @@ const { /** * The OJS utility class */ - Utils: ojsUtils + Utils: ojsUtils, -} = new OpenScript.Initializer(); + /** + * Gets a Component + */ + component, + + /** + * The Mediator Manager + */ + mediatorManager, + /** + * The Mediator Manager + */ + MediatorManager, + /** + * Fetch Mediators + */ + mediators, + + /** + * The Broker Object + */ + broker, + + /** + * The Event Data Class + */ + EventData, + + /** + * Creates an event data object + */ + eData +} = new OpenScript.Initializer(); \ No newline at end of file diff --git a/ojs-config.js b/ojs-config.js new file mode 100644 index 0000000..e70b6e1 --- /dev/null +++ b/ojs-config.js @@ -0,0 +1,100 @@ +/*---------------------------------- + | Do OpenScript Configurations Here + |---------------------------------- +*/ + +/**---------------------------------- + * + * Set the default route path here + * ---------------------------------- + */ +route.basePath(''); // === '/' + +/*----------------------------------- + | set the directories in which we + | can find the context files + |----------------------------------- +*/ +ContextProvider.directory = route.baseUrl('js/contexts'); + +/*----------------------------------- + | set the version number of the + | context files so that we can + | always load the new files incase + | files change + |----------------------------------- +*/ +ContextProvider.version = '1.0.0'; + +/*----------------------------------- + | Set the Mediators directory + | so that we an load the mediators + | from that directory + |----------------------------------- +*/ +MediatorManager.directory = route.baseUrl('js/mediators'); + +/*----------------------------------- + | Set the version number of the + | mediator files so that we can + | always load a fresh copy of the + | mediators files upon changes. + |---------------------------------- +*/ +MediatorManager.version = '1.0.0'; + +/*----------------------------------- + | Set the default component + | directory for the loader + |----------------------------------- +*/ +loader.dir = route.baseUrl('js/components'); + +/*----------------------------------- + | set the version number of the + | component files so that we load + | a fresh file when they change + |----------------------------------- +*/ +loader.version = '1.0.0'; + +/*----------------------------------- + | Set the default directory of the + | autoload object for loading + | files. + |----------------------------------- +*/ + +autoload.dir = route.baseUrl('js/classes'); + +/*----------------------------------- + | set the version number of the + | JS files so that we load + | a fresh file when they change + |----------------------------------- +*/ +autoload.version = '1.0.0'; + +/*-------------------------------- + | Set the logs clearing interval + | for the broker to remove stale + | events. (milliseconds) + |-------------------------------- +*/ +broker.CLEAR_LOGS_AFTER = 30000; // 30 secs + +/*-------------------------------- + | Set how old an event must be + | to be deleted from the broker's + | event log during logs clearing + |-------------------------------- +*/ +broker.TIME_TO_GC = 10000; // 10 secs + + +/*------------------------------------------- + | Start the garbage + | collector for the broker + |------------------------------------------- +*/ +broker.removeStaleEvents(); // broker garbage collection started \ No newline at end of file