@twobirds/microcomponents
Version:
Micro Components Organization Class
379 lines (374 loc) • 13 kB
JavaScript
;
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