@ruledwdl/dom 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -1
- package/dist/wdl-dom.min.js +2 -2
- package/package.json +1 -1
- package/src/index.js +581 -169
package/README.md
CHANGED
|
@@ -136,9 +136,17 @@ hero.registry.addRule({
|
|
|
136
136
|
|
|
137
137
|
### Instance Methods
|
|
138
138
|
- `dom.remount()`: Rebuild the entire DOM from state snapshot.
|
|
139
|
-
- `dom.getLiveMap()`: Returns a snapshot copy of `Map<semanticId, HTMLElement
|
|
139
|
+
- `dom.getLiveMap()`: Returns a snapshot copy of `Map<semanticId, HTMLElement>` (first element per semantic ID).
|
|
140
|
+
- `dom.getLiveNodes(semanticId)`: Returns an array of all live elements matching `semanticId` (e.g. repeated loop rows).
|
|
141
|
+
- `dom.getNode(semanticId, index = 0)`: Returns the live element at the specified index for `semanticId`.
|
|
140
142
|
- `dom.destroy()`: Removes event listeners, empties container, and cleans up style tags.
|
|
141
143
|
|
|
144
|
+
### Repeated Loops & Surgical Data Reconciliation
|
|
145
|
+
`@ruledwdl/dom` natively reconciles repeated WDL loop layers (e.g. `li.feature*features`) without remounting or wiping `innerHTML`:
|
|
146
|
+
- Materializes all items on initial mount with `data-wdl-index="0"`, `data-wdl-index="1"`, etc.
|
|
147
|
+
- Resolves per-item attribute templates (e.g. `${text}`, `${_index}`).
|
|
148
|
+
- On `component.data.set('features', nextItems)`, surgically updates retained rows, appends added rows, and removes deleted rows.
|
|
149
|
+
|
|
142
150
|
---
|
|
143
151
|
|
|
144
152
|
## License
|
package/dist/wdl-dom.min.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var f=new Set(["text","class","html"]);function p(l){if(typeof l!="string"){if(l&&typeof l=="object")return{tag:String(l.tag||"div").toLowerCase(),semanticId:String(l.semanticId||l.id||"").replace(/^\./,"")};throw new Error("[wdl-dom] invalid layer expression")}let e=l.trim(),i=e.match(/^([a-zA-Z][a-zA-Z0-9_-]*)(?:\.([a-zA-Z0-9_-]+))?$/);return i?{tag:i[1].toLowerCase(),semanticId:(i[2]||"").replace(/^\./,"")}:{tag:e.toLowerCase()||"div",semanticId:""}}function g(l){if(Array.isArray(l))return structuredClone(l);if(typeof l!="string"||!l.trim())return[];let e={tag:"__root__",semanticId:"",children:[]},i=[e],s=0,a=()=>i[i.length-1];for(;s<l.length;){let t=l[s];if(/\s/.test(t)){s++;continue}if(t===">"){let r=a().children[a().children.length-1];r&&i.push(r),s++;continue}if(t==="+"){s++;continue}if(t==="<"){s++,i.length>1&&i.pop();continue}let n="";for(;s<l.length&&/[a-zA-Z0-9_-]/.test(l[s]);)n+=l[s++];let o="";if(l[s]===".")for(s++;s<l.length&&/[a-zA-Z0-9_-]/.test(l[s]);)o+=l[s++];a().children.push({tag:n.toLowerCase()||"div",semanticId:o,children:[]})}return e.children}function h(l){return l==null?"":String(l).replace(/^\./,"")}var m=class{constructor(e={}){let{container:i,component:s,onDataBind:a=null,styleTarget:t=typeof document<"u"?document.head:null,debug:n=!1}=e;if(!i)throw new Error("[wdl-dom] container is required");if(!s)throw new Error("[wdl-dom] component (ComponentState) is required");if(this.container=typeof i=="string"?document.querySelector(i):i,!this.container)throw new Error("[wdl-dom] container not found");this.component=s,this.onDataBind=a,this.styleTarget=t,this.debug=n,this.liveMap=new Map,this._unsubs=[],this._styleEl=null,this._mount(),this._bindStateEvents()}remount(){this._mount()}getLiveMap(){return new Map(this.liveMap)}destroy(){this._unsubs.forEach(e=>{try{e()}catch{}}),this._unsubs=[],this.container.replaceChildren(),this.liveMap.clear(),this._styleEl&&this._styleEl.parentNode&&this._styleEl.parentNode.removeChild(this._styleEl),this._styleEl=null}_mount(){this.container.replaceChildren(),this.liveMap.clear();let e;try{e=typeof this.component.layers?.tree=="function"?this.component.layers.tree():g(this.component.layers?.list?.()??this.component.getSnapshot?.()?.layers)}catch(s){this._log("warn","tree() failed, falling back to simple parse",s);let a=this.component.getSnapshot?.()?.layers??"";e=g(a)}Array.isArray(e)||(e=[]);for(let s of e){let a=this._createElementFromNode(s);this.container.appendChild(a)}let i=this.component.attr?.list?.()??this.component.getSnapshot?.()?.attr??{};for(let[s,a]of Object.entries(i))this._applyAttrs(h(s),a);this._applyRootVariant(),this._syncRegistryStyles(),this._log("mount",`mounted ${this.liveMap.size} nodes`)}_createElementFromNode(e){let i=(e.tag||"div").toLowerCase(),s=h(e.semanticId||e.id||""),a=document.createElement(i);if(s&&(a.classList.add(s),a.setAttribute("wdl-comp",s),this.liveMap.set(s,a)),Array.isArray(e.children))for(let t of e.children)a.appendChild(this._createElementFromNode(t));return a}_bindStateEvents(){let e=this.component;if(typeof e.on!="function"){this._log("warn","component has no .on() \u2014 events will not be applied");return}let i=a=>this._onStateEvent(a),s=["layers:change","attr:change","data:change","variant:change","registry:change"];for(let a of s){let t=e.on(a,i);typeof t=="function"?this._unsubs.push(t):this._unsubs.push(()=>e.off?.(a,i))}}_onStateEvent(e){if(!(!e||!e.type))switch(this._log("event",e.type,e.action,e.targetId,e.payload),e.type){case"layers:change":this._handleLayers(e);break;case"attr:change":this._handleAttr(e);break;case"data:change":this._handleData(e);break;case"variant:change":this._handleVariant(e);break;case"registry:change":this._handleRegistry(e);break;default:this._log("warn","unknown event type",e.type)}}_handleLayers(e){let{action:i,targetId:s,payload:a}=e,t=h(s);switch(i){case"set":{this._mount();break}case"append":{let n=this.liveMap.get(t);if(!n){this._log("warn",`append: parent "${t}" not in liveMap \u2014 remounting`),this._mount();return}let o=p(a),r=this._createElementFromNode({tag:o.tag,semanticId:o.semanticId,children:[]});n.appendChild(r),this._log("op",`append <${o.tag}.${o.semanticId}> \u2192 #${t}`);break}case"prepend":{let n=this.liveMap.get(t);if(!n){this._log("warn",`prepend: parent "${t}" not in liveMap \u2014 remounting`),this._mount();return}let o=p(a),r=this._createElementFromNode({tag:o.tag,semanticId:o.semanticId,children:[]});n.insertBefore(r,n.firstChild),this._log("op",`prepend <${o.tag}.${o.semanticId}> \u2192 #${t}`);break}case"before":case"after":{let n=this.liveMap.get(t);if(!n||!n.parentNode){this._log("warn",`${i}: target "${t}" missing \u2014 remounting`),this._mount();return}let o=p(a),r=this._createElementFromNode({tag:o.tag,semanticId:o.semanticId,children:[]});i==="before"?n.parentNode.insertBefore(r,n):n.parentNode.insertBefore(r,n.nextSibling),this._log("op",`${i} <${o.tag}.${o.semanticId}> relative to #${t}`);break}case"wrap":{let n=this.liveMap.get(t);if(!n||!n.parentNode){this._log("warn",`wrap: target "${t}" missing \u2014 remounting`),this._mount();return}let o=p(a),r=this._createElementFromNode({tag:o.tag,semanticId:o.semanticId,children:[]});n.parentNode.insertBefore(r,n),r.appendChild(n),this._log("op",`wrap #${t} with <${o.tag}.${o.semanticId}>`);break}case"unwrap":{let n=this.liveMap.get(t);if(!n||!n.parentNode){this._log("warn",`unwrap: target "${t}" missing \u2014 remounting`),this._mount();return}let o=n.parentNode;for(;n.firstChild;)o.insertBefore(n.firstChild,n);n.remove(),this.liveMap.delete(t),this._log("op",`unwrap #${t}`);break}case"move":{let n=this.liveMap.get(t),{targetSemanticId:o,position:r}=a||{},c=h(o),d=this.liveMap.get(c);if(!n||!d){this._log("warn",`move: source "${t}" or target "${c}" missing \u2014 remounting`),this._mount();return}r==="before"?d.parentNode?.insertBefore(n,d):r==="after"?d.parentNode?.insertBefore(n,d.nextSibling):d.appendChild(n),this._log("op",`move #${t} -> ${r} #${c}`);break}case"remove":{let n=this.liveMap.get(t);if(!n){this._log("warn",`remove: #${t} not in liveMap`);return}for(let[o,r]of[...this.liveMap])o!==t&&n.contains(r)&&this.liveMap.delete(o);n.remove(),this.liveMap.delete(t),this._log("op",`remove #${t}`);break}case"update":{let n=this.liveMap.get(t);if(!n){this._log("warn",`update: #${t} missing \u2014 remounting`),this._mount();return}let o=a||{};if(o.tag&&o.tag.toLowerCase()!==n.tagName.toLowerCase()){let r=document.createElement(String(o.tag).toLowerCase());for(let c of n.attributes)r.setAttribute(c.name,c.value);for(;n.firstChild;)r.appendChild(n.firstChild);if(n.parentNode?.replaceChild(r,n),this.liveMap.set(t,r),o.semanticId&&h(o.semanticId)!==t){let c=h(o.semanticId);r.classList.remove(t),r.classList.add(c),r.setAttribute("wdl-comp",c),this.liveMap.delete(t),this.liveMap.set(c,r)}}else if(o.semanticId&&h(o.semanticId)!==t){let r=h(o.semanticId);n.classList.remove(t),n.classList.add(r),n.setAttribute("wdl-comp",r),this.liveMap.delete(t),this.liveMap.set(r,n)}this._log("op",`update #${t}`,o);break}default:this._log("warn",`layers: unknown action "${i}" \u2014 remounting`),this._mount()}}_handleAttr(e){let{action:i,targetId:s,payload:a}=e,t=h(s);if(i==="set"||i==="update"){this._applyAttrs(t,a||{}),this._log("op",`attr.${i} #${t}`,a);return}if(i==="remove"){let n=this.liveMap.get(t);if(!n)return;let o=a;o==null||o===""?(n.textContent="",n.className=t,this._log("op",`attr.remove entire #${t}`)):o==="text"?(n.textContent="",this._log("op",`attr.remove text from #${t}`)):o==="class"?(n.className=t,this._log("op",`attr.remove class from #${t}`)):o==="html"?(n.innerHTML="",this._log("op",`attr.remove html from #${t}`)):(n.removeAttribute(o),this._log("op",`attr.remove ${o} from #${t}`))}}_applyAttrs(e,i){if(!i||typeof i!="object")return;let s=this.liveMap.get(e);if(!s){this._log("warn",`attr: #${e} not in liveMap`);return}if(i.text!==void 0&&(s.textContent=String(i.text)),i.html!==void 0&&(s.innerHTML=String(i.html)),i.class!==void 0){let a=String(i.class).trim();s.className=a?`${e} ${a}`:e}for(let[a,t]of Object.entries(i))if(!f.has(a)){if(a==="style"&&t&&typeof t=="object"){Object.assign(s.style,t);continue}t==null?s.removeAttribute(a):s.setAttribute(a,String(t))}}_handleData(e){let{action:i,targetId:s,payload:a}=e,t=s||"";if(typeof this.onDataBind=="function")try{this.onDataBind(this.container,t,i==="remove"?void 0:a)}catch(n){this._log("warn","onDataBind error",n)}this._log("op",`data.${i} ${t}`,i==="remove"?void 0:a)}_handleVariant(e){let{targetId:i,payload:s}=e,a=h(i)||this._rootSemanticId(),t=this.liveMap.get(a);if(!t){let n=this._rootSemanticId(),o=n?this.liveMap.get(n):null;o&&(s?o.dataset.variant=String(s):delete o.dataset.variant,this._log("op",`variant \u2192 ${s||"(none)"} on #${n}`));return}s?t.dataset.variant=String(s):delete t.dataset.variant,this._log("op",`variant \u2192 ${s||"(none)"} on #${a}`)}_applyRootVariant(){let e=this.component.attr?.list?.()??this.component.getSnapshot?.()?.attr??{},i=this._rootSemanticId();if(!i)return;let s="."+i,a=e[s]?.["data-variant"]??e["."]?.["data-variant"]??null;a&&this.liveMap.has(i)&&(this.liveMap.get(i).dataset.variant=String(a))}_rootSemanticId(){for(let[i,s]of this.liveMap)if(s.parentNode===this.container)return i;let e=this.liveMap.keys().next();return e.done?"":e.value}_handleRegistry(e){this._syncRegistryStyles(),this._log("op",`registry.${e.action}`)}_syncRegistryStyles(){if(!this.styleTarget)return;let e=this.component.registry?.get?.()??this.component.getSnapshot?.()?.registry??null,i=this.component.id||this.component.getSnapshot?.()?.id||"comp";if(this._styleEl||(this._styleEl=document.createElement("style"),this._styleEl.setAttribute("data-wdl-dom",i),this.styleTarget.appendChild(this._styleEl)),!e){this._styleEl.textContent="";return}let s=Array.isArray(e.rules)?e.rules:[],a=[];if(e.vars&&typeof e.vars=="object"){let t=Object.entries(e.vars).map(([n,o])=>`--${n}: ${o};`).join(" ");t&&a.push(`:root, [wdl-comp] { ${t} }`)}for(let t of s){if(!t||!t.selector)continue;let n=typeof t.css=="string"?t.css:t.css&&typeof t.css=="object"?Object.entries(t.css).map(([c,d])=>`${u(c)}: ${d};`).join(" "):"";if(!n)continue;let o=t.selector.startsWith("&")?t.selector.replace("&",`[wdl-comp="${i}"]`):t.selector,r=t.media?`@media ${t.media} { ${o} { ${n} } }`:`${o} { ${n} }`;a.push(r)}this._styleEl.textContent=a.join(`
|
|
2
|
-
`)}_log(e,...i){if(!this.debug&&e!=="warn")return;let
|
|
1
|
+
var N=new Set(["text","class","html","alpine","htmx","attr-ref"]);function g(l,e){return!e||l==null?l??"":String(e).split(".").reduce((i,t)=>i!=null?i[t]:"",l)??""}function S(l,e){return typeof l!="string"?l:l.replace(/\$\{([\w.]+)\}/g,(i,t)=>{let s=g(e,t);return s!=null?String(s):""}).replace(/\{\{([\w.]+)\}\}/g,(i,t)=>{let s=g(e,t);return s!=null?String(s):""})}function f(l,e){return l==null?l:typeof l=="function"?"":typeof l=="string"?S(l,e):Array.isArray(l)?l.map(i=>f(i,e)):typeof l=="object"?Object.fromEntries(Object.entries(l).map(([i,t])=>[i,f(t,e)])):l}function m(l){if(typeof l!="string"){if(l&&typeof l=="object")return{tag:String(l.tag||"div").toLowerCase(),semanticId:String(l.semanticId||l.id||"").replace(/^\./,""),repeator:l.repeator||l.loopKey||null};throw new Error("[wdl-dom] invalid layer expression")}let i=l.trim(),t=null,s=i.indexOf("*");s!==-1&&(t=i.slice(s+1),i=i.slice(0,s));let n=i.match(/^([a-zA-Z][a-zA-Z0-9_-]*)(?:\.([a-zA-Z0-9_-]+))?$/);return n?{tag:n[1].toLowerCase(),semanticId:(n[2]||"").replace(/^\./,""),repeator:t}:{tag:i.toLowerCase()||"div",semanticId:"",repeator:t}}function w(l){if(Array.isArray(l))return structuredClone(l);if(typeof l!="string"||!l.trim())return[];let e={tag:"__root__",semanticId:"",children:[]},i=[e],t=0,s=()=>i[i.length-1];for(;t<l.length;){let n=l[t];if(/\s/.test(n)){t++;continue}if(n===">"){let r=s().children[s().children.length-1];r&&i.push(r),t++;continue}if(n==="+"){t++;continue}if(n==="<"){if(t++,t<l.length&&l[t]==="*"){t++;let r="";for(;t<l.length&&/\d/.test(l[t]);)r+=l[t++];let h=r?parseInt(r,10):1;for(let d=0;d<h;d++)i.length>1&&i.pop()}else if(t<l.length&&l[t]==="@"){t++;let r="";for(;t<l.length&&/\d/.test(l[t]);)r+=l[t++];let d=(r?parseInt(r,10):0)+1;for(;i.length>d&&i.length>1;)i.pop()}else{let r=1;for(;t<l.length&&l[t]==="<";)r++,t++;for(let h=0;h<r;h++)i.length>1&&i.pop()}continue}let c="";for(;t<l.length&&/[a-zA-Z0-9_-]/.test(l[t]);)c+=l[t++];let o="";if(l[t]===".")for(t++;t<l.length&&/[a-zA-Z0-9_-]/.test(l[t]);)o+=l[t++];let a=null;if(l[t]==="*"){t++;let r="";for(;t<l.length&&/[a-zA-Z0-9_.]/.test(l[t]);)r+=l[t++];r&&(a=r)}s().children.push({tag:c.toLowerCase()||"div",semanticId:o,repeator:a,children:[]})}return e.children}function p(l){return l==null?"":String(l).replace(/^\./,"")}var y=class{constructor(e={}){let{container:i,component:t,onDataBind:s=null,styleTarget:n=typeof document<"u"?document.head:null,debug:c=!1}=e;if(!i)throw new Error("[wdl-dom] container is required");if(!t)throw new Error("[wdl-dom] component (ComponentState) is required");if(this.container=typeof i=="string"?document.querySelector(i):i,!this.container)throw new Error("[wdl-dom] container not found");this.component=t,this.onDataBind=s,this.styleTarget=n,this.debug=c,this.liveMap=new Map,this.liveNodes=new Map,this._loopBindings=[],this._unsubs=[],this._styleEl=null,this._mount(),this._bindStateEvents()}remount(){this._mount()}getLiveMap(){return new Map(this.liveMap)}getLiveNodes(e){let i=p(e);return[...this.liveNodes.get(i)||[]]}getNode(e,i=0){return this.getLiveNodes(e)[i]||null}destroy(){this._unsubs.forEach(e=>{try{e()}catch{}}),this._unsubs=[],this.container.replaceChildren(),this.liveMap.clear(),this.liveNodes.clear(),this._loopBindings=[],this._styleEl&&this._styleEl.parentNode&&this._styleEl.parentNode.removeChild(this._styleEl),this._styleEl=null}_registerNode(e,i){if(!e)return;let t=p(e);this.liveNodes.has(t)||this.liveNodes.set(t,[]);let s=this.liveNodes.get(t);s.includes(i)||s.push(i),this.liveMap.set(t,s[0])}_unregisterNode(e,i){if(!e)return;let t=p(e);if(this.liveNodes.has(t)){let s=this.liveNodes.get(t),n=s.indexOf(i);n!==-1&&s.splice(n,1),s.length===0?(this.liveNodes.delete(t),this.liveMap.delete(t)):this.liveMap.set(t,s[0])}else this.liveMap.delete(t)}_unregisterElementHierarchy(e){if(!e)return;let i=e.getAttribute?.("wdl-comp");if(i&&this._unregisterNode(i,e),Array.isArray(e.children))for(let t of e.children)this._unregisterElementHierarchy(t)}_getComponentData(){return this.component.data?.get?.()??this.component.getSnapshot?.()?.data??{}}_getComponentAttrMap(){return this.component.attr?.list?.()??this.component.getSnapshot?.()?.attr??{}}_resolveLoopItems(e,i){if(!e)return[];if(/^\d+$/.test(String(e))){let s=parseInt(e,10);return Array.from({length:Math.max(0,s)},(n,c)=>({_index:c}))}let t=g(i,e);return Array.isArray(t)?t:typeof t=="number"?Array.from({length:Math.max(0,t)},(s,n)=>({_index:n})):[]}_createItemScope(e,i,t){let s=t||this._getComponentData();return typeof e=="object"&&e!==null?{...s,...e,item:e,_index:i}:{...s,value:e,item:e,_index:i}}_mount(){this.container.replaceChildren(),this.liveMap.clear(),this.liveNodes.clear(),this._loopBindings=[];let e;try{e=typeof this.component.layers?.tree=="function"?this.component.layers.tree():w(this.component.layers?.list?.()??this.component.getSnapshot?.()?.layers)}catch(t){this._log("warn","tree() failed, falling back to simple parse",t);let s=this.component.getSnapshot?.()?.layers??"";e=w(s)}Array.isArray(e)||(e=[]);let i=this._getComponentData();for(let t of e)this._mountLayerNode(this.container,t,i);this._applyRootVariant(),this._syncRegistryStyles(),this._log("mount",`mounted ${this.liveMap.size} semantic keys, ${this._loopBindings.length} loops`)}_mountLayerNode(e,i,t){if(i.repeator){let n=this._resolveLoopItems(i.repeator,t),c=[];for(let a=0;a<n.length;a++){let r=this._createItemScope(n[a],a,t),h=this._createSingleElement(i,r,a);e.appendChild(h),c.push(h)}let o=null;typeof document.createComment=="function"&&(o=document.createComment(`wdl-loop:${i.repeator}`),e.appendChild(o)),this._loopBindings.push({parentEl:e,node:i,repeator:i.repeator,elements:c,anchor:o});return}let s=this._createSingleElement(i,t);e.appendChild(s)}_createSingleElement(e,i,t){let s=(e.tag||"div").toLowerCase(),n=p(e.semanticId||e.id||""),c=document.createElement(s);n&&(c.classList.add(n),c.setAttribute("wdl-comp",n),this._registerNode(n,c));let o=t!==void 0?t:i?._index;if(o!==void 0){let a=String(o);c.setAttribute("data-wdl-index",a),c.dataset&&(c.dataset.wdlIndex=a)}if(this._applyNodeAttrs(c,e,i),Array.isArray(e.children))for(let a of e.children)this._mountLayerNode(c,a,i);return c}_applyNodeAttrs(e,i,t){let s=this._getComponentAttrMap(),n=p(i.semanticId||i.id||""),c=(i.tag||"div").toLowerCase(),o=s[c]||{},a=n?s["."+n]||{}:{},r=n?s[n]||{}:{},h={...o,...a,...r},d=f(h,t);this._applyPropsToElement(e,n,d)}_applyPropsToElement(e,i,t){if(!(!t||typeof t!="object")){if(t.text!==void 0&&(e.textContent=String(t.text)),t.html!==void 0&&(e.innerHTML=String(t.html)),t.class!==void 0){let s=String(t.class).trim();e.className=i?s?`${i} ${s}`:i:s}for(let[s,n]of Object.entries(t))if(!N.has(s)){if(s==="style"&&n&&typeof n=="object"){Object.assign(e.style,n);continue}n==null?e.removeAttribute(s):e.setAttribute(s,String(n))}}}_bindStateEvents(){let e=this.component;if(typeof e.on!="function"){this._log("warn","component has no .on() \u2014 events will not be applied");return}let i=s=>this._onStateEvent(s),t=["layers:change","attr:change","data:change","variant:change","registry:change"];for(let s of t){let n=e.on(s,i);typeof n=="function"?this._unsubs.push(n):this._unsubs.push(()=>e.off?.(s,i))}}_onStateEvent(e){if(!(!e||!e.type))switch(this._log("event",e.type,e.action,e.targetId,e.payload),e.type){case"layers:change":this._handleLayers(e);break;case"attr:change":this._handleAttr(e);break;case"data:change":this._handleData(e);break;case"variant:change":this._handleVariant(e);break;case"registry:change":this._handleRegistry(e);break;default:this._log("warn","unknown event type",e.type)}}_handleLayers(e){let{action:i,targetId:t,payload:s}=e,n=p(t),c=this._getComponentData();switch(i){case"set":{this._mount();break}case"append":{let o=this.getNode(n);if(!o){this._log("warn",`append: parent "${n}" not in liveMap \u2014 remounting`),this._mount();return}let a=m(s);this._mountLayerNode(o,a,c),this._log("op",`append <${a.tag}.${a.semanticId}> \u2192 #${n}`);break}case"prepend":{let o=this.getNode(n);if(!o){this._log("warn",`prepend: parent "${n}" not in liveMap \u2014 remounting`),this._mount();return}let a=m(s),r=this._createSingleElement(a,c);o.insertBefore(r,o.firstChild),this._log("op",`prepend <${a.tag}.${a.semanticId}> \u2192 #${n}`);break}case"before":case"after":{let o=this.getNode(n);if(!o||!o.parentNode){this._log("warn",`${i}: target "${n}" missing \u2014 remounting`),this._mount();return}let a=m(s),r=this._createSingleElement(a,c);i==="before"?o.parentNode.insertBefore(r,o):o.parentNode.insertBefore(r,o.nextSibling),this._log("op",`${i} <${a.tag}.${a.semanticId}> relative to #${n}`);break}case"wrap":{let o=this.getNode(n);if(!o||!o.parentNode){this._log("warn",`wrap: target "${n}" missing \u2014 remounting`),this._mount();return}let a=m(s),r=this._createSingleElement(a,c);o.parentNode.insertBefore(r,o),r.appendChild(o),this._log("op",`wrap #${n} with <${a.tag}.${a.semanticId}>`);break}case"unwrap":{let o=this.getNode(n);if(!o||!o.parentNode){this._log("warn",`unwrap: target "${n}" missing \u2014 remounting`),this._mount();return}let a=o.parentNode;for(;o.firstChild;)a.insertBefore(o.firstChild,o);this._unregisterElementHierarchy(o),o.remove(),this._log("op",`unwrap #${n}`);break}case"move":{let o=this.getNode(n),{targetSemanticId:a,position:r}=s||{},h=p(a),d=this.getNode(h);if(!o||!d){this._log("warn",`move: source "${n}" or target "${h}" missing \u2014 remounting`),this._mount();return}r==="before"?d.parentNode?.insertBefore(o,d):r==="after"?d.parentNode?.insertBefore(o,d.nextSibling):d.appendChild(o),this._log("op",`move #${n} -> ${r} #${h}`);break}case"remove":{let o=this.getLiveNodes(n);if(o.length===0){this._log("warn",`remove: #${n} not in liveMap`);return}for(let a of o)this._unregisterElementHierarchy(a),a.remove();this._loopBindings=this._loopBindings.filter(a=>a.node.semanticId!==n),this._log("op",`remove #${n}`);break}case"update":{let o=this.getNode(n);if(!o){this._log("warn",`update: #${n} missing \u2014 remounting`),this._mount();return}let a=s||{};if(a.tag&&a.tag.toLowerCase()!==o.tagName.toLowerCase()){let r=document.createElement(String(a.tag).toLowerCase());for(let d of o.attributes)r.setAttribute(d.name,d.value);for(;o.firstChild;)r.appendChild(o.firstChild);o.parentNode?.replaceChild(r,o),this._unregisterNode(n,o);let h=a.semanticId?p(a.semanticId):n;a.semanticId&&h!==n&&(r.classList.remove(n),r.classList.add(h),r.setAttribute("wdl-comp",h)),this._registerNode(h,r)}else if(a.semanticId&&p(a.semanticId)!==n){let r=p(a.semanticId);o.classList.remove(n),o.classList.add(r),o.setAttribute("wdl-comp",r),this._unregisterNode(n,o),this._registerNode(r,o)}this._log("op",`update #${n}`,a);break}default:this._log("warn",`layers: unknown action "${i}" \u2014 remounting`),this._mount()}}_handleAttr(e){let{action:i,targetId:t,payload:s}=e,n=p(t);if(i==="set"||i==="update"){let c=this.getLiveNodes(n);if(c.length===0){this._log("warn",`attr: #${n} not in liveMap`);return}let o=this._getComponentData();for(let a of c){let r=a.getAttribute("data-wdl-index"),h=o;if(r!=null){let u=parseInt(r,10),v=this._findLoopForElement(a);if(v){let _=g(o,v.repeator);Array.isArray(_)&&_[u]!==void 0?h=this._createItemScope(_[u],u,o):h={...o,_index:u}}else h={...o,_index:u}}let d=f(s||{},h);this._applyPropsToElement(a,n,d)}this._log("op",`attr.${i} #${n}`,s);return}if(i==="remove"){let c=this.getLiveNodes(n);if(c.length===0)return;let o=s;for(let a of c)o==null||o===""?(a.textContent="",a.className=n):o==="text"?a.textContent="":o==="class"?a.className=n:o==="html"?a.innerHTML="":a.removeAttribute(o);this._log("op",`attr.remove ${o||"all"} from #${n}`)}}_applyAttrs(e,i){let t=this.getLiveNodes(e);if(t.length===0){this._log("warn",`attr: #${e} not in liveMap`);return}let s=this._getComponentData();for(let n of t){let c=n.getAttribute("data-wdl-index"),o=s;if(c!=null){let r=parseInt(c,10),h=this._findLoopForElement(n);if(h){let d=g(s,h.repeator);Array.isArray(d)&&d[r]!==void 0?o=this._createItemScope(d[r],r,s):o={...s,_index:r}}}let a=f(i||{},o);this._applyPropsToElement(n,e,a)}}_findLoopForElement(e){return this._loopBindings.find(i=>i.elements.includes(e))||null}_handleData(e){let{action:i,targetId:t,payload:s}=e,n=t||"",c=this._getComponentData();if(this._reconcileLoops(n,c),this._reconcileStaticDataBindings(n,c),typeof this.onDataBind=="function")try{this.onDataBind(this.container,n,i==="remove"?void 0:s)}catch(o){this._log("warn","onDataBind error",o)}this._log("op",`data.${i} ${n}`,i==="remove"?void 0:s)}_reconcileLoops(e,i){for(let t of this._loopBindings){if(!(!e||t.repeator===e||e.startsWith(t.repeator+".")||t.repeator.startsWith(e+".")))continue;let n=this._resolveLoopItems(t.repeator,i),c=n.length,o=t.elements.length,a=Math.min(o,c);for(let r=0;r<a;r++){let h=t.elements[r],d=this._createItemScope(n[r],r,i);h.setAttribute("data-wdl-index",String(r)),h.dataset&&(h.dataset.wdlIndex=String(r)),this._updateElementSubtree(h,t.node,d,r)}if(c>o)for(let r=o;r<c;r++){let h=this._createItemScope(n[r],r,i),d=this._createSingleElement(t.node,h,r);if(t.anchor&&t.anchor.parentNode===t.parentEl)t.parentEl.insertBefore(d,t.anchor);else if(t.elements.length>0){let u=t.elements[t.elements.length-1];t.parentEl.insertBefore(d,u.nextSibling)}else t.parentEl.appendChild(d);t.elements.push(d)}if(c<o)for(let r=o-1;r>=c;r--){let h=t.elements[r];this._unregisterElementHierarchy(h),h.remove(),t.elements.pop()}}}_updateElementSubtree(e,i,t,s){if(this._applyNodeAttrs(e,i,t),Array.isArray(i.children)&&Array.isArray(e.children)){let n=0;for(let c of i.children)if(c.repeator){let o=this._loopBindings.find(a=>a.parentEl===e&&a.node===c);o&&this._reconcileLoops(o.repeator,t)}else e.children[n]&&(this._updateElementSubtree(e.children[n],c,t,s),n++)}}_reconcileStaticDataBindings(e,i){let t=this._getComponentAttrMap();for(let[s,n]of Object.entries(t)){let c=p(s),o=JSON.stringify(n);if(!o.includes("${")&&!o.includes("{{")||e&&!o.includes(e))continue;let a=this.getLiveNodes(c);for(let r of a){if(r.hasAttribute("data-wdl-index")||this._findLoopForElement(r))continue;let h=f(n,i);this._applyPropsToElement(r,c,h)}}}_handleVariant(e){let{targetId:i,payload:t}=e,s=p(i)||this._rootSemanticId(),n=this.getNode(s);if(!n){let c=this._rootSemanticId(),o=c?this.getNode(c):null;o&&(t?o.dataset.variant=String(t):delete o.dataset.variant,this._log("op",`variant \u2192 ${t||"(none)"} on #${c}`));return}t?n.dataset.variant=String(t):delete n.dataset.variant,this._log("op",`variant \u2192 ${t||"(none)"} on #${s}`)}_applyRootVariant(){let e=this._getComponentAttrMap(),i=this._rootSemanticId();if(!i)return;let t="."+i,s=e[t]?.["data-variant"]??e["."]?.["data-variant"]??null;if(s&&this.liveMap.has(i)){let n=this.getNode(i);n&&n.dataset&&(n.dataset.variant=String(s))}}_rootSemanticId(){for(let[i,t]of this.liveMap)if(t.parentNode===this.container)return i;let e=this.liveMap.keys().next();return e.done?"":e.value}_handleRegistry(e){this._syncRegistryStyles(),this._log("op",`registry.${e.action}`)}_syncRegistryStyles(){if(!this.styleTarget)return;let e=this.component.registry?.get?.()??this.component.getSnapshot?.()?.registry??null,i=this.component.id||this.component.getSnapshot?.()?.id||"comp";if(this._styleEl||(this._styleEl=document.createElement("style"),this._styleEl.setAttribute("data-wdl-dom",i),this.styleTarget.appendChild(this._styleEl)),!e){this._styleEl.textContent="";return}let t=Array.isArray(e.rules)?e.rules:[],s=[];if(e.vars&&typeof e.vars=="object"){let n=Object.entries(e.vars).map(([c,o])=>`--${c}: ${o};`).join(" ");n&&s.push(`:root, [wdl-comp] { ${n} }`)}for(let n of t){if(!n||!n.selector)continue;let c=typeof n.css=="string"?n.css:n.css&&typeof n.css=="object"?Object.entries(n.css).map(([r,h])=>`${$(r)}: ${h};`).join(" "):"";if(!c)continue;let o=n.selector.startsWith("&")?n.selector.replace("&",`[wdl-comp="${i}"]`):n.selector,a=n.media?`@media ${n.media} { ${o} { ${c} } }`:`${o} { ${c} }`;s.push(a)}this._styleEl.textContent=s.join(`
|
|
2
|
+
`)}_log(e,...i){if(!this.debug&&e!=="warn")return;let t=`[wdl-dom:${e}]`;e==="warn"?console.warn(t,...i):console.log(t,...i)}};function $(l){return String(l).replace(/[A-Z]/g,e=>"-"+e.toLowerCase())}function E(l){return new y(l)}var A=E;export{y as WdlDom,E as createWdlDom,A as default,p as normalizeId,m as parseLayerToken,w as parseLayersSimple,f as resolveAll,g as resolvePath,S as resolveStr};
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
* No innerHTML on updates.
|
|
7
7
|
*
|
|
8
8
|
* State events handled (from @ruledwdl/state):
|
|
9
|
-
* layers:change actions: set | append | before | after | wrap | remove | update
|
|
9
|
+
* layers:change actions: set | append | prepend | before | after | wrap | unwrap | move | remove | update
|
|
10
10
|
* attr:change actions: set | update | remove
|
|
11
|
-
* data:change actions: set | remove
|
|
11
|
+
* data:change actions: set | update | remove (surgical loop child reconciliation & item-scoped binding)
|
|
12
12
|
* variant:change actions: set
|
|
13
13
|
* registry:change actions: set | update | addRule | removeRule
|
|
14
14
|
*
|
|
@@ -23,59 +23,109 @@
|
|
|
23
23
|
* }
|
|
24
24
|
*
|
|
25
25
|
* Usage:
|
|
26
|
-
* import { createWdlDom } from '
|
|
26
|
+
* import { createWdlDom } from '@ruledwdl/dom';
|
|
27
27
|
* // or: import { ComponentManager } from '@ruledwdl/state';
|
|
28
28
|
*
|
|
29
|
-
* const
|
|
29
|
+
* const comp = manager.create('pricing-card', {
|
|
30
|
+
* layers: 'ul.features > li.feature*features',
|
|
31
|
+
* attr: { '.feature': { text: '${text}' } },
|
|
32
|
+
* data: { features: [{ text: 'First' }, { text: 'Second' }] }
|
|
33
|
+
* });
|
|
30
34
|
* const dom = createWdlDom({
|
|
31
35
|
* container: document.getElementById('app'),
|
|
32
|
-
* component:
|
|
33
|
-
* // optional:
|
|
34
|
-
* // onDataBind: (el, path, value) => { ... },
|
|
35
|
-
* // styleTarget: document.head,
|
|
36
|
+
* component: comp
|
|
36
37
|
* });
|
|
37
38
|
*
|
|
38
|
-
* // later:
|
|
39
|
+
* // later: comp.data.set('features', [{ text: 'Updated' }]); // → surgical row reconciliation
|
|
39
40
|
* // destroy: dom.destroy();
|
|
40
41
|
*/
|
|
41
42
|
|
|
42
43
|
// ---------------------------------------------------------------------------
|
|
43
|
-
// Helpers
|
|
44
|
+
// Helpers & Data Resolvers
|
|
44
45
|
// ---------------------------------------------------------------------------
|
|
45
46
|
|
|
46
|
-
const ATTR_SKIP = new Set(['text', 'class', 'html']);
|
|
47
|
+
const ATTR_SKIP = new Set(['text', 'class', 'html', 'alpine', 'htmx', 'attr-ref']);
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Resolve dot-notated path on an object (e.g. "user.name" or "text")
|
|
51
|
+
*/
|
|
52
|
+
export function resolvePath(obj, path) {
|
|
53
|
+
if (!path || obj == null) return obj ?? '';
|
|
54
|
+
return String(path).split('.').reduce((a, k) => (a != null ? a[k] : ''), obj) ?? '';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Resolve template strings with ${path} or {{path}} expressions
|
|
59
|
+
*/
|
|
60
|
+
export function resolveStr(str, data) {
|
|
61
|
+
if (typeof str !== 'string') return str;
|
|
62
|
+
return str
|
|
63
|
+
.replace(/\$\{([\w.]+)\}/g, (_, p) => {
|
|
64
|
+
const val = resolvePath(data, p);
|
|
65
|
+
return val != null ? String(val) : '';
|
|
66
|
+
})
|
|
67
|
+
.replace(/\{\{([\w.]+)\}\}/g, (_, p) => {
|
|
68
|
+
const val = resolvePath(data, p);
|
|
69
|
+
return val != null ? String(val) : '';
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Deeply resolve string templates within objects/arrays
|
|
75
|
+
*/
|
|
76
|
+
export function resolveAll(obj, data) {
|
|
77
|
+
if (obj == null) return obj;
|
|
78
|
+
if (typeof obj === 'function') return '';
|
|
79
|
+
if (typeof obj === 'string') return resolveStr(obj, data);
|
|
80
|
+
if (Array.isArray(obj)) return obj.map((v) => resolveAll(v, data));
|
|
81
|
+
if (typeof obj === 'object') {
|
|
82
|
+
return Object.fromEntries(
|
|
83
|
+
Object.entries(obj).map(([k, v]) => [k, resolveAll(v, data)])
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
return obj;
|
|
87
|
+
}
|
|
47
88
|
|
|
48
89
|
/**
|
|
49
|
-
* Parse a single layer token: "button.cta" | "
|
|
50
|
-
* @returns {{ tag: string, semanticId: string }}
|
|
90
|
+
* Parse a single layer token: "button.cta" | "li.feature*features" | "div.card*3"
|
|
91
|
+
* @returns {{ tag: string, semanticId: string, repeator: string | null }}
|
|
51
92
|
*/
|
|
52
|
-
function parseLayerToken(expr) {
|
|
93
|
+
export function parseLayerToken(expr) {
|
|
53
94
|
if (typeof expr !== 'string') {
|
|
54
95
|
if (expr && typeof expr === 'object') {
|
|
55
96
|
return {
|
|
56
97
|
tag: String(expr.tag || 'div').toLowerCase(),
|
|
57
98
|
semanticId: String(expr.semanticId || expr.id || '').replace(/^\./, ''),
|
|
99
|
+
repeator: expr.repeator || expr.loopKey || null,
|
|
58
100
|
};
|
|
59
101
|
}
|
|
60
102
|
throw new Error('[wdl-dom] invalid layer expression');
|
|
61
103
|
}
|
|
62
104
|
const trimmed = expr.trim();
|
|
63
|
-
|
|
105
|
+
let str = trimmed;
|
|
106
|
+
let repeator = null;
|
|
107
|
+
const multIdx = str.indexOf('*');
|
|
108
|
+
if (multIdx !== -1) {
|
|
109
|
+
repeator = str.slice(multIdx + 1);
|
|
110
|
+
str = str.slice(0, multIdx);
|
|
111
|
+
}
|
|
112
|
+
const m = str.match(/^([a-zA-Z][a-zA-Z0-9_-]*)(?:\.([a-zA-Z0-9_-]+))?$/);
|
|
64
113
|
if (!m) {
|
|
65
114
|
// fallback: treat whole string as tag
|
|
66
|
-
return { tag:
|
|
115
|
+
return { tag: str.toLowerCase() || 'div', semanticId: '', repeator };
|
|
67
116
|
}
|
|
68
117
|
return {
|
|
69
118
|
tag: m[1].toLowerCase(),
|
|
70
119
|
semanticId: (m[2] || '').replace(/^\./, ''),
|
|
120
|
+
repeator,
|
|
71
121
|
};
|
|
72
122
|
}
|
|
73
123
|
|
|
74
124
|
/**
|
|
75
|
-
*
|
|
125
|
+
* Simple layers string → tree parser (supports >, +, <, <*N, <@N, and *repeator).
|
|
76
126
|
* For full WDL grammar prefer tree from component.layers.tree().
|
|
77
127
|
*/
|
|
78
|
-
function parseLayersSimple(str) {
|
|
128
|
+
export function parseLayersSimple(str) {
|
|
79
129
|
if (Array.isArray(str)) return structuredClone(str);
|
|
80
130
|
if (typeof str !== 'string' || !str.trim()) return [];
|
|
81
131
|
|
|
@@ -87,20 +137,57 @@ function parseLayersSimple(str) {
|
|
|
87
137
|
|
|
88
138
|
while (i < str.length) {
|
|
89
139
|
const ch = str[i];
|
|
90
|
-
if (/\s/.test(ch)) {
|
|
140
|
+
if (/\s/.test(ch)) {
|
|
141
|
+
i++;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
91
144
|
if (ch === '>') {
|
|
92
145
|
const last = top().children[top().children.length - 1];
|
|
93
146
|
if (last) stack.push(last);
|
|
94
147
|
i++;
|
|
95
148
|
continue;
|
|
96
149
|
}
|
|
97
|
-
if (ch === '+') {
|
|
150
|
+
if (ch === '+') {
|
|
151
|
+
i++;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
98
154
|
if (ch === '<') {
|
|
99
155
|
i++;
|
|
100
|
-
if (
|
|
156
|
+
if (i < str.length && str[i] === '*') {
|
|
157
|
+
i++;
|
|
158
|
+
let numStr = '';
|
|
159
|
+
while (i < str.length && /\d/.test(str[i])) {
|
|
160
|
+
numStr += str[i++];
|
|
161
|
+
}
|
|
162
|
+
const count = numStr ? parseInt(numStr, 10) : 1;
|
|
163
|
+
for (let k = 0; k < count; k++) {
|
|
164
|
+
if (stack.length > 1) stack.pop();
|
|
165
|
+
}
|
|
166
|
+
} else if (i < str.length && str[i] === '@') {
|
|
167
|
+
i++;
|
|
168
|
+
let numStr = '';
|
|
169
|
+
while (i < str.length && /\d/.test(str[i])) {
|
|
170
|
+
numStr += str[i++];
|
|
171
|
+
}
|
|
172
|
+
const targetDepth = numStr ? parseInt(numStr, 10) : 0;
|
|
173
|
+
const targetStackLen = targetDepth + 1;
|
|
174
|
+
while (stack.length > targetStackLen && stack.length > 1) {
|
|
175
|
+
stack.pop();
|
|
176
|
+
}
|
|
177
|
+
} else {
|
|
178
|
+
let count = 1;
|
|
179
|
+
while (i < str.length && str[i] === '<') {
|
|
180
|
+
count++;
|
|
181
|
+
i++;
|
|
182
|
+
}
|
|
183
|
+
for (let k = 0; k < count; k++) {
|
|
184
|
+
if (stack.length > 1) stack.pop();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
101
187
|
continue;
|
|
102
188
|
}
|
|
103
|
-
|
|
189
|
+
|
|
190
|
+
// Element token
|
|
104
191
|
let tag = '';
|
|
105
192
|
while (i < str.length && /[a-zA-Z0-9_-]/.test(str[i])) tag += str[i++];
|
|
106
193
|
let semanticId = '';
|
|
@@ -108,12 +195,24 @@ function parseLayersSimple(str) {
|
|
|
108
195
|
i++;
|
|
109
196
|
while (i < str.length && /[a-zA-Z0-9_-]/.test(str[i])) semanticId += str[i++];
|
|
110
197
|
}
|
|
111
|
-
|
|
198
|
+
let repeator = null;
|
|
199
|
+
if (str[i] === '*') {
|
|
200
|
+
i++;
|
|
201
|
+
let rep = '';
|
|
202
|
+
while (i < str.length && /[a-zA-Z0-9_.]/.test(str[i])) rep += str[i++];
|
|
203
|
+
if (rep) repeator = rep;
|
|
204
|
+
}
|
|
205
|
+
top().children.push({
|
|
206
|
+
tag: tag.toLowerCase() || 'div',
|
|
207
|
+
semanticId,
|
|
208
|
+
repeator,
|
|
209
|
+
children: [],
|
|
210
|
+
});
|
|
112
211
|
}
|
|
113
212
|
return root.children;
|
|
114
213
|
}
|
|
115
214
|
|
|
116
|
-
function normalizeId(id) {
|
|
215
|
+
export function normalizeId(id) {
|
|
117
216
|
if (id == null) return '';
|
|
118
217
|
return String(id).replace(/^\./, '');
|
|
119
218
|
}
|
|
@@ -126,7 +225,7 @@ export class WdlDom {
|
|
|
126
225
|
/**
|
|
127
226
|
* @param {object} options
|
|
128
227
|
* @param {HTMLElement|string} options.container
|
|
129
|
-
* @param {object} options.component ComponentState instance (has .on, .layers, .attr, .getSnapshot)
|
|
228
|
+
* @param {object} options.component ComponentState instance (has .on, .layers, .attr, .data, .getSnapshot)
|
|
130
229
|
* @param {(el: HTMLElement, path: string, value: any) => void} [options.onDataBind]
|
|
131
230
|
* @param {HTMLElement|Document} [options.styleTarget=document.head]
|
|
132
231
|
* @param {boolean} [options.debug=false]
|
|
@@ -154,9 +253,18 @@ export class WdlDom {
|
|
|
154
253
|
this.styleTarget = styleTarget;
|
|
155
254
|
this.debug = debug;
|
|
156
255
|
|
|
157
|
-
/** @type {Map<string, HTMLElement>} semanticId → element */
|
|
256
|
+
/** @type {Map<string, HTMLElement>} semanticId → first live element (backward compatible) */
|
|
158
257
|
this.liveMap = new Map();
|
|
159
258
|
|
|
259
|
+
/** @type {Map<string, HTMLElement[]>} semanticId → all live elements */
|
|
260
|
+
this.liveNodes = new Map();
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Active loop bindings for surgical reconciliation
|
|
264
|
+
* @type {Array<{ parentEl: HTMLElement, node: any, repeator: string, elements: HTMLElement[], anchor: any }>}
|
|
265
|
+
*/
|
|
266
|
+
this._loopBindings = [];
|
|
267
|
+
|
|
160
268
|
/** @type {Array<() => void>} */
|
|
161
269
|
this._unsubs = [];
|
|
162
270
|
|
|
@@ -176,25 +284,131 @@ export class WdlDom {
|
|
|
176
284
|
this._mount();
|
|
177
285
|
}
|
|
178
286
|
|
|
179
|
-
/** Current live map (read-only view) */
|
|
287
|
+
/** Current live map of first elements (read-only view) */
|
|
180
288
|
getLiveMap() {
|
|
181
289
|
return new Map(this.liveMap);
|
|
182
290
|
}
|
|
183
291
|
|
|
292
|
+
/** Get all live elements matching a semantic ID */
|
|
293
|
+
getLiveNodes(semanticId) {
|
|
294
|
+
const id = normalizeId(semanticId);
|
|
295
|
+
return [...(this.liveNodes.get(id) || [])];
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** Get single element by semantic ID and optional index */
|
|
299
|
+
getNode(semanticId, index = 0) {
|
|
300
|
+
const list = this.getLiveNodes(semanticId);
|
|
301
|
+
return list[index] || null;
|
|
302
|
+
}
|
|
303
|
+
|
|
184
304
|
/** Clean up listeners and DOM */
|
|
185
305
|
destroy() {
|
|
186
306
|
this._unsubs.forEach((u) => {
|
|
187
|
-
try {
|
|
307
|
+
try {
|
|
308
|
+
u();
|
|
309
|
+
} catch (_) {}
|
|
188
310
|
});
|
|
189
311
|
this._unsubs = [];
|
|
190
312
|
this.container.replaceChildren();
|
|
191
313
|
this.liveMap.clear();
|
|
314
|
+
this.liveNodes.clear();
|
|
315
|
+
this._loopBindings = [];
|
|
192
316
|
if (this._styleEl && this._styleEl.parentNode) {
|
|
193
317
|
this._styleEl.parentNode.removeChild(this._styleEl);
|
|
194
318
|
}
|
|
195
319
|
this._styleEl = null;
|
|
196
320
|
}
|
|
197
321
|
|
|
322
|
+
// -----------------------------------------------------------------------
|
|
323
|
+
// Node Registry Management
|
|
324
|
+
// -----------------------------------------------------------------------
|
|
325
|
+
|
|
326
|
+
_registerNode(id, el) {
|
|
327
|
+
if (!id) return;
|
|
328
|
+
const norm = normalizeId(id);
|
|
329
|
+
if (!this.liveNodes.has(norm)) {
|
|
330
|
+
this.liveNodes.set(norm, []);
|
|
331
|
+
}
|
|
332
|
+
const list = this.liveNodes.get(norm);
|
|
333
|
+
if (!list.includes(el)) {
|
|
334
|
+
list.push(el);
|
|
335
|
+
}
|
|
336
|
+
this.liveMap.set(norm, list[0]);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
_unregisterNode(id, el) {
|
|
340
|
+
if (!id) return;
|
|
341
|
+
const norm = normalizeId(id);
|
|
342
|
+
if (this.liveNodes.has(norm)) {
|
|
343
|
+
const list = this.liveNodes.get(norm);
|
|
344
|
+
const idx = list.indexOf(el);
|
|
345
|
+
if (idx !== -1) list.splice(idx, 1);
|
|
346
|
+
if (list.length === 0) {
|
|
347
|
+
this.liveNodes.delete(norm);
|
|
348
|
+
this.liveMap.delete(norm);
|
|
349
|
+
} else {
|
|
350
|
+
this.liveMap.set(norm, list[0]);
|
|
351
|
+
}
|
|
352
|
+
} else {
|
|
353
|
+
this.liveMap.delete(norm);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
_unregisterElementHierarchy(el) {
|
|
358
|
+
if (!el) return;
|
|
359
|
+
const compId = el.getAttribute?.('wdl-comp');
|
|
360
|
+
if (compId) {
|
|
361
|
+
this._unregisterNode(compId, el);
|
|
362
|
+
}
|
|
363
|
+
if (Array.isArray(el.children)) {
|
|
364
|
+
for (const child of el.children) {
|
|
365
|
+
this._unregisterElementHierarchy(child);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// -----------------------------------------------------------------------
|
|
371
|
+
// Data and Scope Helpers
|
|
372
|
+
// -----------------------------------------------------------------------
|
|
373
|
+
|
|
374
|
+
_getComponentData() {
|
|
375
|
+
return (
|
|
376
|
+
this.component.data?.get?.() ??
|
|
377
|
+
this.component.getSnapshot?.()?.data ??
|
|
378
|
+
{}
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
_getComponentAttrMap() {
|
|
383
|
+
return (
|
|
384
|
+
this.component.attr?.list?.() ??
|
|
385
|
+
this.component.getSnapshot?.()?.attr ??
|
|
386
|
+
{}
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
_resolveLoopItems(repeator, dataScope) {
|
|
391
|
+
if (!repeator) return [];
|
|
392
|
+
if (/^\d+$/.test(String(repeator))) {
|
|
393
|
+
const count = parseInt(repeator, 10);
|
|
394
|
+
return Array.from({ length: Math.max(0, count) }, (_, idx) => ({ _index: idx }));
|
|
395
|
+
}
|
|
396
|
+
const resolved = resolvePath(dataScope, repeator);
|
|
397
|
+
if (Array.isArray(resolved)) return resolved;
|
|
398
|
+
if (typeof resolved === 'number') {
|
|
399
|
+
return Array.from({ length: Math.max(0, resolved) }, (_, idx) => ({ _index: idx }));
|
|
400
|
+
}
|
|
401
|
+
return [];
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
_createItemScope(item, index, baseData) {
|
|
405
|
+
const data = baseData || this._getComponentData();
|
|
406
|
+
if (typeof item === 'object' && item !== null) {
|
|
407
|
+
return { ...data, ...item, item, _index: index };
|
|
408
|
+
}
|
|
409
|
+
return { ...data, value: item, item, _index: index };
|
|
410
|
+
}
|
|
411
|
+
|
|
198
412
|
// -----------------------------------------------------------------------
|
|
199
413
|
// Initial mount
|
|
200
414
|
// -----------------------------------------------------------------------
|
|
@@ -202,12 +416,18 @@ export class WdlDom {
|
|
|
202
416
|
_mount() {
|
|
203
417
|
this.container.replaceChildren();
|
|
204
418
|
this.liveMap.clear();
|
|
419
|
+
this.liveNodes.clear();
|
|
420
|
+
this._loopBindings = [];
|
|
205
421
|
|
|
206
422
|
let tree;
|
|
207
423
|
try {
|
|
208
|
-
tree =
|
|
209
|
-
|
|
210
|
-
|
|
424
|
+
tree =
|
|
425
|
+
typeof this.component.layers?.tree === 'function'
|
|
426
|
+
? this.component.layers.tree()
|
|
427
|
+
: parseLayersSimple(
|
|
428
|
+
this.component.layers?.list?.() ??
|
|
429
|
+
this.component.getSnapshot?.()?.layers
|
|
430
|
+
);
|
|
211
431
|
} catch (e) {
|
|
212
432
|
this._log('warn', 'tree() failed, falling back to simple parse', e);
|
|
213
433
|
const layers = this.component.getSnapshot?.()?.layers ?? '';
|
|
@@ -216,18 +436,10 @@ export class WdlDom {
|
|
|
216
436
|
|
|
217
437
|
if (!Array.isArray(tree)) tree = [];
|
|
218
438
|
|
|
219
|
-
|
|
220
|
-
const el = this._createElementFromNode(node);
|
|
221
|
-
this.container.appendChild(el);
|
|
222
|
-
}
|
|
439
|
+
const compData = this._getComponentData();
|
|
223
440
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
this.component.attr?.list?.() ??
|
|
227
|
-
this.component.getSnapshot?.()?.attr ??
|
|
228
|
-
{};
|
|
229
|
-
for (const [sel, props] of Object.entries(attrMap)) {
|
|
230
|
-
this._applyAttrs(normalizeId(sel), props);
|
|
441
|
+
for (const node of tree) {
|
|
442
|
+
this._mountLayerNode(this.container, node, compData);
|
|
231
443
|
}
|
|
232
444
|
|
|
233
445
|
// Apply variant if present on root
|
|
@@ -236,13 +448,50 @@ export class WdlDom {
|
|
|
236
448
|
// Registry styles
|
|
237
449
|
this._syncRegistryStyles();
|
|
238
450
|
|
|
239
|
-
this._log('mount', `mounted ${this.liveMap.size}
|
|
451
|
+
this._log('mount', `mounted ${this.liveMap.size} semantic keys, ${this._loopBindings.length} loops`);
|
|
240
452
|
}
|
|
241
453
|
|
|
242
454
|
/**
|
|
243
|
-
*
|
|
455
|
+
* Mount a layer node into a parent container/element.
|
|
456
|
+
* Handles loop expansion vs single element mounting.
|
|
244
457
|
*/
|
|
245
|
-
|
|
458
|
+
_mountLayerNode(parentEl, node, dataScope) {
|
|
459
|
+
if (node.repeator) {
|
|
460
|
+
const items = this._resolveLoopItems(node.repeator, dataScope);
|
|
461
|
+
const elements = [];
|
|
462
|
+
|
|
463
|
+
for (let i = 0; i < items.length; i++) {
|
|
464
|
+
const itemScope = this._createItemScope(items[i], i, dataScope);
|
|
465
|
+
const el = this._createSingleElement(node, itemScope, i);
|
|
466
|
+
parentEl.appendChild(el);
|
|
467
|
+
elements.push(el);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Create comment anchor to keep track of loop boundary
|
|
471
|
+
let anchor = null;
|
|
472
|
+
if (typeof document.createComment === 'function') {
|
|
473
|
+
anchor = document.createComment(`wdl-loop:${node.repeator}`);
|
|
474
|
+
parentEl.appendChild(anchor);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
this._loopBindings.push({
|
|
478
|
+
parentEl,
|
|
479
|
+
node,
|
|
480
|
+
repeator: node.repeator,
|
|
481
|
+
elements,
|
|
482
|
+
anchor,
|
|
483
|
+
});
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const el = this._createSingleElement(node, dataScope);
|
|
488
|
+
parentEl.appendChild(el);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Create a single DOM element for a layer node with item scope and attributes.
|
|
493
|
+
*/
|
|
494
|
+
_createSingleElement(node, dataScope, index) {
|
|
246
495
|
const tag = (node.tag || 'div').toLowerCase();
|
|
247
496
|
const id = normalizeId(node.semanticId || node.id || '');
|
|
248
497
|
const el = document.createElement(tag);
|
|
@@ -250,17 +499,77 @@ export class WdlDom {
|
|
|
250
499
|
if (id) {
|
|
251
500
|
el.classList.add(id);
|
|
252
501
|
el.setAttribute('wdl-comp', id);
|
|
253
|
-
this.
|
|
502
|
+
this._registerNode(id, el);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const effectiveIndex = index !== undefined ? index : dataScope?._index;
|
|
506
|
+
if (effectiveIndex !== undefined) {
|
|
507
|
+
const idxStr = String(effectiveIndex);
|
|
508
|
+
el.setAttribute('data-wdl-index', idxStr);
|
|
509
|
+
if (el.dataset) el.dataset.wdlIndex = idxStr;
|
|
254
510
|
}
|
|
255
511
|
|
|
512
|
+
this._applyNodeAttrs(el, node, dataScope);
|
|
513
|
+
|
|
256
514
|
if (Array.isArray(node.children)) {
|
|
257
515
|
for (const child of node.children) {
|
|
258
|
-
|
|
516
|
+
this._mountLayerNode(el, child, dataScope);
|
|
259
517
|
}
|
|
260
518
|
}
|
|
519
|
+
|
|
261
520
|
return el;
|
|
262
521
|
}
|
|
263
522
|
|
|
523
|
+
/**
|
|
524
|
+
* Apply matching attributes to an element using resolved dataScope.
|
|
525
|
+
*/
|
|
526
|
+
_applyNodeAttrs(el, node, dataScope) {
|
|
527
|
+
const attrMap = this._getComponentAttrMap();
|
|
528
|
+
const id = normalizeId(node.semanticId || node.id || '');
|
|
529
|
+
const tag = (node.tag || 'div').toLowerCase();
|
|
530
|
+
|
|
531
|
+
// Priority: tag fallback -> semanticId (.id or id)
|
|
532
|
+
const tagAttrs = attrMap[tag] || {};
|
|
533
|
+
const idDotAttrs = id ? attrMap['.' + id] || {} : {};
|
|
534
|
+
const idAttrs = id ? attrMap[id] || {} : {};
|
|
535
|
+
|
|
536
|
+
const merged = { ...tagAttrs, ...idDotAttrs, ...idAttrs };
|
|
537
|
+
const resolved = resolveAll(merged, dataScope);
|
|
538
|
+
|
|
539
|
+
this._applyPropsToElement(el, id, resolved);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Directly apply a resolved properties dictionary onto an element.
|
|
544
|
+
*/
|
|
545
|
+
_applyPropsToElement(el, id, props) {
|
|
546
|
+
if (!props || typeof props !== 'object') return;
|
|
547
|
+
|
|
548
|
+
if (props.text !== undefined) {
|
|
549
|
+
el.textContent = String(props.text);
|
|
550
|
+
}
|
|
551
|
+
if (props.html !== undefined) {
|
|
552
|
+
el.innerHTML = String(props.html);
|
|
553
|
+
}
|
|
554
|
+
if (props.class !== undefined) {
|
|
555
|
+
const extra = String(props.class).trim();
|
|
556
|
+
el.className = id ? (extra ? `${id} ${extra}` : id) : extra;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
for (const [key, value] of Object.entries(props)) {
|
|
560
|
+
if (ATTR_SKIP.has(key)) continue;
|
|
561
|
+
if (key === 'style' && value && typeof value === 'object') {
|
|
562
|
+
Object.assign(el.style, value);
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
if (value == null) {
|
|
566
|
+
el.removeAttribute(key);
|
|
567
|
+
} else {
|
|
568
|
+
el.setAttribute(key, String(value));
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
264
573
|
// -----------------------------------------------------------------------
|
|
265
574
|
// State event binding
|
|
266
575
|
// -----------------------------------------------------------------------
|
|
@@ -274,7 +583,6 @@ export class WdlDom {
|
|
|
274
583
|
|
|
275
584
|
const handler = (event) => this._onStateEvent(event);
|
|
276
585
|
|
|
277
|
-
// All events documented by @ruledwdl/state
|
|
278
586
|
const types = [
|
|
279
587
|
'layers:change',
|
|
280
588
|
'attr:change',
|
|
@@ -285,7 +593,6 @@ export class WdlDom {
|
|
|
285
593
|
|
|
286
594
|
for (const type of types) {
|
|
287
595
|
const off = c.on(type, handler);
|
|
288
|
-
// state.on may return unsubscribe fn or nothing
|
|
289
596
|
if (typeof off === 'function') this._unsubs.push(off);
|
|
290
597
|
else this._unsubs.push(() => c.off?.(type, handler));
|
|
291
598
|
}
|
|
@@ -321,68 +628,54 @@ export class WdlDom {
|
|
|
321
628
|
|
|
322
629
|
// -----------------------------------------------------------------------
|
|
323
630
|
// layers:change
|
|
324
|
-
// actions: set | append | before | after | wrap | remove | update
|
|
631
|
+
// actions: set | append | prepend | before | after | wrap | unwrap | move | remove | update
|
|
325
632
|
// -----------------------------------------------------------------------
|
|
326
633
|
|
|
327
634
|
_handleLayers(event) {
|
|
328
635
|
const { action, targetId, payload } = event;
|
|
329
636
|
const id = normalizeId(targetId);
|
|
637
|
+
const compData = this._getComponentData();
|
|
330
638
|
|
|
331
639
|
switch (action) {
|
|
332
640
|
case 'set': {
|
|
333
|
-
// Full layers replacement → remount
|
|
334
641
|
this._mount();
|
|
335
642
|
break;
|
|
336
643
|
}
|
|
337
644
|
case 'append': {
|
|
338
|
-
|
|
339
|
-
const parentEl = this.liveMap.get(id);
|
|
645
|
+
const parentEl = this.getNode(id);
|
|
340
646
|
if (!parentEl) {
|
|
341
647
|
this._log('warn', `append: parent "${id}" not in liveMap — remounting`);
|
|
342
648
|
this._mount();
|
|
343
649
|
return;
|
|
344
650
|
}
|
|
345
651
|
const token = parseLayerToken(payload);
|
|
346
|
-
|
|
347
|
-
tag: token.tag,
|
|
348
|
-
semanticId: token.semanticId,
|
|
349
|
-
children: [],
|
|
350
|
-
});
|
|
351
|
-
parentEl.appendChild(el);
|
|
652
|
+
this._mountLayerNode(parentEl, token, compData);
|
|
352
653
|
this._log('op', `append <${token.tag}.${token.semanticId}> → #${id}`);
|
|
353
654
|
break;
|
|
354
655
|
}
|
|
355
656
|
case 'prepend': {
|
|
356
|
-
const parentEl = this.
|
|
657
|
+
const parentEl = this.getNode(id);
|
|
357
658
|
if (!parentEl) {
|
|
358
659
|
this._log('warn', `prepend: parent "${id}" not in liveMap — remounting`);
|
|
359
660
|
this._mount();
|
|
360
661
|
return;
|
|
361
662
|
}
|
|
362
663
|
const token = parseLayerToken(payload);
|
|
363
|
-
const el = this.
|
|
364
|
-
tag: token.tag,
|
|
365
|
-
semanticId: token.semanticId,
|
|
366
|
-
children: [],
|
|
367
|
-
});
|
|
664
|
+
const el = this._createSingleElement(token, compData);
|
|
368
665
|
parentEl.insertBefore(el, parentEl.firstChild);
|
|
369
666
|
this._log('op', `prepend <${token.tag}.${token.semanticId}> → #${id}`);
|
|
370
667
|
break;
|
|
371
668
|
}
|
|
372
669
|
case 'before':
|
|
373
670
|
case 'after': {
|
|
374
|
-
const targetEl = this.
|
|
671
|
+
const targetEl = this.getNode(id);
|
|
375
672
|
if (!targetEl || !targetEl.parentNode) {
|
|
376
673
|
this._log('warn', `${action}: target "${id}" missing — remounting`);
|
|
377
674
|
this._mount();
|
|
378
675
|
return;
|
|
379
676
|
}
|
|
380
677
|
const token = parseLayerToken(payload);
|
|
381
|
-
const el = this.
|
|
382
|
-
tag: token.tag,
|
|
383
|
-
semanticId: token.semanticId,
|
|
384
|
-
children: [],
|
|
385
|
-
});
|
|
678
|
+
const el = this._createSingleElement(token, compData);
|
|
386
679
|
if (action === 'before') {
|
|
387
680
|
targetEl.parentNode.insertBefore(el, targetEl);
|
|
388
681
|
} else {
|
|
@@ -392,26 +685,21 @@ export class WdlDom {
|
|
|
392
685
|
break;
|
|
393
686
|
}
|
|
394
687
|
case 'wrap': {
|
|
395
|
-
|
|
396
|
-
const targetEl = this.liveMap.get(id);
|
|
688
|
+
const targetEl = this.getNode(id);
|
|
397
689
|
if (!targetEl || !targetEl.parentNode) {
|
|
398
690
|
this._log('warn', `wrap: target "${id}" missing — remounting`);
|
|
399
691
|
this._mount();
|
|
400
692
|
return;
|
|
401
693
|
}
|
|
402
694
|
const token = parseLayerToken(payload);
|
|
403
|
-
const wrapper = this.
|
|
404
|
-
tag: token.tag,
|
|
405
|
-
semanticId: token.semanticId,
|
|
406
|
-
children: [],
|
|
407
|
-
});
|
|
695
|
+
const wrapper = this._createSingleElement(token, compData);
|
|
408
696
|
targetEl.parentNode.insertBefore(wrapper, targetEl);
|
|
409
697
|
wrapper.appendChild(targetEl);
|
|
410
698
|
this._log('op', `wrap #${id} with <${token.tag}.${token.semanticId}>`);
|
|
411
699
|
break;
|
|
412
700
|
}
|
|
413
701
|
case 'unwrap': {
|
|
414
|
-
const wrapperEl = this.
|
|
702
|
+
const wrapperEl = this.getNode(id);
|
|
415
703
|
if (!wrapperEl || !wrapperEl.parentNode) {
|
|
416
704
|
this._log('warn', `unwrap: target "${id}" missing — remounting`);
|
|
417
705
|
this._mount();
|
|
@@ -421,16 +709,16 @@ export class WdlDom {
|
|
|
421
709
|
while (wrapperEl.firstChild) {
|
|
422
710
|
parent.insertBefore(wrapperEl.firstChild, wrapperEl);
|
|
423
711
|
}
|
|
712
|
+
this._unregisterElementHierarchy(wrapperEl);
|
|
424
713
|
wrapperEl.remove();
|
|
425
|
-
this.liveMap.delete(id);
|
|
426
714
|
this._log('op', `unwrap #${id}`);
|
|
427
715
|
break;
|
|
428
716
|
}
|
|
429
717
|
case 'move': {
|
|
430
|
-
const sourceEl = this.
|
|
718
|
+
const sourceEl = this.getNode(id);
|
|
431
719
|
const { targetSemanticId, position } = payload || {};
|
|
432
720
|
const targetId = normalizeId(targetSemanticId);
|
|
433
|
-
const targetEl = this.
|
|
721
|
+
const targetEl = this.getNode(targetId);
|
|
434
722
|
|
|
435
723
|
if (!sourceEl || !targetEl) {
|
|
436
724
|
this._log('warn', `move: source "${id}" or target "${targetId}" missing — remounting`);
|
|
@@ -449,23 +737,22 @@ export class WdlDom {
|
|
|
449
737
|
break;
|
|
450
738
|
}
|
|
451
739
|
case 'remove': {
|
|
452
|
-
const
|
|
453
|
-
if (
|
|
740
|
+
const nodes = this.getLiveNodes(id);
|
|
741
|
+
if (nodes.length === 0) {
|
|
454
742
|
this._log('warn', `remove: #${id} not in liveMap`);
|
|
455
743
|
return;
|
|
456
744
|
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
745
|
+
for (const el of nodes) {
|
|
746
|
+
this._unregisterElementHierarchy(el);
|
|
747
|
+
el.remove();
|
|
460
748
|
}
|
|
461
|
-
|
|
462
|
-
this.
|
|
749
|
+
// Also cleanup loop bindings associated with this semantic ID
|
|
750
|
+
this._loopBindings = this._loopBindings.filter((l) => l.node.semanticId !== id);
|
|
463
751
|
this._log('op', `remove #${id}`);
|
|
464
752
|
break;
|
|
465
753
|
}
|
|
466
754
|
case 'update': {
|
|
467
|
-
|
|
468
|
-
const el = this.liveMap.get(id);
|
|
755
|
+
const el = this.getNode(id);
|
|
469
756
|
if (!el) {
|
|
470
757
|
this._log('warn', `update: #${id} missing — remounting`);
|
|
471
758
|
this._mount();
|
|
@@ -473,28 +760,25 @@ export class WdlDom {
|
|
|
473
760
|
}
|
|
474
761
|
const patch = payload || {};
|
|
475
762
|
if (patch.tag && patch.tag.toLowerCase() !== el.tagName.toLowerCase()) {
|
|
476
|
-
// Tag change requires recreate
|
|
477
763
|
const next = document.createElement(String(patch.tag).toLowerCase());
|
|
478
|
-
// copy attributes & children
|
|
479
764
|
for (const attr of el.attributes) next.setAttribute(attr.name, attr.value);
|
|
480
765
|
while (el.firstChild) next.appendChild(el.firstChild);
|
|
481
766
|
el.parentNode?.replaceChild(next, el);
|
|
482
|
-
this.
|
|
483
|
-
|
|
484
|
-
|
|
767
|
+
this._unregisterNode(id, el);
|
|
768
|
+
const finalId = patch.semanticId ? normalizeId(patch.semanticId) : id;
|
|
769
|
+
if (patch.semanticId && finalId !== id) {
|
|
485
770
|
next.classList.remove(id);
|
|
486
|
-
next.classList.add(
|
|
487
|
-
next.setAttribute('wdl-comp',
|
|
488
|
-
this.liveMap.delete(id);
|
|
489
|
-
this.liveMap.set(newId, next);
|
|
771
|
+
next.classList.add(finalId);
|
|
772
|
+
next.setAttribute('wdl-comp', finalId);
|
|
490
773
|
}
|
|
774
|
+
this._registerNode(finalId, next);
|
|
491
775
|
} else if (patch.semanticId && normalizeId(patch.semanticId) !== id) {
|
|
492
776
|
const newId = normalizeId(patch.semanticId);
|
|
493
777
|
el.classList.remove(id);
|
|
494
778
|
el.classList.add(newId);
|
|
495
779
|
el.setAttribute('wdl-comp', newId);
|
|
496
|
-
this.
|
|
497
|
-
this.
|
|
780
|
+
this._unregisterNode(id, el);
|
|
781
|
+
this._registerNode(newId, el);
|
|
498
782
|
}
|
|
499
783
|
this._log('op', `update #${id}`, patch);
|
|
500
784
|
break;
|
|
@@ -515,89 +799,114 @@ export class WdlDom {
|
|
|
515
799
|
const id = normalizeId(targetId);
|
|
516
800
|
|
|
517
801
|
if (action === 'set' || action === 'update') {
|
|
518
|
-
|
|
519
|
-
|
|
802
|
+
const nodes = this.getLiveNodes(id);
|
|
803
|
+
if (nodes.length === 0) {
|
|
804
|
+
this._log('warn', `attr: #${id} not in liveMap`);
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
const compData = this._getComponentData();
|
|
808
|
+
for (const el of nodes) {
|
|
809
|
+
const idx = el.getAttribute('data-wdl-index');
|
|
810
|
+
let scope = compData;
|
|
811
|
+
if (idx !== null && idx !== undefined) {
|
|
812
|
+
const indexNum = parseInt(idx, 10);
|
|
813
|
+
const loop = this._findLoopForElement(el);
|
|
814
|
+
if (loop) {
|
|
815
|
+
const items = resolvePath(compData, loop.repeator);
|
|
816
|
+
if (Array.isArray(items) && items[indexNum] !== undefined) {
|
|
817
|
+
scope = this._createItemScope(items[indexNum], indexNum, compData);
|
|
818
|
+
} else {
|
|
819
|
+
scope = { ...compData, _index: indexNum };
|
|
820
|
+
}
|
|
821
|
+
} else {
|
|
822
|
+
scope = { ...compData, _index: indexNum };
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
const resolved = resolveAll(payload || {}, scope);
|
|
826
|
+
this._applyPropsToElement(el, id, resolved);
|
|
827
|
+
}
|
|
520
828
|
this._log('op', `attr.${action} #${id}`, payload);
|
|
521
829
|
return;
|
|
522
830
|
}
|
|
523
831
|
|
|
524
832
|
if (action === 'remove') {
|
|
525
|
-
const
|
|
526
|
-
if (
|
|
527
|
-
const attrKey = payload;
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
this._log('op', `attr.remove html from #${id}`);
|
|
542
|
-
} else {
|
|
543
|
-
el.removeAttribute(attrKey);
|
|
544
|
-
this._log('op', `attr.remove ${attrKey} from #${id}`);
|
|
833
|
+
const nodes = this.getLiveNodes(id);
|
|
834
|
+
if (nodes.length === 0) return;
|
|
835
|
+
const attrKey = payload;
|
|
836
|
+
for (const el of nodes) {
|
|
837
|
+
if (attrKey == null || attrKey === '') {
|
|
838
|
+
el.textContent = '';
|
|
839
|
+
el.className = id;
|
|
840
|
+
} else if (attrKey === 'text') {
|
|
841
|
+
el.textContent = '';
|
|
842
|
+
} else if (attrKey === 'class') {
|
|
843
|
+
el.className = id;
|
|
844
|
+
} else if (attrKey === 'html') {
|
|
845
|
+
el.innerHTML = '';
|
|
846
|
+
} else {
|
|
847
|
+
el.removeAttribute(attrKey);
|
|
848
|
+
}
|
|
545
849
|
}
|
|
850
|
+
this._log('op', `attr.remove ${attrKey || 'all'} from #${id}`);
|
|
546
851
|
}
|
|
547
852
|
}
|
|
548
853
|
|
|
549
|
-
/**
|
|
550
|
-
* Apply attribute object onto live element.
|
|
551
|
-
* Supports: text, class, html, data-*, style object, arbitrary attrs.
|
|
552
|
-
*/
|
|
553
854
|
_applyAttrs(id, props) {
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
if (!el) {
|
|
855
|
+
const nodes = this.getLiveNodes(id);
|
|
856
|
+
if (nodes.length === 0) {
|
|
557
857
|
this._log('warn', `attr: #${id} not in liveMap`);
|
|
558
858
|
return;
|
|
559
859
|
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
Object.assign(el.style, value);
|
|
576
|
-
continue;
|
|
577
|
-
}
|
|
578
|
-
if (value == null) {
|
|
579
|
-
el.removeAttribute(key);
|
|
580
|
-
} else {
|
|
581
|
-
el.setAttribute(key, String(value));
|
|
860
|
+
const compData = this._getComponentData();
|
|
861
|
+
for (const el of nodes) {
|
|
862
|
+
const idx = el.getAttribute('data-wdl-index');
|
|
863
|
+
let scope = compData;
|
|
864
|
+
if (idx !== null && idx !== undefined) {
|
|
865
|
+
const indexNum = parseInt(idx, 10);
|
|
866
|
+
const loop = this._findLoopForElement(el);
|
|
867
|
+
if (loop) {
|
|
868
|
+
const items = resolvePath(compData, loop.repeator);
|
|
869
|
+
if (Array.isArray(items) && items[indexNum] !== undefined) {
|
|
870
|
+
scope = this._createItemScope(items[indexNum], indexNum, compData);
|
|
871
|
+
} else {
|
|
872
|
+
scope = { ...compData, _index: indexNum };
|
|
873
|
+
}
|
|
874
|
+
}
|
|
582
875
|
}
|
|
876
|
+
const resolved = resolveAll(props || {}, scope);
|
|
877
|
+
this._applyPropsToElement(el, id, resolved);
|
|
583
878
|
}
|
|
584
879
|
}
|
|
585
880
|
|
|
881
|
+
_findLoopForElement(el) {
|
|
882
|
+
return this._loopBindings.find((l) => l.elements.includes(el)) || null;
|
|
883
|
+
}
|
|
884
|
+
|
|
586
885
|
// -----------------------------------------------------------------------
|
|
587
886
|
// data:change
|
|
588
|
-
// actions: set | remove
|
|
589
|
-
//
|
|
887
|
+
// actions: set | update | remove
|
|
888
|
+
// Surgically reconciles repeated loop items & per-item template bindings
|
|
590
889
|
// -----------------------------------------------------------------------
|
|
591
890
|
|
|
592
891
|
_handleData(event) {
|
|
593
892
|
const { action, targetId, payload } = event;
|
|
594
|
-
// targetId is the path (e.g. "user.name"), payload is the value
|
|
595
893
|
const path = targetId || '';
|
|
894
|
+
const compData = this._getComponentData();
|
|
895
|
+
|
|
896
|
+
// 1. Surgically reconcile all matching loop bindings
|
|
897
|
+
this._reconcileLoops(path, compData);
|
|
898
|
+
|
|
899
|
+
// 2. Reconcile non-loop elements bound to this path or full data
|
|
900
|
+
this._reconcileStaticDataBindings(path, compData);
|
|
901
|
+
|
|
902
|
+
// 3. Optional onDataBind callback hook for external consumers
|
|
596
903
|
if (typeof this.onDataBind === 'function') {
|
|
597
|
-
// Let consumer decide how data maps to DOM
|
|
598
|
-
// We pass the root container + path + value
|
|
599
904
|
try {
|
|
600
|
-
this.onDataBind(
|
|
905
|
+
this.onDataBind(
|
|
906
|
+
this.container,
|
|
907
|
+
path,
|
|
908
|
+
action === 'remove' ? undefined : payload
|
|
909
|
+
);
|
|
601
910
|
} catch (e) {
|
|
602
911
|
this._log('warn', 'onDataBind error', e);
|
|
603
912
|
}
|
|
@@ -605,6 +914,113 @@ export class WdlDom {
|
|
|
605
914
|
this._log('op', `data.${action} ${path}`, action === 'remove' ? undefined : payload);
|
|
606
915
|
}
|
|
607
916
|
|
|
917
|
+
/**
|
|
918
|
+
* Reconcile loop children surgically on data change.
|
|
919
|
+
*/
|
|
920
|
+
_reconcileLoops(path, compData) {
|
|
921
|
+
for (const loop of this._loopBindings) {
|
|
922
|
+
const matches =
|
|
923
|
+
!path ||
|
|
924
|
+
loop.repeator === path ||
|
|
925
|
+
path.startsWith(loop.repeator + '.') ||
|
|
926
|
+
loop.repeator.startsWith(path + '.');
|
|
927
|
+
|
|
928
|
+
if (!matches) continue;
|
|
929
|
+
|
|
930
|
+
const items = this._resolveLoopItems(loop.repeator, compData);
|
|
931
|
+
const nextCount = items.length;
|
|
932
|
+
const prevCount = loop.elements.length;
|
|
933
|
+
const minCount = Math.min(prevCount, nextCount);
|
|
934
|
+
|
|
935
|
+
// 1. Update retained rows in place
|
|
936
|
+
for (let i = 0; i < minCount; i++) {
|
|
937
|
+
const el = loop.elements[i];
|
|
938
|
+
const itemScope = this._createItemScope(items[i], i, compData);
|
|
939
|
+
el.setAttribute('data-wdl-index', String(i));
|
|
940
|
+
if (el.dataset) el.dataset.wdlIndex = String(i);
|
|
941
|
+
this._updateElementSubtree(el, loop.node, itemScope, i);
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// 2. Insert newly added rows
|
|
945
|
+
if (nextCount > prevCount) {
|
|
946
|
+
for (let i = prevCount; i < nextCount; i++) {
|
|
947
|
+
const itemScope = this._createItemScope(items[i], i, compData);
|
|
948
|
+
const newEl = this._createSingleElement(loop.node, itemScope, i);
|
|
949
|
+
if (loop.anchor && loop.anchor.parentNode === loop.parentEl) {
|
|
950
|
+
loop.parentEl.insertBefore(newEl, loop.anchor);
|
|
951
|
+
} else if (loop.elements.length > 0) {
|
|
952
|
+
const lastEl = loop.elements[loop.elements.length - 1];
|
|
953
|
+
loop.parentEl.insertBefore(newEl, lastEl.nextSibling);
|
|
954
|
+
} else {
|
|
955
|
+
loop.parentEl.appendChild(newEl);
|
|
956
|
+
}
|
|
957
|
+
loop.elements.push(newEl);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// 3. Remove deleted rows
|
|
962
|
+
if (nextCount < prevCount) {
|
|
963
|
+
for (let i = prevCount - 1; i >= nextCount; i--) {
|
|
964
|
+
const delEl = loop.elements[i];
|
|
965
|
+
this._unregisterElementHierarchy(delEl);
|
|
966
|
+
delEl.remove();
|
|
967
|
+
loop.elements.pop();
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
/**
|
|
974
|
+
* Update an element and its non-loop children with new itemScope.
|
|
975
|
+
*/
|
|
976
|
+
_updateElementSubtree(el, node, itemScope, index) {
|
|
977
|
+
this._applyNodeAttrs(el, node, itemScope);
|
|
978
|
+
|
|
979
|
+
if (Array.isArray(node.children) && Array.isArray(el.children)) {
|
|
980
|
+
let childElIdx = 0;
|
|
981
|
+
for (const childNode of node.children) {
|
|
982
|
+
if (childNode.repeator) {
|
|
983
|
+
// Nested loop: find its binding and reconcile
|
|
984
|
+
const childLoop = this._loopBindings.find(
|
|
985
|
+
(l) => l.parentEl === el && l.node === childNode
|
|
986
|
+
);
|
|
987
|
+
if (childLoop) {
|
|
988
|
+
this._reconcileLoops(childLoop.repeator, itemScope);
|
|
989
|
+
}
|
|
990
|
+
} else if (el.children[childElIdx]) {
|
|
991
|
+
this._updateElementSubtree(
|
|
992
|
+
el.children[childElIdx],
|
|
993
|
+
childNode,
|
|
994
|
+
itemScope,
|
|
995
|
+
index
|
|
996
|
+
);
|
|
997
|
+
childElIdx++;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
/**
|
|
1004
|
+
* Update non-loop live elements that reference template data bindings.
|
|
1005
|
+
*/
|
|
1006
|
+
_reconcileStaticDataBindings(path, compData) {
|
|
1007
|
+
const attrMap = this._getComponentAttrMap();
|
|
1008
|
+
for (const [key, props] of Object.entries(attrMap)) {
|
|
1009
|
+
const id = normalizeId(key);
|
|
1010
|
+
const str = JSON.stringify(props);
|
|
1011
|
+
if (!str.includes('${') && !str.includes('{{')) continue;
|
|
1012
|
+
if (path && !str.includes(path)) continue;
|
|
1013
|
+
|
|
1014
|
+
const nodes = this.getLiveNodes(id);
|
|
1015
|
+
for (const el of nodes) {
|
|
1016
|
+
// Skip elements governed by loops as they are reconciled separately
|
|
1017
|
+
if (el.hasAttribute('data-wdl-index') || this._findLoopForElement(el)) continue;
|
|
1018
|
+
const resolved = resolveAll(props, compData);
|
|
1019
|
+
this._applyPropsToElement(el, id, resolved);
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
608
1024
|
// -----------------------------------------------------------------------
|
|
609
1025
|
// variant:change
|
|
610
1026
|
// action: set targetId = semanticId (or component id) payload = variantName
|
|
@@ -613,11 +1029,10 @@ export class WdlDom {
|
|
|
613
1029
|
_handleVariant(event) {
|
|
614
1030
|
const { targetId, payload } = event;
|
|
615
1031
|
const id = normalizeId(targetId) || this._rootSemanticId();
|
|
616
|
-
const el = this.
|
|
1032
|
+
const el = this.getNode(id);
|
|
617
1033
|
if (!el) {
|
|
618
|
-
// try root
|
|
619
1034
|
const rootId = this._rootSemanticId();
|
|
620
|
-
const rootEl = rootId ? this.
|
|
1035
|
+
const rootEl = rootId ? this.getNode(rootId) : null;
|
|
621
1036
|
if (rootEl) {
|
|
622
1037
|
if (payload) rootEl.dataset.variant = String(payload);
|
|
623
1038
|
else delete rootEl.dataset.variant;
|
|
@@ -631,11 +1046,7 @@ export class WdlDom {
|
|
|
631
1046
|
}
|
|
632
1047
|
|
|
633
1048
|
_applyRootVariant() {
|
|
634
|
-
|
|
635
|
-
const attr =
|
|
636
|
-
this.component.attr?.list?.() ??
|
|
637
|
-
this.component.getSnapshot?.()?.attr ??
|
|
638
|
-
{};
|
|
1049
|
+
const attr = this._getComponentAttrMap();
|
|
639
1050
|
const rootId = this._rootSemanticId();
|
|
640
1051
|
if (!rootId) return;
|
|
641
1052
|
const rootKey = '.' + rootId;
|
|
@@ -644,12 +1055,12 @@ export class WdlDom {
|
|
|
644
1055
|
attr['.']?.['data-variant'] ??
|
|
645
1056
|
null;
|
|
646
1057
|
if (variant && this.liveMap.has(rootId)) {
|
|
647
|
-
this.
|
|
1058
|
+
const el = this.getNode(rootId);
|
|
1059
|
+
if (el && el.dataset) el.dataset.variant = String(variant);
|
|
648
1060
|
}
|
|
649
1061
|
}
|
|
650
1062
|
|
|
651
1063
|
_rootSemanticId() {
|
|
652
|
-
// First entry in liveMap that is a direct child of container, or first key
|
|
653
1064
|
for (const [id, el] of this.liveMap) {
|
|
654
1065
|
if (el.parentNode === this.container) return id;
|
|
655
1066
|
}
|
|
@@ -751,3 +1162,4 @@ export function createWdlDom(options) {
|
|
|
751
1162
|
}
|
|
752
1163
|
|
|
753
1164
|
export default createWdlDom;
|
|
1165
|
+
|