// Signal implementation function signal(value) { const subscribers = new Set(); return { get value() { return value; }, set value(newValue) { value = newValue; subscribers.forEach(fn => fn()); }, subscribe(fn) { subscribers.add(fn); return () => subscribers.delete(fn); } }; } export class Mite { constructor(selector, options) { this.root = document.querySelector(selector); if (!this.root) throw new Error(`Element ${selector} not found`); this.state = {}; for (const key in options.state) { this.state[key] = signal(options.state[key]); } this.methods = options || {}; this.lifecycle = { mounted: options.mounted || (() => {}), unmounted: options.unmounted || (() => {}) }; for (const key in this.methods) { if (typeof this.methods[key] === "function") { this.methods[key] = this.methods[key].bind(this); } } this.template = this.root.cloneNode(true); this.bindings = new Map(); this.render(); this.lifecycle.mounted.call(this); } render() { this.bindings.forEach(binding => binding.forEach(b => b.unsubscribe?.())); this.bindings.clear(); const fragment = this.template.cloneNode(true); this.root.innerHTML = ""; this.root.appendChild(fragment); this.scanAndBind(this.root); for (const key in this.state) { this.update(key); } } scanAndBind(root) { const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT); let node; while ((node = walker.nextNode())) { if (node.nodeType === Node.TEXT_NODE && node.textContent.includes("{")) { const matches = node.textContent.match(/{(\w+)}/g); if (matches) { matches.forEach(token => { const key = token.slice(1, -1); if (this.state[key]) { if (!this.bindings.has(key)) this.bindings.set(key, []); const updateFn = () => this.update(key); const unsubscribe = this.state[key].subscribe(updateFn); this.bindings.get(key).push({ node, type: "text", original: node.textContent, unsubscribe }); } }); } } else if (node.nodeType === Node.ELEMENT_NODE) { for (const attr of node.attributes) { const matches = attr.value.match(/{(\w+)}/g); if (matches) { if (attr.name.startsWith("@")) { const eventName = attr.name.slice(1); const methodName = attr.value.slice(1, -1); if (this.methods[methodName]) { node.addEventListener(eventName, this.methods[methodName]); } } else { matches.forEach(token => { const key = token.slice(1, -1); if (this.state[key]) { if (!this.bindings.has(key)) this.bindings.set(key, []); const updateFn = () => this.update(key); const unsubscribe = this.state[key].subscribe(updateFn); this.bindings.get(key).push({ node, type: "attr", attrName: attr.name, original: attr.value, unsubscribe }); } }); } } } const rif = node.getAttribute("r-if"); if (rif && rif.match(/{(\w+)}$/)) { const key = rif.slice(1, -1); if (this.state[key]) { if (!this.bindings.has(key)) this.bindings.set(key, []); const updateFn = () => this.update(key); const unsubscribe = this.state[key].subscribe(updateFn); this.bindings.get(key).push({ node, type: "if", unsubscribe }); } } const reach = node.getAttribute("r-each"); if (reach) { const match = reach.match(/{(.+?)}/); if (!match) { console.error(`Invalid r-each syntax: ${reach}`); continue; } const content = match[1]; const parts = content.split(" in "); if (parts.length !== 2) { console.error(`Invalid r-each split on " in ": ${content}, parts: ${parts}`); continue; } let [names, listName] = parts; names = names.trim(); listName = listName.trim(); const nameParts = names.split(",").map(part => part.trim()); const itemName = nameParts[0]; const indexName = nameParts.length > 1 ? nameParts[1] : undefined; if (this.state[listName]) { if (!this.bindings.has(listName)) this.bindings.set(listName, []); const updateFn = () => this.update(listName); const unsubscribe = this.state[listName].subscribe(updateFn); this.bindings.get(listName).push({ node, type: "each", itemName, indexName, unsubscribe, originalReach: reach, parentSelector: node.parentNode.tagName.toLowerCase() }); } else { console.error(`State key not found: ${listName}`); } } } } } update(key) { const bindings = this.bindings.get(key) || []; bindings.forEach(binding => { // Remove the parentNode check for "each" bindings since we use parentSelector if (!binding.node) return; switch (binding.type) { case "text": if (!binding.node.parentNode) return; binding.node.textContent = binding.original.replace(/{(\w+)}/g, (_, k) => { return this.state[k] ? this.state[k].value : `{${k}}`; }); break; case "attr": if (!binding.node.parentNode) return; binding.node.setAttribute(binding.attrName, binding.original.replace(/{(\w+)}/g, (_, k) => { return this.state[k] ? this.state[k].value : `{${k}}`; })); break; case "if": if (!binding.node.parentNode) return; binding.node.style.display = this.state[key].value ? "" : "none"; break; case "each": { const parent = this.root.querySelector(binding.parentSelector); if (!parent) { console.error(`Parent node (${binding.parentSelector}) not found for r-each="${binding.originalReach}"`); return; } const template = this.template.querySelector(`[r-each="${binding.originalReach}"]`); if (!template) { console.error(`Template not found for r-each="${binding.originalReach}"`); return; } const items = this.state[key].value || []; parent.innerHTML = ""; items.forEach((item, index) => { const clone = template.cloneNode(true); clone.removeAttribute("r-each"); const walker = document.createTreeWalker(clone, NodeFilter.SHOW_TEXT); let textNode; while ((textNode = walker.nextNode())) { if (textNode.textContent.includes("{")) { textNode.textContent = textNode.textContent.replace(/{(\w+)}/g, (_, k) => { if (k === binding.itemName) return item; if (k === binding.indexName) return index; return this.state[k] ? this.state[k].value : `{${k}}`; }); } } const eventElements = clone.querySelectorAll("[\\@click]"); eventElements.forEach(el => { const clickAttr = el.getAttribute("@click"); if (clickAttr && clickAttr.match(/{(\w+),\s*(\w+)}/)) { const [, methodName, param] = clickAttr.match(/{(\w+),\s*(\w+)}/); if (this.methods[methodName] && param === binding.indexName) { el.addEventListener("click", (e) => this.methods[methodName](e, index)); } } }); parent.appendChild(clone); }); break; } } }); } destroy() { this.lifecycle.unmounted.call(this); this.bindings.forEach(binding => binding.forEach(b => b.unsubscribe?.())); this.root.innerHTML = this.template.innerHTML; } } export default Mite;