Files
SGF/scripts/purchasing-workspace-dom.mjs
2026-11-04 09:37:20 +02:00

36 lines
4.9 KiB
JavaScript

// Small DOM for exercising the actual local workspace modules against real HTTP.
// It is not a browser renderer or a visual/accessibility acceptance test.
export const walk=e=>[e,...e.children.flatMap(walk)];
const decode=s=>s.replace(/&(?:amp|quot|lt|gt|apos|#039);/g,v=>({'&amp;':'&','&quot;':'"','&lt;':'<','&gt;':'>','&apos;':"'",'&#039;':"'"})[v]);
export class Element {
constructor(tag,text=''){this.tag=tag.toLowerCase();this._text=text;this.children=[];this.dataset={};this.style={};this.attributes={};this.checked=false;this.disabled=false;this.hidden=false;this.isConnected=true;this.parent=null;}
get textContent(){return this._text+this.children.map(e=>e.textContent).join('');}set textContent(v){this._text=String(v);this.replaceChildren();}
get value(){if(this._value!==undefined)return this._value;if(this.tag==='select')return this.children.find(e=>e.selected)?.value??this.children[0]?.value??'';return this.tag==='textarea'?this._text:'';}set value(v){this._value=String(v??'');}
append(...items){for(let item of items){if(typeof item==='string')item=new Element('span',item);item.parent=this;item.isConnected=this.isConnected;this.children.push(item);}}
prepend(...items){for(const item of items){item.parent=this;item.isConnected=this.isConnected;}this.children.unshift(...items);}
replaceChildren(...items){for(const item of this.children)for(const e of walk(item))e.isConnected=false;this.children=[];this.append(...items);}
setAttribute(k,v){this.attributes[k]=v;if(k.startsWith('data-'))this.dataset[k.slice(5).replace(/-([a-z])/g,(_,c)=>c.toUpperCase())]=v;else this[k]=v;}
getAttribute(k){return this.attributes[k]??this[k]??null;}
remove(){if(this.parent)this.parent.children=this.parent.children.filter(e=>e!==this);for(const e of walk(this))e.isConnected=false;}
matches(selector){if(selector.startsWith('#'))return this.id===selector.slice(1);if(selector.startsWith('.'))return String(this.className??this.class??'').split(' ').includes(selector.slice(1));const attr=/^(\w+)?\[([^=\]]+)(?:=["']?([^"'\]]+)["']?)?\]$/.exec(selector);if(attr)return (!attr[1]||this.tag===attr[1])&&(attr[3]===undefined?this.getAttribute(attr[2])!==null:String(this.getAttribute(attr[2]))===attr[3]);return this.tag===selector;}
querySelectorAll(selector){if(selector.includes(','))return selector.split(',').flatMap(s=>this.querySelectorAll(s.trim()));return walk(this).slice(1).filter(e=>e.matches(selector));}
querySelector(s){return this.querySelectorAll(s)[0]??null;}
closest(s){for(let e=this;e;e=e.parent)if(e.matches(s))return e;return null;}
dispatchEvent(event){for(let e=this;e;e=event.bubbles?e.parent:null)e['on'+event.type]?.(event);return true;}
addEventListener(name,callback){this['on'+name]=callback;}
reportValidity(){return true;}focus(){}scrollIntoView(){}
}
export function parsePage(html){
const root=new Element('document'),stack=[root],voids=new Set(['input','meta','link','br','hr','img','source']);
for(const token of html.match(/<[^>]+>|[^<]+/g)??[]){if(token.startsWith('<!'))continue;if(token.startsWith('</')){const tag=token.slice(2,-1).trim().toLowerCase();while(stack.length>1){if(stack.pop().tag===tag)break;}continue;}
if(token.startsWith('<')){const tag=/^<([\w-]+)/.exec(token)?.[1];if(!tag)continue;const element=new Element(tag);for(const m of token.matchAll(/\s([\w:-]+)(?:="([^"]*)"|='([^']*)'|=([^\s>]+))?/g)){const v=decode(m[2]??m[3]??m[4]??'');if(['required','hidden','disabled','checked','selected'].includes(m[1]))element[m[1]]=true;else element.setAttribute(m[1],v);}
stack.at(-1).append(element);if(!voids.has(tag)&&!token.endsWith('/>'))stack.push(element);
}else stack.at(-1)._text+=decode(token);
}
const querySelector=selector=>{const nav=/^nav\[aria-label="Etapas de Compras"\] a\[href="([^"]+)"\]$/.exec(selector);if(nav)return walk(root).find(e=>e.tag==='nav'&&e.getAttribute('aria-label')==='Etapas de Compras')?.querySelector('a[href="'+nav[1]+'"]')??null;return root.querySelector(selector);};
return {root,createElement:tag=>new Element(tag),querySelector,querySelectorAll:s=>root.querySelectorAll(s)};
}
export function installDom(document){globalThis.document=document;globalThis.Option=class extends Element{constructor(text,value){super('option',text);this.value=value;}};globalThis.FormData=class{constructor(form){this.values=walk(form).filter(e=>['input','textarea','select'].includes(e.tag)&&e.name&&!e.disabled&&(!['checkbox','radio'].includes(e.type)||e.checked)).map(e=>[e.name,e.value]);}*[Symbol.iterator](){yield* this.values;}};}
export function byName(root,name){return walk(root).find(e=>e.name===name);}
export function setLabel(form,label,value){const e=walk(form).find(e=>e.tag==='label'&&e._text.includes(label));if(!e)throw Error('Missing field label: '+label);const input=walk(e).find(v=>['input','select','textarea'].includes(v.tag));if(!input)throw Error('Missing input: '+label);if(input.type==='checkbox')input.checked=Boolean(value);else input.value=value;input.onchange?.();return input;}