/** * renderer.js — DOM rendering and patching for el-ui. * * v0.1 strategy: full string re-render with event rebinding. * The architecture is designed for future targeted DOM patching: * `patch()` will accept the activated node set and only update * DOM subtrees whose data-el-tag corresponds to an activated node. * * Event binding uses data-el-* attributes set by the compiler, * which avoids innerHTML XSS risks for handler references while * keeping the runtime small (no virtual DOM, no event delegation table). */ export class Renderer { /** * @param {HTMLElement} root the mount point * @param {import('./index.js').Component} component component instance */ constructor(root, component) { this.root = root; this.component = component; this._currentHtml = ''; this._boundHandlers = new WeakMap(); } /** * Initial mount — render and bind events. */ mount() { this._currentHtml = this.component.render(); this.root.innerHTML = this._currentHtml; this._bindEvents(); if (typeof this.component.onMount === 'function') { this.component.onMount(); } } /** * Patch the DOM after a state change. * Called by the component when spreading activation completes. * * @param {Set} [_activatedNodes] future: targeted patching by node set */ patch(_activatedNodes) { const newHtml = this.component.render(); if (newHtml !== this._currentHtml) { // Preserve scroll position and focused element identity const focused = document.activeElement; const focusedId = focused && focused !== document.body ? focused.id : null; this.root.innerHTML = newHtml; this._bindEvents(); this._currentHtml = newHtml; // Restore focus if element still exists if (focusedId) { const el = this.root.querySelector(`#${CSS.escape(focusedId)}`); if (el) el.focus(); } } } /** * Bind all data-el-* event handlers declared by the compiler. * * The compiler emits attributes like: * data-el-click="__self.handleClick()" * data-el-input="(e) => __self.setState('value', e.target.value)" * * We eval these in the context of the component instance. * (For production use, a full CSP-compatible binding would generate * handler functions at compile time.) * * @private */ _bindEvents() { const comp = this.component; const eventNames = ['click', 'input', 'change', 'mouseenter', 'mouseleave', 'keydown', 'keyup', 'keypress', 'focus', 'blur', 'submit', 'mousedown', 'mouseup', 'dblclick', 'contextmenu']; for (const eventName of eventNames) { const attr = `data-el-${eventName}`; this.root.querySelectorAll(`[${attr}]`).forEach(el => { const handlerExpr = el.getAttribute(attr); if (!handlerExpr) return; // Build a handler function in the component's scope const handler = this._compileHandler(handlerExpr, comp); if (handler) { // Remove previous binding if any const prev = this._boundHandlers.get(el); if (prev) el.removeEventListener(eventName, prev[eventName]); el.addEventListener(eventName, handler); if (!this._boundHandlers.has(el)) this._boundHandlers.set(el, {}); this._boundHandlers.get(el)[eventName] = handler; } }); } } /** * Compile a handler expression string into a callable function. * The expression is evaluated with `__self` bound to the component. * * @private * @param {string} expr * @param {object} comp * @returns {function|null} */ _compileHandler(expr, comp) { try { // Decode HTML entities (attrs may have " etc.) const decoded = expr .replace(/"/g, '"') .replace(/'/g, "'") .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>'); // If it looks like an arrow function or function reference, wrap it const isArrow = decoded.includes('=>'); const isCall = decoded.includes('('); if (isArrow) { // Arrow function: `(e) => __self.setState(...)` or `() => ...` // eslint-disable-next-line no-new-func const fn = new Function('__self', `return ${decoded}`)(comp); return typeof fn === 'function' ? fn : null; } else if (isCall) { // Method call: `__self.handleClick()` // eslint-disable-next-line no-new-func return new Function('__self', 'event', `${decoded}`).bind(null, comp); } else { // Bare identifier: method name on the component return comp[decoded] ? comp[decoded].bind(comp) : null; } } catch (e) { console.warn(`el-ui: failed to compile handler: ${expr}`, e); return null; } } }