phonon
Version:
Phonon is an open source HTML, CSS and JavaScript agnostic framework that allows to create a website or a hybrid Web app.
144 lines (116 loc) • 3.42 kB
text/typescript
import Component from '../component';
import { isElement } from './selector';
import stack from './stack';
/*
* Interface for components that have subscribed
* to DOM changes (nodes added or removed)
*/
export interface IMutatorSubscriber {
componentClass: string;
onAdded?: (element: HTMLElement, create: (component: Component) => void) => void;
/* tslint:disable:max-line-length */
onRemoved?: (element: HTMLElement, remove: (className: string, element: HTMLElement) => void) => void;
}
/*
* List of component subscribers
* A component subscribes to relevant DOM changes (nodes added or removed)
*/
const mutatorSubscribers: IMutatorSubscriber[] = [];
/**
* Observes for node mutations
*/
function subscribe(subscriber: IMutatorSubscriber) {
mutatorSubscribers.push(subscriber);
if (document.body) {
Array.from(document.body.querySelectorAll(`.${subscriber.componentClass}`) || [])
.filter(component => component.getAttribute('data-no-boot') === null)
.forEach((component) => {
dispatchChangeEvent(subscriber, 'onAdded', component as HTMLElement, stack.addComponent);
});
}
}
/**
* Dispatch DOM changes such as nodes added and removed
* @param subscriber
* @param eventName
* @param args
*/
function dispatchChangeEvent(subscriber: IMutatorSubscriber, eventName: string, ...args: any[]) {
const callback = subscriber[eventName];
if (!callback) {
return;
}
callback.apply(callback, args);
}
function nodeFn(element: HTMLElement, added = true) {
// Check no-boot attribute for child nodes
if (element.getAttribute('data-no-boot') !== null) {
return;
}
const elementClasses = element.className.split(' ');
const subscriber = mutatorSubscribers.find(l => elementClasses.indexOf(l.componentClass) > -1);
if (!subscriber) {
return;
}
const eventName = added ? 'onAdded' : 'onRemoved';
const args = added ? [element, stack.addComponent] : [element, stack.removeComponent];
dispatchChangeEvent(subscriber, eventName, ...args);
}
function apply(node, added = true) {
nodeFn(node, added);
let nextNode = node.firstElementChild;
while (nextNode) {
const next = nextNode.nextElementSibling;
if (isElement(nextNode)) {
apply(nextNode, added);
}
nextNode = next;
}
}
function getElements(nodes: NodeList) {
return Array
.from(nodes)
.filter((node: Node) => isElement(node));
}
function observe() {
(new MutationObserver(mutations => mutations.forEach((mutation: MutationRecord) => {
if (mutation.type === 'attributes') {
// stop observing attrs
// @todo
return;
}
const { addedNodes, removedNodes } = mutation;
// added nodes
getElements(addedNodes).forEach(node => apply(node, true));
// removed nodes
getElements(removedNodes).forEach(node => apply(node, false));
}))).observe(document, {
childList: true,
subtree: true,
characterData: true,
attributes: true,
});
}
function boot() {
if (!('MutationObserver' in window)) {
return;
}
// DOM
if (document.body) {
observe();
} else {
const obs = new MutationObserver(() => {
if (document.body) {
obs.disconnect();
observe();
}
});
// virtual DOM
obs.observe(document, { childList: true, subtree: true });
}
}
boot();
export default {
subscribe,
getComponent: stack.getComponent,
};