lighty
Version:
The tiny engine for your handy microframework
200 lines (146 loc) • 4.78 kB
Flow
/*!
* lighty v0.9.0
* https://github.com/demiazz/lighty
*
* Copyright 2016-present Alexey Plutalov <demiazz.py@gmail.com>
* Released under the MIT license
*
* @flow
*/
/* ----- Shortcuts ----- */
const doc: Document = window.document;
const body: Element = ((doc.body: any): Element);
/* ----- Matches Helper ----- */
type MatchesFn = (selector: string) => boolean;
type MatchesAPI = { matches: MatchesFn } & { matchesSelector: MatchesFn } & {
msMatchesSelector: MatchesFn
} & { mozMatchesSelector: MatchesFn } & {
webkitMatchesSelector: MatchesFn
} & { oMatchesSelector: MatchesFn };
function getMatchesFn(): MatchesFn {
const e = ((doc.createElement("div"): any): MatchesAPI);
return (
e.matches ||
e.matchesSelector ||
e.msMatchesSelector ||
e.mozMatchesSelector ||
e.webkitMatchesSelector ||
e.oMatchesSelector
);
}
const matchesFn: MatchesFn = getMatchesFn();
function matches(element: Element, selector: string): boolean {
return matchesFn.call(element, selector);
}
/* ----- Walk Helper ----- */
type Trees = Element | NodeList<*> | Array<*> | string;
type WalkFn = (element: Element) => mixed;
function filterElements(elements: Array<*>): Array<Element> {
return elements.filter(e => e instanceof Element);
}
function toArray(nodeList: NodeList<*>): Array<*> {
return [].slice.call(nodeList);
}
function walk(trees: Trees, selector: string, callback: WalkFn): void {
let roots;
if (trees instanceof Element) {
roots = [trees];
} else if (trees instanceof NodeList) {
roots = filterElements(toArray(trees));
} else if (Array.isArray(trees)) {
roots = filterElements(trees);
} else if (typeof trees === "string") {
roots = toArray(doc.querySelectorAll(trees));
} else {
throw new TypeError("Unsupported type of tree root");
}
roots.forEach(root => {
if (matches(root, selector)) {
callback(root);
}
toArray(root.querySelectorAll(selector)).forEach(callback);
});
}
/* ----- Engine ID Helper ----- */
const getEngineId: () => string = (() => {
let engineId: number = -1;
return (): string => {
engineId += 1;
return `__lighty__${engineId}`;
};
})();
/* ----- Binding Helpers ----- */
type Bindable = { [key: string]: Array<number> };
function hasBinding(element: Bindable, key: string, id: number): boolean {
const bindings: ?Array<number> = element[key];
return bindings ? bindings.indexOf(id) !== -1 : false;
}
function bind(element: Bindable, key: string, id: number): void {
const bindings: ?Array<number> = element[key];
element[key] = bindings ? bindings.concat(id) : [id];
}
/* ----- DOM Ready Helper ----- */
function onDOMContentLoaded(action: () => void) {
if (doc.readyState === "loading") {
doc.addEventListener("DOMContentLoaded", action);
} else {
// See https://connect.microsoft.com/IE/feedback/details/792880/document-readystat
setTimeout(action, 1);
}
}
/* ----- Engine Factory ----- */
type BuilderFn = (element: Element, ...args: Array<mixed>) => mixed;
type OnStartFn = () => mixed;
type Component = [number, string, Array<mixed>];
interface Engine {
component(selector: string, ...args: Array<mixed>): void,
vitalize(trees?: Trees): void
}
function createEngine(builder: BuilderFn, onStart?: OnStartFn): Engine {
const engineId: string = getEngineId();
const components: Array<Component> = [];
let isRunning: boolean = false;
/* ----- Components Instatiation ----- */
function build(trees: Trees, component: Component): void {
const id: number = component[0];
const selector: string = component[1];
const args: Array<mixed> = component[2];
walk(trees, selector, element => {
if (hasBinding(element, engineId, id)) {
return;
}
(builder: any).apply(null, [element].concat(args));
bind(element, engineId, id);
});
}
function vitalize(trees?: Trees): void {
if (!isRunning) {
throw Error("Document is not ready yet.");
}
const roots = trees || body;
components.forEach(component => build(roots, component));
}
/* ----- Components Registration ----- */
function register(selector: string, ...args: Array<mixed>): void {
const id: number = components.length;
const component: Component = [id, selector, args];
components.push(component);
if (isRunning) {
build(body, component);
}
}
/* ----- Initialization ----- */
function start(): void {
isRunning = true;
vitalize();
if (onStart) {
onStart();
}
}
if (!(builder instanceof Function)) {
throw new TypeError("Builder must be a function");
}
onDOMContentLoaded(start);
return { component: register, vitalize };
}
export default createEngine;