UNPKG

fluxel

Version:

An ultra-lightweight, high-performance library for efficient DOM building and dynamic web UIs

390 lines (389 loc) 16.2 kB
import { cleanupTargetListenerRecursive } from "./internalStore.js"; import ReactiveDependency from "./reactiveDependency.js"; import tags from "./tags__generated.js"; function getFluxelHydrationMetadata() { if (typeof window !== "undefined" && "fluxelH" in window) { return window.fluxelH; } else { return null; } } function fluxelInternal(tagName, options) { if (typeof tagName !== "string") { throw new TypeError(`Expected string for tagName, got ${typeof tagName}`); } const hydrationMetadata = getFluxelHydrationMetadata(); const element = hydrationMetadata ? hydrationMetadata.getElementByEid(`${hydrationMetadata.count++}`) : document.createElement(tagName); return applyProps(element, options); } function applyProps(element, options) { const isHydrating = !!getFluxelHydrationMetadata(); let attributes = null; let originalChildren = null; let children = null; let variableChildrenLength = false; let reactiveIndex = null; if (options) { if (typeof options === "string" || Array.isArray(options) || options instanceof Node || options instanceof ReactiveDependency) { originalChildren = options; const childrenTransformResult = normalizeChildren(options); children = childrenTransformResult.children; variableChildrenLength = childrenTransformResult.variableChildrenLength; reactiveIndex = childrenTransformResult.reactiveIndex; } else { attributes = Object.assign(Object.create(null), options); if ("textContent" in attributes) { throw new TypeError("textContent is not allowed"); } if ("children" in attributes && attributes.children !== null && attributes.children !== undefined) { originalChildren = attributes.children; const childrenTransformResult = normalizeChildren(attributes.children); children = childrenTransformResult.children; variableChildrenLength = childrenTransformResult.variableChildrenLength; reactiveIndex = childrenTransformResult.reactiveIndex; delete attributes.children; } } } attributes = attributes || {}; children = children || null; const eventHandlers = new Map(); for(const key in attributes){ if (key.startsWith("on") && attributes[key]) { const eventName = key.slice(2).toLowerCase(); const handlers = eventHandlers.get(eventName) || []; eventHandlers.set(eventName, handlers); const fns = attributes[key]; if (Array.isArray(fns)) { fns.forEach((fn)=>{ if (typeof fn === "function") { handlers.push(fn); } else { throw new TypeError(`Expected function for event handler ${key}, got ${typeof fn}`); } }); } else { if (typeof fns === "function") { handlers.push(fns); } else { throw new TypeError(`Expected function for event handler ${key}, got ${typeof fns}`); } } delete attributes[key]; } } const styles = Object.assign(Object.create(null), "style" in attributes ? attributes.style : null); if (styles && typeof styles === "object" && !Array.isArray(styles)) { for(const key in styles){ if (styles[key] instanceof ReactiveDependency) { const dep = styles[key]; dep.addDependency(element, ()=>{ if (key.startsWith("--")) { element.style.setProperty(key, dep.value); } else { element.style[key] = dep.value; } }); styles[key] = dep.value; } } } delete attributes.style; const classList = new Set(); const addClass = (cls)=>{ if (!cls) return; const trimmed = cls.trim(); if (!trimmed) return; classList.add(trimmed); }; element.classList.forEach((cls)=>addClass(cls)); if (attributes.classList) { const addClassList = (cls)=>{ if (typeof cls === "string") { addClass(cls); } else if ("forEach" in cls) { cls.forEach((cls)=>addClass(cls)); } else if (cls instanceof ReactiveDependency) { const dep = cls; dep.addDependency(element, ()=>{ element.className = ""; classList.clear(); addClassList(dep.value); classList.forEach((cls)=>element.classList.add(cls)); }); addClassList(dep.value); } else { throw new TypeError(`Expected string or iterable for 'classList', got ${typeof attributes.classList}`); } }; const attributesClassList = attributes.classList; const transformedClassList = Array.isArray(attributesClassList) && attributesClassList.some((cls)=>cls instanceof ReactiveDependency) ? new ReactiveDependency({ get: ()=>attributesClassList.map((cls)=>cls instanceof ReactiveDependency ? cls.value : cls), set: ()=>{ throw new TypeError("Cannot set value of classList directly"); } }, (targetObj, dep)=>{ attributesClassList.forEach((cls)=>cls instanceof ReactiveDependency && cls.addDependency(targetObj, dep)); }) : attributes.classList; addClassList(transformedClassList); delete attributes.classList; } if ("className" in attributes) { delete attributes.className; } const datasets = attributes.dataset || null; if (datasets && typeof datasets === "object" && !Array.isArray(datasets)) { for(const key in datasets){ if (datasets[key] instanceof ReactiveDependency) { const dep = datasets[key]; dep.addDependency(element, ()=>{ element.dataset[key] = dep.value; }); datasets[key] = dep.value; } } } delete attributes.dataset; for(const key in attributes){ if (attributes[key] instanceof ReactiveDependency) { const dep = attributes[key]; dep.addDependency(element, ()=>{ element[key] = dep.value; }); if (!isHydrating) { element[key] = dep.value; } delete attributes[key]; } } if (!isHydrating) { Object.assign(element, attributes); if (element.style && styles) { Object.assign(element.style, styles); Object.keys(styles).forEach((key)=>{ if (key.startsWith("--")) { element.style.setProperty(key, styles[key]); } }); } if (element.dataset && datasets) { Object.assign(element.dataset, datasets); } if (classList.size > 0) { classList.forEach((cls)=>element.classList.add(cls)); } } if (variableChildrenLength && originalChildren) { originalChildren.addDependency(element, ()=>{ const currentChildren = spreadHTMLCollection(element.childNodes); const newChildren = normalizeChildren(originalChildren).children; const currentChildrenMap = arrayToMap(currentChildren); const newChildrenMap = arrayToMap(newChildren); if (currentChildren.length === newChildren.length && currentChildren.every((child, index)=>child.isSameNode(newChildren[index]))) { return; } const mostEfficientFixedElementIndex = currentChildren.reduce((prev, current, index)=>{ var _newChildrenMap_get; const newChildrenAtIndex = (_newChildrenMap_get = newChildrenMap.get(current)) !== null && _newChildrenMap_get !== void 0 ? _newChildrenMap_get : -1; if (newChildrenAtIndex < 0) return prev; let reusable = new Set([ index ]); let currentPointer = index; for(let i = newChildrenAtIndex + 1; i < newChildren.length; i++){ var _currentChildrenMap_get; const currentChildrenAtIndex = (_currentChildrenMap_get = currentChildrenMap.get(newChildren[i])) !== null && _currentChildrenMap_get !== void 0 ? _currentChildrenMap_get : -1; if (currentChildrenAtIndex < currentPointer) { continue; } reusable.add(currentChildrenAtIndex); currentPointer = currentChildrenAtIndex; } if (reusable.size > prev.size) { return reusable; } return prev; }, new Set()); const deleteElements = new Set(); if (mostEfficientFixedElementIndex.size !== currentChildren.length) { for(let i = currentChildren.length - 1; i >= 0; i--){ if (!mostEfficientFixedElementIndex.has(i)) { const deleteElement = currentChildren[i]; element.removeChild(deleteElement); deleteElements.add(deleteElement); } } } if (element.childNodes.length === newChildren.length) return; newChildren.forEach((newChild, index)=>{ deleteElements.delete(newChild); if (!element.childNodes[index]) { element.appendChild(newChild); } else if (element.childNodes[index] !== newChild) { element.insertBefore(newChild, element.childNodes[index]); } }); deleteElements.forEach((element)=>cleanupTargetListenerRecursive(element)); }); } else if (reactiveIndex && reactiveIndex.length > 0 && originalChildren) { for (const index of reactiveIndex){ const originalChild = originalChildren instanceof ReactiveDependency ? originalChildren : originalChildren[index]; if (originalChild instanceof ReactiveDependency) { originalChild.addDependency(element, ()=>{ const newChild = normalizeChildren(originalChild.value).children[0]; const oldChild = element.childNodes[index]; if (oldChild === newChild) return; element.replaceChild(newChild, oldChild); cleanupTargetListenerRecursive(oldChild); }); children[index] = originalChild.value; } } } if (children) { if (isHydrating) { children.forEach((child, index)=>{ if (child instanceof Text) { element.replaceChild(child, element.childNodes[index]); } }); } else { element.append(...children); } } eventHandlers.forEach((handlers, eventName)=>{ handlers.forEach((handler)=>{ element.addEventListener(eventName, handler); }); }); return element; } function normalizeChildren(children) { let variableChildrenLength = false; const reactiveIndex = []; const normalizeChildrenInternal = (children, arrayDepth = 0, arrayIndex = -1)=>{ if (arrayDepth > 1) { throw new TypeError("Children cannot be nested more than one level deep"); } if (typeof children === "string") { return [ document.createTextNode(children) ]; } else if (Array.isArray(children)) { children = children.flat(Infinity); const childrenLength = children.length; const result = children.flatMap((child, index)=>normalizeChildrenInternal(child, arrayDepth + (childrenLength === 1 ? 0 : 1), index)); return result; } else if (children instanceof Node) { return [ children ]; } else if (children instanceof ReactiveDependency) { const evaluatedChildrenValue = children.value; if (Array.isArray(evaluatedChildrenValue)) { if (arrayDepth === 0) { variableChildrenLength = true; } return evaluatedChildrenValue.flat(Infinity).flatMap((child, index)=>normalizeChildrenInternal(child, arrayDepth + 1, index)); } else if (evaluatedChildrenValue instanceof Node) { if (arrayDepth === 0) { variableChildrenLength = true; } reactiveIndex.push(arrayIndex); return [ evaluatedChildrenValue ]; } else { const textNode = document.createTextNode(evaluatedChildrenValue); children.addDependency(textNode, ()=>{ textNode.textContent = String(children.value); }); return [ textNode ]; } } else { return []; } }; return { children: normalizeChildrenInternal(children), variableChildrenLength, reactiveIndex }; } fluxelInternal.fragment = function(children, options) { const normalizedChildrenResult = normalizeChildren(children); const transformedChildren = normalizedChildrenResult.children; const variableChildrenLength = normalizedChildrenResult.variableChildrenLength; const reactiveIndex = normalizedChildrenResult.reactiveIndex; const result = transformedChildren; result.forEach((child)=>{ if (child instanceof HTMLElement) { applyProps(child, options); } }); if (variableChildrenLength) { return children.derive(()=>transformedChildren); } else if (reactiveIndex.length > 0) { for (const index of reactiveIndex){ result[index] = children[index].derive(()=>transformedChildren[index]); } } return result; }; function getFluxelStyleElement() { const style = document.querySelector("style[data-fluxel]"); if (style) return style; const styleElement = document.createElement("style"); styleElement.dataset.fluxel = "true"; document.head.appendChild(styleElement); return styleElement; } fluxelInternal.forwardStyle = function(style) { if (getFluxelHydrationMetadata()) return; if (!fluxelInternal.forwardStyleCache.includes(style)) { fluxelInternal.forwardStyleCache.push(style); if (typeof window !== "undefined") { getFluxelStyleElement().textContent += style; } } }; fluxelInternal.forwardStyleCache = []; function generateUniqueId() { return `${Date.now().toString(36)}${Math.random().toString(36).substring(2)}`; } function spreadHTMLCollection(collection) { if (typeof Array.from === "function") { return Array.from(collection); } const result = []; for(let i = 0; i < collection.length; i++){ result.push(collection[i]); } return result; } function arrayToMap(array) { const map = new Map(); array.forEach((item, index)=>{ map.set(item, index); }); return map; } fluxelInternal.useUniqueString = function(renderer) { const id = generateUniqueId(); return renderer(id); }; fluxelInternal.createComponent = function(renderer) { return (props = {})=>{ return renderer(props); }; }; fluxelInternal.createElement = fluxelInternal; const Fluxel = fluxelInternal; tags.forEach((tag)=>{ Fluxel[tag] = (options)=>{ return fluxelInternal(tag, options); }; }); export default Fluxel;