UNPKG

@twobirds/microcomponents

Version:

Micro Components Organization Class

379 lines (374 loc) 13 kB
'use strict'; import { McEvent } from './McEvent.js'; var isBrowser = new Function('try {return this===window;}catch(e){ return false;}'); let nativeEventNames = new Set(); if (isBrowser()) { Object.keys(HTMLElement.prototype) .filter((key) => /^on/.test(key)) .forEach((eventName) => nativeEventNames.add(eventName)); } function getMicroComponents(elements, search) { let mcValues = []; elements.forEach((e) => { if (e) { if (e instanceof HTMLElement) { const _mc = e?._mc; if (!search && _mc) { mcValues = mcValues.concat(Object.values(e._mc)); } else if (search && _mc && _mc[search]) { mcValues.push(_mc[search]); } } } }); return mcValues; } function traverseNodes(node, search, firstonly = true) { let mcValues = []; while (node) { if (node._mc) { if (!search) { mcValues = mcValues.concat(Object.values(node._mc)); } else if (search && node._mc[search]) { mcValues.push(node._mc[search]); } if (firstonly) return mcValues; } node = node.parentNode; } return mcValues; } function isDOM(element) { return element && element instanceof HTMLElement; } function addListener(domNode, eventName, handler, capture = false, once = false) { let options = { once: false }; if (capture) { options.capture = capture; } if (once) { options.once = once; } domNode.addEventListener(eventName, handler, options); } function removeListener(domNode, eventName, handler) { domNode.removeEventListener(eventName, handler); } function add(target, key, element = {}) { let node = target; if (!node._mc) { let debug = DC?.debug; DC.debug = false; node._mc = new DC(target); DC.debug = debug; delete node._mc._mc; } const _mc = node._mc; if (key.length && !_mc[key]) { _mc[key] = element; if (target instanceof HTMLElement && !target.getAttribute('_mc')) { target.setAttribute('_mc', ''); } } return element; } function remove(target, key) { if (!key) return; const _mc = target?._mc || {}; if (_mc && _mc[key]) { delete _mc[key]; } if (!Object.keys(_mc).length && target instanceof HTMLElement && target.hasAttribute('_mc')) { target.removeAttribute('_mc'); } return; } function trigger(target, ev, data = {}, bubble = 'l') { let _mc = target?._mc; if (!_mc) return; let event = ev instanceof McEvent ? new McEvent(ev.type, ev.data, 'l') : new McEvent(ev, data, 'l'), mcEvent = typeof ev !== 'string' ? ev : new McEvent(ev, data, bubble); [...Object.values(_mc)].forEach((mc) => { if (mc?.trigger && typeof mc?.trigger === 'function') { mc?.trigger(event); } }); if (/[ud]/.test(mcEvent.bubble)) { bubbleEvent(target, mcEvent); } return; } function init(element) { if (element.constructor === DC) return; setTimeout(() => { element.trigger('Init'); }, 0); } function autoAttachListeners(element) { if (!isBrowser() || element.constructor === DC) return; const target = element.target; if (target instanceof HTMLElement) { let that = element; Object.getOwnPropertyNames(that.constructor.prototype) .filter((key) => /^on[A-Z]|one[A-Z]/.test(key) && typeof that.constructor.prototype[key] === 'function') .forEach((key) => { let once = /^one/.test(key), withoutOnOne = key.replace(/^on[e]{0,1}/, ''), nativeEventName = withoutOnOne.toLowerCase(), listener = (ev) => { that.trigger(withoutOnOne, ev); }; if (once) { Object.defineProperty(element, 'on' + withoutOnOne, { configurable: true, enumerable: false, writable: true, value: (ev) => { that.constructor.prototype[key].bind(that)(ev); }, }); } if (nativeEventNames.has('on' + nativeEventName)) { addListener(target, nativeEventName, listener, false, once); } }); } } function bubbleEvent(target, ev) { if (/[ud]/.test(ev.bubble)) { setTimeout(() => { if (ev.bubble.indexOf('l') === -1) { ev.bubble += 'l'; } if (ev.bubble.indexOf('u') > -1) { let parent = traverseNodes(target, '', true)?.[0].target; if (parent) DC.trigger(parent, ev); } if (ev.bubble.indexOf('d') > -1) { let targets = new Set(target._mc .children() .map((c) => c?.target)); targets.forEach((target) => { if (target) DC.trigger(target, ev); }); } }, 0); } } class McBase { #target; constructor(target, debug = false) { let that = this, element = that; this.#target = new WeakRef(target || {}); init(element); autoAttachListeners(element); } get target() { return this.#target.deref(); } } class MC extends McBase { _mc = {}; constructor(target) { super(target); } static trigger = trigger; trigger(ev, data = {}, bubble = 'l') { let that = this; if (that.constructor === MC || that.constructor === DC) { let event = ev instanceof McEvent ? new McEvent(ev.type, ev.data, 'l') : new McEvent(ev, data, 'l'), mcEvent = typeof ev !== 'string' ? ev : new McEvent(ev, data, bubble); [...Object.getOwnPropertyNames(that)].forEach((mc) => { if (that[mc]?.trigger && typeof that[mc]?.trigger === 'function') { that[mc]?.trigger(event); } }); if (/[ud]/.test(mcEvent.bubble)) { bubbleEvent(that.target, mcEvent); } return; } if (that instanceof MC || that instanceof DC) { const that = this, mcEvent = typeof ev !== 'string' ? ev : new McEvent(ev, data, bubble), handlerName = 'on' + mcEvent.type[0].toUpperCase() + mcEvent.type.slice(1); if (mcEvent.bubble.indexOf('l') > -1) { if (!mcEvent.immediateStopped && typeof that[handlerName] === 'function') { that[handlerName](mcEvent); if (that.hasOwnProperty(handlerName)) { delete that[handlerName]; } } } if (mcEvent.stopped || mcEvent.bubble === 'l') { return that; } if (/[ud]/.test(mcEvent.bubble)) { bubbleEvent(that.target, mcEvent); } } } } class DC extends McBase { _mc = {}; constructor(target, debug = false) { super(target); } static add = add; static remove = remove; static trigger = trigger; trigger(ev, data = {}, bubble) { MC.prototype.trigger.call(this, ev, data, bubble); } parent(search = '') { let target = this.target; if (!isDOM(target)) return []; return traverseNodes(target.parentNode, search, true); } ancestors(search = '') { let target = this?.target; if (!isDOM(target)) return []; return traverseNodes(this.target.parentNode, search, false); } children(search = '') { let target = this?.target; if (!isDOM(target)) return []; const id = Math.random().toString().replace('.', ''); const selector = '[_mc]:not([temp_id="' + id + '"] [_mc] [_mc])'; target.setAttribute('temp_id', id); const myDomElements = target.querySelectorAll(selector); const mcValues = getMicroComponents([...myDomElements], search); target.removeAttribute('temp_id'); return mcValues; } descendants(search = '') { let target = this?.target; if (!isDOM(target)) return []; const myDomElements = target.querySelectorAll('*[_mc]'); return getMicroComponents([...myDomElements], search); } } const CEs = new Map(); const MCs = new Map(); function makeLoadScript(element) { const fileName = './' + element.split('-').join('/') + '.js'; const se = document.createElement('script'); se.setAttribute('src', fileName); se.setAttribute('blocking', 'render'); se.async = true; se.setAttribute('type', 'module'); se.setAttribute('name', element); se.setAttribute('loading', ''); addListener(se, 'load', (ev) => { se.removeAttribute('loading'); se.setAttribute('loaded', ''); }); addListener(se, 'error', (ev) => { console.error('could not load Custom Element code from', fileName); }); return se; } function onLoadCallback(script) { setTimeout(function () { const importFileName = script.getAttribute('src'); const elementName = script.getAttribute('name'); const se = document.createElement('script'); se.async = true; se.setAttribute('type', 'module'); const code = ` import _ from "${importFileName}"; import { DC } from "./microcomponents.js"; const elements = Array.from(document.querySelectorAll('[_mc*="${elementName}"]')).filter( element => element instanceof HTMLElement ); elements.forEach((element) => { // add class to MCs repository autoload.state.MCs.set( '${elementName}', _ ); // add instance to HTMLElement DC.add(element, _.name, new _(element)); // remove instance name from elements "_mc" attribute element.setAttribute('_mc', element.getAttribute('_mc').replace('${elementName}', '').replace( /\w\w/g, ' ').trim()); }); setTimeout( () => { document.body.querySelector('script[name="${elementName}]"')?.remove(); },0); `; se.innerHTML = code; document.body.append(se); }, 0); } function loadUndefinedMicroComponents() { [...document.querySelectorAll(':not(:defined)')] .filter((element) => !!element && !customElements.get(element.tagName.toLowerCase()) && !CEs.has(element.tagName.toLowerCase())) .forEach((el) => { const elementName = el.tagName.toLowerCase(), fileName = './' + elementName.split('-').join('/') + '.js'; const se = makeLoadScript(elementName); addListener(se, 'load', () => { CEs.set(elementName, customElements.get(el.tagName.toLowerCase())); }); CEs.set(elementName, 'loading'); document.head.append(se); }); [...document.querySelectorAll('[_mc]:not([_mc=""]')] .filter((element) => !!element) .forEach((el) => { const elements = el.getAttribute('_mc')?.split(' ').filter(m => !!m) || []; const elementsToBeLoaded = elements.filter(m => !MCs.has(m)); elementsToBeLoaded?.forEach(elementName => { const se = makeLoadScript(elementName); addListener(se, 'load', () => onLoadCallback(document.head.querySelector(`script[name="${elementName}"]`))); MCs.set(elementName, 'loading'); document.head.append(se); }); const elementsAlreadyLoaded = elements.filter(m => MCs.has(m) && !MCs.get(m).length); elementsAlreadyLoaded?.forEach(elementName => { setTimeout(() => { DC.add(el, elementName, new (autoload.state.MCs.get(elementName))(el)); const newAttribute = el.getAttribute('_mc') .replace('${elementName}', '') .replace(/\w\w/g, ' ') .trim() || '_mc'; el.setAttribute('_mc'); }, 5); }); }); } let autoloadEnabled = false; let observer = null; function autoload(enable = true) { if (!enable) { observer?.disconnect(); observer = null; return autoloadEnabled; } autoloadEnabled = enable; observer = new MutationObserver(loadUndefinedMicroComponents); setTimeout(() => { observer.observe(document.body, { childList: true, subtree: true }); loadUndefinedMicroComponents(); }, 5); window.autoload = window.autoload || autoload; return autoloadEnabled; } autoload.state = { CEs, MCs }; export { addListener, removeListener, McBase, MC, DC, McEvent, autoload }; //# sourceMappingURL=MC.js.map