From 6b5296f533165536844d0fc2b00d54e6b54b28e4 Mon Sep 17 00:00:00 2001 From: svsptech Date: Mon, 18 May 2026 16:07:00 +0500 Subject: [PATCH] =?UTF-8?q?=D0=97=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D1=84=D0=B0=D0=B9=D0=BB=D1=8B=20=D0=B2=20=C2=AB?= =?UTF-8?q?/=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dom.js | 523 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 523 insertions(+) create mode 100644 dom.js diff --git a/dom.js b/dom.js new file mode 100644 index 0000000..a41bd47 --- /dev/null +++ b/dom.js @@ -0,0 +1,523 @@ +const EVENT_ALIASES = { + // Keyboard + keyDown: 'keydown', + keyUp: 'keyup', + keyPress: 'keypress', + + // Mouse + mouseEnter: 'mouseenter', + mouseLeave: 'mouseleave', + mouseDown: 'mousedown', + mouseUp: 'mouseup', + mouseMove: 'mousemove', + mouseOver: 'mouseover', + mouseOut: 'mouseout', + + // Touch + touchStart: 'touchstart', + touchEnd: 'touchend', + touchMove: 'touchmove', + touchCancel: 'touchcancel', + + // Focus + focusIn: 'focusin', + focusOut: 'focusout', + + // Animation / Transition + animationStart: 'animationstart', + animationEnd: 'animationend', + animationIteration: 'animationiteration', + transitionEnd: 'transitionend', + + // Drag + dragStart: 'dragstart', + dragEnd: 'dragend', + dragEnter: 'dragenter', + dragLeave: 'dragleave', + dragOver: 'dragover', + + // Wheel + wheelStart: 'wheelstart', + wheelEnd: 'wheelend', + + // Composition + compositionStart: 'compositionstart', + compositionEnd: 'compositionend', + compositionUpdate: 'compositionupdate', + + // Pointer + pointerDown: 'pointerdown', + pointerUp: 'pointerup', + pointerMove: 'pointermove', + pointerEnter: 'pointerenter', + pointerLeave: 'pointerleave', + pointerOver: 'pointerover', + pointerOut: 'pointerout', + pointerCancel: 'pointercancel', + + // Context menu + contextMenu: 'contextmenu', + + // Form + formData: 'formdata', + formChange: 'formchange', + formInput: 'forminput', +}; + +function createSignal(initialValue) { + let value = initialValue; + const listeners = new Set(); + + const signalId = `signal_${Math.random().toString(36).slice(2, 7)}`; + let childCounter = 0; + + function signal(newValue) { + if (arguments.length === 0) { + return value; + } + const oldValue = value; + value = newValue; + if (oldValue !== newValue) { + listeners.forEach(fn => fn(value, oldValue)); + } + return value; + } + + signal.id = signalId; + + signal.on = function(fn) { + listeners.add(fn); + //fn(value, value); + return () => listeners.delete(fn); + }; + + signal.map = function(fn, mapId) { + childCounter++; + const derivedId = mapId || `${signalId}_map_${childCounter}`; + const derived = createSignal(fn(value), derivedId); + + const unsubscribe = signal.on(newVal => derived(fn(newVal))); + + derived._parentUnsubscribe = unsubscribe; + derived._parent = signal; + + return derived; + }; + + signal.destroy = function() { + if (signal._parentUnsubscribe) { + signal._parentUnsubscribe(); + } + listeners.clear(); + signal._parent = null; + signal._parentUnsubscribe = null; + }; + + signal.chain = function() { + const chain = [signalId]; + let current = signal; + while (current._parent) { + chain.unshift(current._parent.id); + current = current._parent; + } + return chain.join(' → '); + }; + + signal.listenerCount = function() { + return listeners.size; + }; + + Object.defineProperty(signal, 'value', { + get: () => signal(), + set: (v) => signal(v) + }); + + return signal; +} + + +window.dom = new Proxy({ + signal: createSignal, +}, { + get(target, prop) { + if (prop in target) return target[prop]; + const realTag = prop.replace(/([A-Z])/g, '-$1').toLowerCase(); + return (id) => { + const el = document.createElement(realTag); + if (id !== undefined && typeof id === 'string') { + el.id = id; + } + return createBuilder(el); + }; + } +}); + +function createBuilder(el) { + + const isSignal = (val) => typeof val === 'function' && typeof val.on === 'function'; + + const builder = { + element: el, + _proxy: null, + _dom_instr: [ + 'id', 'cls', 'atr', 'css', 'pcss', 'mcss', + 'text', 'child' //'html' + ], + _condition_instr: [ + 'if', 'elif', 'else', 'endif' + ], + _each_instr: [ + 'ueach', 'oeach' + ], + _adding_signals: [], + _subscribed_signals: new Set(), + _unsubscribe_fns: [], + + stack: [[]], + + get current_stack() { + return this.stack[this.stack.length - 1]; + }, + + push_instruction(instruction, type, ...value){ + this.current_stack.push([instruction, type, ...value]); + return this._proxy; + }, + + processValue(val) { + if (isSignal(val)) { + if(!this._adding_signals.includes(val.id)) { + //this.stack[0].push(['atr', 'value', val.id]); + this._adding_signals.push(val.id); + } + return ['signal', val]; + } + return ['value', val]; + }, + + handle_instruction(prop, ...args){ + if (args.length === 1) { + const [type, value] = this.processValue(args[0]); + return this.push_instruction(prop, type, value); + } else { + const hvalarr = args.flatMap(val => this.processValue(val)); + return this.push_instruction(prop, 'varr', [...hvalarr]); + } + }, + + handle_condition(prop, ...args){ + if(prop === 'if'){ + this.stack.push([]); + return this.handle_instruction(prop, ...args); + } else if(['elif', 'else', 'endif'].includes(prop)){ + const condition_branch = this.current_stack; + const condition_instr = this.current_stack[0]; + this.stack.pop(); + const cproxy = this.handle_instruction(condition_instr[0], condition_instr[2] ?? null, condition_branch.slice(1)); + if(prop === 'endif') return cproxy; + cproxy.stack.push([]); + return cproxy.handle_instruction(prop, ...args); + } + }, + + handle_each(prop, ...args){ + var arr = isSignal(args[0]) ? args[0].value : args[0]; + if(Array.isArray(arr)){ + const map = []; + for(var i = 0; i < arr.length; i++){ + map.push(dom.signal(arr[i])); + } + return this.handle_instruction(prop, args[0], map, null); + } + }, + + update_each(inst){ + const data = inst[2][1].value; + const map = inst[2][3]; + const list = inst[2][5]; + + while (list.children.length > data.length) { + list.removeChild(list.lastChild); + map.pop(); + } + + while (list.children.length < data.length) { + const value = dom.signal("text"); + list.appendChild(dom.li().text(value).done()); + map.push(value); + } + + for(var i = 0; i < data.length; i++){ + if(map[i].value !== data[i]) { + map[i](data[i]); + } + } + + inst[2][3] = map; + }, + + on(event, handler) { + el.addEventListener(event, handler); + return this._proxy; + }, + + atrs(attrs) { + Object.entries(attrs).forEach(([k, v]) => this.handle_instruction('atr', k, v)); + return this._proxy; + }, + + children(arr) { + arr.forEach((el) => this.handle_instruction('child', el)) + return this._proxy; + }, + + unpack(val) { + const processValue = (type, value) => { + return type === 'signal' ? value() : value; + }; + + if (val[1] === 'value') { + return [val[2]]; + } else if (val[1] === 'signal') { + return [processValue('signal', val[2])]; + } else if (val[1] === 'varr') { + let res = []; + for (let i = 0; i < val[2].length; i += 2) { + const type = val[2][i]; + const value = val[2][i + 1]; + res.push(processValue(type, value)); + } + return res; + } + }, + + render(stack, clear = true){ + var isLastConditionTrue = false; + if(clear) el.innerHTML = ''; + stack.forEach((inst) => { + const val = this.unpack(inst); + //console.log(inst[0], val); + switch (inst[0]) { + case 'id': + el.id = val[0]; + break; + case 'text': + el.innerText = val[0]; + break; + /*case 'html': + el.innerHTML = val[0]; + break;*/ + case 'cls': + el.classList.add(...String(val[0]).split(' ').filter(Boolean)); + break; + case 'atr': + el.setAttribute(val[0], val[1]); + break; + case 'css': + el.style.cssText = (el.style.cssText || '') + String(val[0]); + break; + case 'pcss': + case 'mcss': + const type = inst[0] === 'pcss' ? 'pcss' : 'mcss'; + const uniqueCls = `${type}-${Math.random().toString(36).slice(2, 9)}`; + el.classList.add(uniqueCls); + const existingClasses = el.getAttribute(type) || ''; + el.setAttribute(type, existingClasses ? `${existingClasses},${uniqueCls}` : uniqueCls); + const newStyle = document.createElement('style'); + newStyle.textContent = type === 'pcss' + ? `.${uniqueCls}${val[0]} { ${val[1]} }` + : `@media ${val[0]} {.${uniqueCls}{ ${val[1]} }}`; + newStyle.setAttribute(type, uniqueCls); + document.head.appendChild(newStyle); + break; + case 'child': + if(val[0] instanceof HTMLElement) el.appendChild(val[0]); + else el.appendChild(val[0].done()); + break; + case 'if': + if(val[0]){ + this.render(val[1], false); + isLastConditionTrue = true; + } + break; + case 'elif': + if(val[0] && !isLastConditionTrue){ + this.render(val[1], false); + isLastConditionTrue = true; + } + break; + case 'else': + if(!isLastConditionTrue){ + this.render(val[1], false); + } + break; + case 'oeach': + case 'ueach': + const listel = inst[0] === 'oeach' ? + document.createElement('ol') : + document.createElement('ul'); + val[1].forEach((value) => { + const li = dom.li().text(value).done(); + listel.appendChild(li); + }); + + inst[2][5] = listel; + el.appendChild(listel); + break; + default: + console.warn('Unknown instruction', inst) + } + }); + }, + + subscribeToSignals() { + const subscribe = (val) => { + if (val[1] === 'signal') { + const signal = val[2]; + if (!this._subscribed_signals.has(signal)) { + this._subscribed_signals.add(signal); + var unsubscribe = null; + if(['if', 'elif', 'else'].includes(val[0])){ + unsubscribe = signal.on(() => this.render(this.stack[0])); + } else if(this._each_instr.includes(val[0])){ + unsubscribe = signal.on(() => this.update_each(val[3])); + } else { + unsubscribe = signal.on(() => this.findLastAndCallInstruction(this.stack[0], val[0])); + } + this._unsubscribe_fns.push(unsubscribe); + } + } else if (val[1] === 'varr') { + for (let i = 0; i < val[2].length; i += 2) { + const type = val[2][i]; + const value = val[2][i + 1]; + if (type === 'signal') { + subscribe([val[0], 'signal', value, val]); + } + } + } + }; + + this.stack[0].forEach(inst => subscribe(inst)); + }, + + findLastAndCallInstruction(stack, instructionType) { + let foundInstructions = []; + + function search(instructions, depth) { + const foundAtThisLevel = []; + + for (let i = 0; i < instructions.length; i++) { + const inst = instructions[i]; + const type = inst[0]; + + if (type === instructionType) { + foundAtThisLevel.push({ instruction: inst, depth: depth, index: i }); + } + + if (type === 'if' || type === 'elif' || type === 'else') { + const valArr = inst[2]; + let conditionTrue = false; + + if (type === 'else') { + let anyPreviousTrue = false; + for (let j = i - 1; j >= 0; j--) { + const prev = instructions[j]; + if (prev[0] === 'if' || prev[0] === 'elif') { + const prevCondition = evaluateCondition(prev); + if (prevCondition) { + anyPreviousTrue = true; + break; + } + } + if (prev[0] === 'if') break; + } + conditionTrue = !anyPreviousTrue; + } else { + conditionTrue = evaluateCondition(inst); + } + + if (conditionTrue && valArr[3]) { + search(valArr[3], depth + 1); + } + } + } + foundInstructions.push(...foundAtThisLevel); + } + + function evaluateCondition(inst) { + const valArr = inst[2]; + if (valArr[0] === 'signal' && typeof valArr[1] === 'function') { + return valArr[1](); + } + return valArr[1]; + } + + search(stack, 0); + + foundInstructions.sort((a, b) => { + if (a.depth !== b.depth) return b.depth - a.depth; + return b.index - a.index; + }); + + const maxDepth = foundInstructions.length > 0 ? foundInstructions[0].depth : -1; + const result = foundInstructions + .filter(item => item.depth === maxDepth) + .sort((a, b) => a.index - b.index) + .map(item => item.instruction); + + if (result.length > 0) { + result.forEach((inst) => { + const clearType = inst[0] === 'pcss' ? 'pcss' : 'mcss'; + const classesToRemove = (el.getAttribute(clearType) || '').split(',').filter(Boolean); + + classesToRemove.forEach(cls => { + el.classList.remove(cls); + const styleToRemove = document.querySelector(`style[${clearType}="${cls}"]`); + if (styleToRemove) styleToRemove.remove(); + }); + + el.removeAttribute(clearType); + }) + this.render(result, false); + } + }, + + done() { + this.subscribeToSignals(); + this.render(this.stack[0]); + return el; + } + }; + + const proxy = new Proxy(builder, { + get(target, prop) { + if (prop in target) return target[prop]; + if (target._dom_instr.includes(prop)) { + return (...args) => { + return target.handle_instruction(prop, ...args); + }; + } + if (target._condition_instr.includes(prop)) { + return (...args) => { + return target.handle_condition(prop, ...args); + }; + } + if (target._each_instr.includes(prop)) { + return (...args) => { + return target.handle_each(prop, ...args); + }; + } + if (typeof prop === 'symbol') return undefined; + if (EVENT_ALIASES[prop]) { + const eventName = EVENT_ALIASES[prop]; + return (handler) => target.on(eventName, handler); + } + const event = prop.replace(/([A-Z])/g, '-$1').toLowerCase(); + return (handler) => target.on(event, handler); + } + }); + + builder._proxy = proxy; + + return proxy; +} \ No newline at end of file