fluxel
Version:
An ultra-lightweight, high-performance library for efficient DOM building and dynamic web UIs
405 lines (404 loc) • 16.7 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
Object.defineProperty(exports, "default", {
enumerable: true,
get: function() {
return _default;
}
});
const _internalStore = require("./internalStore.js");
const _reactiveDependency = /*#__PURE__*/ _interop_require_default(require("./reactiveDependency.js"));
const _tags__generated = /*#__PURE__*/ _interop_require_default(require("./tags__generated.js"));
function _interop_require_default(obj) {
return obj && obj.__esModule ? obj : {
default: obj
};
}
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.default) {
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.default) {
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.default) {
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.default) ? new _reactiveDependency.default({
get: ()=>attributesClassList.map((cls)=>cls instanceof _reactiveDependency.default ? cls.value : cls),
set: ()=>{
throw new TypeError("Cannot set value of classList directly");
}
}, (targetObj, dep)=>{
attributesClassList.forEach((cls)=>cls instanceof _reactiveDependency.default && 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.default) {
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.default) {
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)=>(0, _internalStore.cleanupTargetListenerRecursive)(element));
});
} else if (reactiveIndex && reactiveIndex.length > 0 && originalChildren) {
for (const index of reactiveIndex){
const originalChild = originalChildren instanceof _reactiveDependency.default ? originalChildren : originalChildren[index];
if (originalChild instanceof _reactiveDependency.default) {
originalChild.addDependency(element, ()=>{
const newChild = normalizeChildren(originalChild.value).children[0];
const oldChild = element.childNodes[index];
if (oldChild === newChild) return;
element.replaceChild(newChild, oldChild);
(0, _internalStore.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.default) {
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__generated.default.forEach((tag)=>{
Fluxel[tag] = (options)=>{
return fluxelInternal(tag, options);
};
});
const _default = Fluxel;