sinuous
Version:
🧬 Small, fast, reactive render engine
262 lines (236 loc) • 7.1 kB
JavaScript
import * as observable from 'sinuous/observable';
import { o as o$1 } from 'sinuous/observable';
import htm from 'sinuous/htm';
/*
* @param {object} api
* @param {Function} [api.subscribe] - Function that listens to state changes.
* @param {Function} [api.cleanup] - Add the given function to the cleanup stack.
*/
const api = {};
const EMPTY_ARR = [];
const GROUPING = '__g';
/**
* Clear all nodes in the parent.
* @param {Node} parent
* @param {*} current
* @param {Node} marker - This is the ending marker node.
* @param {Node} startNode - This is the start node.
*/
function clearAll(parent, current, marker, startNode) {
if (marker) {
// `current` can't be `0`, it's coerced to a string in insert.
if (current) {
if (!startNode) {
startNode = marker.previousSibling || parent.lastChild;
// Support fragments
const key = startNode[GROUPING];
if (key) {
startNode = startNode.previousSibling;
while (startNode && startNode[GROUPING] !== key) {
startNode = startNode.previousSibling;
}
}
}
let tmp;
while (startNode && startNode !== marker) {
tmp = startNode.nextSibling;
parent.removeChild(startNode);
startNode[GROUPING] = 0;
startNode = tmp;
}
}
} else {
parent.textContent = '';
}
}
let groupCounter = 0;
function insert(parent, value, marker, current) {
// This is needed if the parent is a DocumentFragment initially.
parent = (marker && marker.parentNode) || parent;
const t = typeof value;
if (value === current);
else if ((!value && value !== 0) || value === true) {
clearAll(parent, current, marker);
current = null;
} else if (
(!current || typeof current === 'string') &&
(t === 'string' || (t === 'number' && (value += '')))
) {
// Block optimized for string insertion.
if (current == null || !parent.firstChild) {
if (marker) {
parent.insertBefore(document.createTextNode(value), marker);
} else {
parent.textContent = value;
}
} else {
if (marker) {
(marker.previousSibling || parent.lastChild).data = value;
} else {
parent.firstChild.data = value;
}
}
current = value;
} else if (t === 'function') {
api.subscribe(function() {
current = insert(parent, value(), marker, current);
});
} else {
// Block for nodes, fragments, Arrays, non-stringables and node -> stringable.
clearAll(parent, current, marker);
if (!(value instanceof Node)) {
// Passing an empty array creates a DocumentFragment.
value = api.h(EMPTY_ARR, value);
}
if (value.nodeType === 11 && value.firstChild !== value.lastChild) {
value.firstChild[GROUPING] = value.lastChild[GROUPING] = ++groupCounter;
}
// If marker is `null`, value will be added to the end of the list.
// IE9 requires an explicit `null` as second argument.
parent.insertBefore(value, marker || null);
current = value;
}
return current;
}
/* Adapted from Hyper DOM Expressions - The MIT License - Ryan Carniato */
/**
* Create a sinuous `h` tag aka hyperscript.
* @param {object} options
* @param {boolean} isSvg
* @return {Function} `h` tag.
*/
function context(options, isSvg) {
for (let i in options) api[i] = options[i];
function h() {
const args = EMPTY_ARR.slice.call(arguments);
let el;
function item(arg) {
const type = typeof arg;
if (arg == null);
else if (type === 'string') {
if (el) {
el.appendChild(document.createTextNode(arg));
} else {
if (isSvg) {
el = document.createElementNS('http://www.w3.org/2000/svg', arg);
} else {
el = document.createElement(arg);
}
}
} else if (Array.isArray(arg)) {
// Support Fragments
if (!el) el = document.createDocumentFragment();
arg.forEach(item);
} else if (arg instanceof Node) {
if (el) {
el.appendChild(arg);
} else {
// Support updates
el = arg;
}
} else if (type === 'object') {
for (let name in arg) {
// Create scope for every entry.
property(name, arg[name], el, isSvg);
}
} else if (type === 'function') {
if (el) {
const marker = el.appendChild(document.createTextNode(''));
if (arg.$t) {
// Record insert action in template, marker is used as pre-fill.
arg.$t(1, insert, el, '');
} else {
insert(el, arg, marker);
}
} else {
// Support Components
el = arg.apply(null, args.splice(0));
}
} else {
el.appendChild(document.createTextNode('' + arg));
}
}
while (args.length) {
item(args.shift());
}
return el;
}
api.h = h;
return h;
}
function property(name, value, el, isSvg, isCss) {
if (name[0] === 'o' && name[1] === 'n' && !value.$o) {
// Functions added as event handlers are not executed
// on render unless they have an observable indicator.
handleEvent(el, name, value);
} else if (typeof value === 'function') {
if (value.$t) {
// Record property action in template.
value.$t(2, property, el, name);
} else {
api.subscribe(() => {
property(name, value(), el, isSvg, isCss);
});
}
} else if (isCss) {
el.style.setProperty(name, value);
} else if (
isSvg ||
name.slice(0, 5) === 'data-' ||
name.slice(0, 5) === 'aria-'
) {
el.setAttribute(name, value);
} else if (name === 'style') {
if (typeof value === 'string') {
el.style.cssText = value;
} else {
for (name in value) {
property(name, value[name], el, isSvg, true);
}
}
} else if (name === 'attrs') {
for (name in value) {
property(name, value[name], el, true);
}
} else {
if (name === 'class') name += 'Name';
el[name] = value;
}
}
function handleEvent(el, name, value) {
name = name.slice(2);
const removeListener = api.cleanup(() =>
el.removeEventListener(name, eventProxy)
);
if (value) {
el.addEventListener(name, eventProxy);
} else {
removeListener();
}
(el._listeners || (el._listeners = {}))[name] = value;
}
/**
* Proxy an event to hooked event handlers.
* @param {Event} e - The event object from the browser.
* @return {Function}
*/
function eventProxy(e) {
// eslint-disable-next-line
return this._listeners[e.type](e);
}
/*
* Sinuous by Wesley Luyten (@luwes).
* Really ties all the packages together.
*/
const h = context(observable);
const hs = context(observable, true);
const o = o$1;
// `export const html = htm.bind(h)` is not tree-shakeable!
function html() {
return htm.apply(h, arguments);
}
// `export const svg = htm.bind(hs)` is not tree-shakeable!
function svg() {
return htm.apply(hs, arguments);
}
export { api, context, h, hs, html, o, o as observable, svg };