UNPKG

sinuous

Version:

🧬 Small, fast, reactive render engine

239 lines (218 loc) • 6.53 kB
/* * @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); } export { api, context };