Загрузить файлы в «/»
This commit is contained in:
parent
83b36f56df
commit
6b5296f533
523
dom.js
Normal file
523
dom.js
Normal file
@ -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;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user