diff --git a/dist/state.js b/dist/state.js index e4eb0e5..11175fd 100644 --- a/dist/state.js +++ b/dist/state.js @@ -213,7 +213,10 @@ function _updateBinding(binding) { const node = binding.node; if (!node.isConnected && node.tagName !== "TEMPLATE") return; setActiveBinding(binding); + if (window.__perfTrace) window.__perfTrace.evalCount++; + const evalStart = window.__perfTrace ? performance.now() : 0; let result = binding.exp ? binding.tpl ? _returnCode(binding.tpl, { thisNode: node }, node._thisObj || node, node._ref || null) : null : binding.tpl; + if (window.__perfTrace) window.__perfTrace.evalTotal += performance.now() - evalStart; setActiveBinding(null); if (binding.prop) { const prop = binding.prop; @@ -241,10 +244,13 @@ function _updateBinding(binding) { if (result) { if (!node._renderedNodes || node._renderedNodes.length === 0) { node._children.forEach((child) => { + child._stManaged = true; node.parentNode.insertBefore(child, node); child._ref = { ...node._ref }; }); node._renderedNodes = [node._children]; + } else { + node._renderedNodes[0].forEach((child) => _scanTree(child, { thisObj: node._thisObj, extendVars: child._ref })); } } else { _clearRenderedNodes(node); @@ -254,6 +260,7 @@ function _updateBinding(binding) { if (result && typeof result === "object") { const asName = node.getAttribute("as") || "item"; const indexName = node.getAttribute("index") || "index"; + const keyName = node.getAttribute("key"); let keys, getVal; if (result instanceof Map) { keys = Array.from(result.keys()); @@ -267,35 +274,50 @@ function _updateBinding(binding) { keys = Object.keys(result); getVal = (k) => result[k]; } + if (!node._keyedNodes) node._keyedNodes = /* @__PURE__ */ new Map(); + const newKeyedNodes = /* @__PURE__ */ new Map(); + const currentRenderedNodes = []; keys.forEach((k, i) => { const item = getVal(k); - if (node._renderedNodes && i < node._renderedNodes.length) { - node._renderedNodes[i].forEach((child) => { + const rawKey = keyName ? item && typeof item === "object" ? item[keyName] : item : k; + const keyVal = rawKey === void 0 || rawKey === null || newKeyedNodes.has(rawKey) ? `st_key_fallback_${i}_${Math.random()}` : rawKey; + let existingNodes = node._keyedNodes.get(keyVal); + if (existingNodes) { + node._keyedNodes.delete(keyVal); + existingNodes.forEach((child) => { child._ref[indexName] = k; - child._ref[asName] = item; - _scanTree(child, { thisObj: node._thisObj, extendVars: child._ref }); + if (child._ref[asName] !== item) { + child._ref[asName] = item; + _scanTree(child, { thisObj: node._thisObj, extendVars: child._ref }); + } else if (node.parentNode.lastChild !== child) { + node.parentNode.insertBefore(child, node); + } }); } else { - const newNodes = []; - if (!node._renderedNodes) node._renderedNodes = []; + existingNodes = []; node._children.forEach((child) => { const cloned = child.cloneNode(true); + cloned._stManaged = true; cloned._ref = { ...node._ref, [indexName]: k, [asName]: item }; cloned._thisObj = node._thisObj; node.parentNode.insertBefore(cloned, node); - newNodes.push(cloned); + existingNodes.push(cloned); }); - node._renderedNodes.push(newNodes); } + newKeyedNodes.set(keyVal, existingNodes); + currentRenderedNodes.push(existingNodes); + existingNodes.forEach((child) => node.parentNode.insertBefore(child, node)); }); - while (node._renderedNodes && node._renderedNodes.length > keys.length) { - node._renderedNodes.pop().forEach((child) => { - _clearRenderedNodes(child); - child.remove(); - }); - } + node._keyedNodes.forEach((nodes) => nodes.forEach((child) => { + _clearRenderedNodes(child); + child.remove(); + })); + node._keyedNodes = newKeyedNodes; + node._renderedNodes = currentRenderedNodes; } else { _clearRenderedNodes(node); + if (node._keyedNodes) node._keyedNodes.forEach((nodes) => nodes.forEach((child) => child.remove())); + node._keyedNodes = /* @__PURE__ */ new Map(); node._renderedNodes = []; } } else if (attr === "bind") { @@ -492,7 +514,9 @@ const _scanTree = (node, scanObj = {}) => { _parseNode(node, scanObj); const nodes = [...node.childNodes || []]; const nextScanObj = { thisObj: scanObj.thisObj, extendVars: { ...node._ref } }; - nodes.forEach((child) => _scanTree(child, nextScanObj)); + nodes.forEach((child) => { + if (!child._stManaged) _scanTree(child, nextScanObj); + }); }; const _unbindTree = (node) => { if (node.nodeType !== 1) return; diff --git a/dist/state.min.js b/dist/state.min.js index 9f9a557..f240414 100644 --- a/dist/state.min.js +++ b/dist/state.min.js @@ -1 +1 @@ -var e;let t=null,n=null;const r=e=>t=e,s=e=>n=e,a=new Set;function o(e={},r=null,s=null){const o={},i=new Map,d=new Map,l=(e,t)=>(d.has(e)||d.set(e,new Set),t?d.get(e).add(t):d.get(e).clear(),()=>d.get(e).delete(t)),c=(e,t)=>{d.has(e)&&d.get(e).delete(t)},h=r||(e=>o[e]),u=s||((e,t)=>o[e]=t);return Object.assign(o,e),new Proxy(o,{get:(e,n)=>"__watch"===n?l:"__unwatch"===n?c:"__isProxy"===n||(t&&(i.has(n)||i.set(n,new Set),i.get(n).add(t),t.node._states||(t.node._states=new Set),t.node._states.add(i)),h(n)),set(e,t,r){if(h(t)!==r&&u(t,r),d.has(t)&&d.get(t).forEach(n=>{const s=n(r);void 0!==s&&(r=s,e[t]=r)}),d.has(null)&&d.get(null).forEach(e=>e(r)),i.has(t)){const e=i.get(t);for(const t of e)t.node.isConnected?n!==t.node&&a.forEach(e=>e(t)):e.delete(t)}return!0}})}const i=(e,t)=>t?e.querySelector(t):document.querySelector(e),d=(e,t)=>t?e.querySelectorAll(t):document.querySelectorAll(e),l=new Map,c=[],h={getTemplate:e=>document.querySelector(`template[component="${e.toUpperCase()}"]`),register:(e,t,n=null,...r)=>{l.set(e.toUpperCase(),t),"loading"!==document.readyState?h._addTemplate(e,n,r):c.push([e,n,r])},exists:e=>l.has(e.toUpperCase()),getSetupFunction:e=>l.get(e.toUpperCase()),_addTemplate:(e,t,n)=>{if(t){const n=document.createElement("TEMPLATE");n.setAttribute("component",e.toUpperCase()),n.content.appendChild(t),document.body.appendChild(n)}n&&n.forEach(e=>document.body.appendChild(e))},_initPending:()=>{c.forEach(([e,t,n])=>h._addTemplate(e,t,n)),c.length=0}};function u(e,t,n,r={}){e.attributes&&Array.from(e.attributes).forEach(e=>{"class"!==e.name&&("style"===e.name?t.hasAttribute("style")?t.setAttribute("style",`${e.value}; ${t.getAttribute("style")}`):t.setAttribute("style",e.value):t.hasAttribute(e.name)||t.setAttribute(e.name,e.value))}),t.classList.add(...e.classList);const s="TEMPLATE"===e.tagName?e.content:e,a="TEMPLATE"===t.tagName?t.content:t;Array.from(s.childNodes).forEach(e=>a.appendChild(e)),e.tagName&&h.exists(e.tagName)&&f(e.tagName,t,n,r)}function f(e,t,n,r={}){if(r[e])return;r[e]=!0,n.thisObj&&Array.from(t.attributes).forEach(e=>{(e.name.startsWith("$")||e.name.startsWith("st-"))&&e.value.includes("this.")&&(e.value=e.value.replace(/\bthis\./g,"this.parent."))});const s=h.getSetupFunction(e),a={};Array.from(t.childNodes).forEach(e=>{e.nodeType===Node.ELEMENT_NODE&&e.hasAttribute("slot")&&(a[e.getAttribute("slot")]=e,e.removeAttribute("slot"))}),t.innerHTML="",t.state=o(t.state||{});const i=h.getTemplate(e);if(i){const e=i.content.cloneNode(!0);if(e.childNodes.length){const s=Array.from(e.childNodes).find(e=>e.nodeType===Node.ELEMENT_NODE);s&&u(s,t,n,r),d(t,"[slot-id]").forEach(e=>{const t=e.getAttribute("slot-id");a[t]&&(e.removeAttribute("slot-id"),e.innerHTML="",u(a[t],e,n,r))})}}s&&s(t)}let b=!1;function m(e){b=e}const p=new Map;function g(e,t,n,r){const s={...r||{},...t||{}},a=Object.keys(s),o=Object.values(s),i=e+a.join(",");try{let t=p.get(i);return t||(t=new Function("Hash","LocalStorage","State",...a,e),p.set(i,t)),t.apply(n,[globalThis.Hash,globalThis.LocalStorage,globalThis.State,...o])}catch(s){return b||console.error(s,r,[e,r,t,n]),null}}function _(e,t,n,r){return e.includes("${")?g("return `"+e+"`",t,n,r):g("return "+e,t,n,r)}let E=(e,t)=>e&&"string"==typeof e?e.replace(/\{(.+?)\}/g,(e,n)=>t.hasOwnProperty(n)?t[n]:e):e;const v=e=>E=e,A=e=>e&&"string"==typeof e&&e.includes("{#")?e.replace(/\{#(.+?)#\}/g,(e,t)=>{const n=t.split("||").map(e=>e.trim()),r={};if(n.length>1){const e=n[0].match(/\{(.+?)\}/g);e&&e.forEach((e,t)=>r[e.substring(1,e.length-1)]=n[t+1]||"")}return E(n[0],r)}):e;if("undefined"!=typeof document)try{document.createElement("div").setAttribute("$t","1")}catch(e){const t=Element.prototype.setAttribute;Element.prototype.setAttribute=function(e,n){return e.startsWith("$")?t.call(this,"st-"+e.substring(1),n):t.call(this,e,n)}}var y;function N(e){e._renderedNodes&&e._renderedNodes.forEach(e=>e.forEach(e=>{e.remove(),e._renderedNodes&&N(e)}))}function O(e){const t=e.node;if(!t.isConnected&&"TEMPLATE"!==t.tagName)return;r(e);let n=e.exp?e.tpl?_(e.tpl,{thisNode:t},t._thisObj||t,t._ref||null):null:e.tpl;if(r(null),e.prop){const r=e.prop;let s=t;for(let e=0;e{t.parentNode.insertBefore(e,t),e._ref={...t._ref}}),t._renderedNodes=[t._children]):(N(t),t._renderedNodes=[]);else if("each"===r)if(n&&"object"==typeof n){const e=t.getAttribute("as")||"item",r=t.getAttribute("index")||"index";let s,a;if(n instanceof Map)s=Array.from(n.keys()),a=e=>n.get(e);else if("function"==typeof n[Symbol.iterator]){const e=Array.isArray(n)?n:Array.from(n);s=new Array(e.length);for(let t=0;te[t]}else s=Object.keys(n),a=e=>n[e];for(s.forEach((n,s)=>{const o=a(n);if(t._renderedNodes&&s{s._ref[r]=n,s._ref[e]=o,j(s,{thisObj:t._thisObj,extendVars:s._ref})});else{const s=[];t._renderedNodes||(t._renderedNodes=[]),t._children.forEach(a=>{const i=a.cloneNode(!0);i._ref={...t._ref,[r]:n,[e]:o},i._thisObj=t._thisObj,t.parentNode.insertBefore(i,t),s.push(i)}),t._renderedNodes.push(s)}});t._renderedNodes&&t._renderedNodes.length>s.length;)t._renderedNodes.pop().forEach(e=>{N(e),e.remove()})}else N(t),t._renderedNodes=[];else if("bind"===r){if(["INPUT","SELECT","TEXTAREA"].includes(t.tagName)&&!t.hasAttribute("autocomplete")&&t.setAttribute("autocomplete","off"),"checkbox"===t.type){"on"===t.value||n||(g(`${e.tpl} = []`,{thisNode:t},t._thisObj||t,t._ref||{}),n=[]),t._checkboxMultiMode=n instanceof Array;const r=n instanceof Array?n.includes(t.value):!!n;t.checked!==r&&(t.checked=r)}else"radio"===t.type?t.checked!==(t.value===String(n??""))&&(t.checked=t.value===String(n??"")):"value"in t&&"file"!==t.type?setTimeout(()=>{t.value!==String(n??"")&&(t.value=n)}):t.isContentEditable&&t.innerHTML!==String(n??"")&&(t.innerHTML=n);t.dispatchEvent(new CustomEvent("bind",{bubbles:!1,detail:n}))}else["checked","disabled","readonly"].includes(r)&&(n=!!n),"boolean"==typeof n?n?t.setAttribute(r,""):t.removeAttribute(r):void 0!==n&&("string"!=typeof n&&(n=JSON.stringify(n)),"text"===r?t.textContent=n??"":"html"===r?t.innerHTML=n??"":"IMG"===t.tagName&&"src"===r&&n.includes(".svg")?t.setAttribute("_src",n??""):t.setAttribute(r,n??""))}}y=e=>O(e),a.add(y);const T=e=>{e.node._bindings||(e.node._bindings=[]),e.node._bindings.push({attr:e.attr,prop:e.prop,tpl:e.tpl,exp:e.exp}),O(e)},j=(e,t={})=>{if(3===e.nodeType){if(e._stTranslated)return;const t=A(e.textContent);return t!==e.textContent&&(e.textContent=t),void(e._stTranslated=!0)}if(1!==e.nodeType)return;if(e._stTranslated||(Array.from(e.attributes).forEach(e=>{if(!e.name.startsWith("$")&&!e.name.startsWith("st-")&&!e.name.startsWith(".")){const t=A(e.value);t!==e.value&&(e.value=t)}}),e._stTranslated=!0),"TEMPLATE"!==e.tagName&&(e.hasAttribute("$if")||e.hasAttribute("$each")||e.hasAttribute("st-if")||e.hasAttribute("st-each"))){const t=document.createElement("TEMPLATE");return Array.from(e.attributes).filter(t=>["$if","$each","st-if","st-each"].includes(t.name)||(e.hasAttribute("$each")||e.hasAttribute("st-each"))&&["as","index"].includes(t.name)).forEach(n=>{t.setAttribute(n.name,n.value),e.removeAttribute(n.name)}),e.parentNode.insertBefore(t,e),t.content.appendChild(e),void(t._ref=e._ref)}if("TEMPLATE"===e.tagName&&(e.hasAttribute("$if")||e.hasAttribute("st-if"))&&(e.hasAttribute("$each")||e.hasAttribute("st-each"))){const t=document.createElement("TEMPLATE"),n=Array.from(e.attributes).filter(e=>["$if","$each","st-if","st-each"].includes(e.name)),r=n[n.length-1];t.setAttribute(r.name,r.value),e.removeAttribute(r.name),"$each"!==r.name&&"st-each"!==r.name||Array.from(e.attributes).filter(e=>["as","index"].includes(e.name)).forEach(n=>{t.setAttribute(n.name,n.value),e.removeAttribute(n.name)}),Array.from(e.content.childNodes).forEach(e=>t.content.appendChild(e)),e.content.appendChild(t),t._ref=e._ref}if("IMG"===e.tagName&&(e.hasAttribute("src")||e.hasAttribute("_src")||e.hasAttribute("$src"))){const t=e;Promise.resolve().then(()=>{const e=t.getAttribute("_src")||t.getAttribute("src");e&&fetch(e,{cache:"force-cache"}).then(e=>e.text()).then(e=>{const n=(new DOMParser).parseFromString(e,"image/svg+xml").querySelector("svg");n&&(Array.from(t.attributes).forEach(e=>n.setAttribute(e.name,e.value)),t.replaceWith(n))})})}if(void 0!==e._thisObj)t.thisObj=e._thisObj||null;else{let n=e;for(;n&&void 0===n._thisObj;)n=n.parentNode;t.thisObj=n?n._thisObj:null}if(void 0===e._ref){let t=e;for(;t&&void 0===t._ref;)t=t.parentNode;e._ref=t?{...t._ref}:{}}t.extendVars&&Object.assign(e._ref,t.extendVars),((e,t)=>{let n=!1;if(e._bindings&&(e._states=new Set,e._bindings.forEach(t=>O({node:e,...t})),e._hasOnUpdate&&e.dispatchEvent(new Event("update",{bubbles:!1})),n=!0),h.exists(e.tagName)&&!e._componentInitialized&&(Array.from(e.attributes).forEach(n=>{var r;if(n.name.startsWith("$.")){const s=n.name.slice(2);let a=A(n.value);a.includes("this.")&&(a=a.replace(/\bthis\./g,"this.parent."));const o=_(a,{thisNode:e},{parent:t.thisObj||e},e._ref||{});let i=e;const d=s.split(".");for(let e=0;ee.removeAttribute("slot-id")),e._componentInitialized=!0,e._thisObj||(e._thisObj=e)),"TEMPLATE"===e.tagName&&(e._children=[...e.content.childNodes],e._renderedNodes||(e._renderedNodes=[])),n)return;let r=[];"TEMPLATE"===e.tagName?["$if","$each","st-if","st-each"].forEach(t=>e.hasAttribute(t)&&r.push(e.getAttributeNode(t))):r=Array.from(e.attributes).filter(e=>(e.name.startsWith("$")||e.name.startsWith("st-"))&&!["$if","$each","st-if","st-each"].includes(e.name)||e.name.includes(".")),e._thisObj&&t.thisObj&&(e._thisObj.parent=t.thisObj),e._thisObj||(e._thisObj=t.thisObj||null),e._ref||(e._ref=t.extendVars||{}),e._states=new Set,r.forEach(n=>{const r=n.name.startsWith("$")||n.name.startsWith("st-"),a=r?n.name.slice(n.name.startsWith("$")?1:3):n.name;let o=n.value;if(e.removeAttribute(n.name),a.startsWith("."))T({node:e,prop:a.split("."),tpl:o,exp:r});else if(a.startsWith("on")){const n=a.slice(2);"update"===n&&(e._hasOnUpdate=!0),"load"!==n||["BODY","IMG","IFRAME"].includes(e.tagName)||(e._hasOnLoad=!0),"unload"!==n||["BODY","IMG","IFRAME"].includes(e.tagName)||(e._hasOnUnload=!0),e.addEventListener(n,n=>g(o,{event:n,thisNode:e,...n.detail||{}},t.thisObj||e,e._ref||{}))}else"bind"===a?e.addEventListener("TEXTAREA"===e.tagName||e.isContentEditable||"text"===e.type||"password"===e.type?"input":"change",n=>{let r=e.isContentEditable?n.target.innerHTML:"checkbox"===e.type?n.target.checked:n.target.files||n.target.value||n.detail;s(e),m(!0),"checkbox"===e.type&&e._checkboxMultiMode?g(`!!checked ? (!${o}.includes(val) && ${o}.push(val)) : (index = ${o}.indexOf(val), index > -1 && ${o}.splice(index, 1))`,{val:e.value,checked:r,thisNode:e},t.thisObj||e,e._ref||{}):g(`${o} = val`,{val:r,thisNode:e},t.thisObj||e,e._ref||{}),m(!1),s(null)}):"text"!==a||o||(o=e.textContent,e.textContent=""),o&&(o=A(o),T({node:e,attr:a,tpl:o,exp:r}))}),(e._hasOnLoad||e._componentInitialized)&&Promise.resolve().then(()=>e.dispatchEvent(new Event("load",{bubbles:!1}))),e._hasOnUpdate&&e.dispatchEvent(new Event("update",{bubbles:!1})),e._thisObj&&(t.thisObj=e._thisObj)})(e,t);const n=[...e.childNodes||[]],r={thisObj:t.thisObj,extendVars:{...e._ref}};n.forEach(e=>j(e,r))},w=e=>{1===e.nodeType&&(e._hasOnUnload&&e.dispatchEvent(new Event("unload",{bubbles:!1})),e._states&&e._states.forEach(t=>{for(const[n,r]of t)for(const t of r)t.node===e&&r.delete(t)}),e.childNodes&&e.childNodes.forEach(e=>w(e)))},x=j,S={clone:window.structuredClone||(e=>JSON.parse(JSON.stringify(e))),base64:e=>btoa(String.fromCharCode(...(new TextEncoder).encode(e))),unbase64:e=>(new TextDecoder).decode(Uint8Array.from(atob(e),e=>e.charCodeAt(0))),urlbase64:e=>S.base64(e).replace(/[+/=]/g,e=>({"+":"-","/":"","=":""}[e])),unurlbase64:e=>S.unbase64(e.replace(/[-_.]/g,e=>({"-":"+",_:"/",".":"="}[e])).padEnd(4*Math.ceil(e.length/4),"=")),safeJson:e=>{try{return JSON.parse(e)}catch{return null}},updateDefaults:(e,t)=>{for(const n in t)void 0===e[n]&&(e[n]=t[n])},copyFunction:(e,t,...n)=>{n.forEach(n=>e[n]=t[n].bind(t))},getFunctionBody:e=>{const t=e.toString();return t.slice(t.indexOf("{")+1,t.lastIndexOf("}")).trim()},makeDom:e=>{e.includes(">\n")&&(e=e.replace(/>\s+<").trim());const t=document.createElement("div");return t.innerHTML=e,t.children[0]},newAvg:()=>{let e=0,t=0,n=0;return{add:r=>(e+=r,t++,n=e/t),get:()=>n,clear:()=>{e=0,t=0,n=0}}},newTimeCount:()=>{let e=0,t=0,n=0;return{start:()=>e=(new Date).getTime(),end:()=>{const r=(new Date).getTime(),s=r-e;return e=r,t+=s,n++,s},avg:()=>t/n}}};globalThis.Util=S;let M=new URLSearchParams((null==(e=window.location.hash)?void 0:e.substring(1))||"");const L=o({},e=>S.safeJson(M.get(e)),(e,t)=>{const n=M.get(e),r=void 0===t?void 0:JSON.stringify(t);n===r||null===n&&void 0===r||(void 0===t?M.delete(e):M.set(e,r),window.location.hash="#"+M.toString())});"undefined"!=typeof window&&window.addEventListener("hashchange",()=>{var e;const t=M;M=new URLSearchParams((null==(e=window.location.hash)?void 0:e.substring(1))||""),M.forEach((e,n)=>{t.get(n)!==e&&(L[n]=S.safeJson(e))}),t.forEach((e,t)=>{void 0===M.get(t)&&(L[t]=void 0)})});const $=o({},e=>S.safeJson(localStorage.getItem(e)),(e,t)=>{const n=localStorage.getItem(e),r=void 0===t?void 0:JSON.stringify(t);n===r||null===n&&void 0===r||(void 0===t?localStorage.removeItem(e):localStorage.setItem(e,r))});if(globalThis.Hash=L,globalThis.LocalStorage=$,"undefined"!=typeof document){const e=()=>{h._initPending(),new MutationObserver(e=>{e.forEach(e=>{e.addedNodes.forEach(e=>{e.isConnected&&j(e)}),e.removedNodes.forEach(e=>w(e))})}).observe(document.documentElement,{childList:!0,subtree:!0}),j(document.documentElement)};"loading"!==document.readyState?e():document.addEventListener("DOMContentLoaded",e,!0)}export{i as $,d as $$,h as Component,L as Hash,$ as LocalStorage,o as NewState,x as RefreshState,v as SetTranslator,S as Util,j as _scanTree,w as _unbindTree}; +var e;let t=null,n=null;const a=e=>t=e,r=e=>n=e,s=new Set;function o(e={},a=null,r=null){const o={},i=new Map,d=new Map,c=(e,t)=>(d.has(e)||d.set(e,new Set),t?d.get(e).add(t):d.get(e).clear(),()=>d.get(e).delete(t)),l=(e,t)=>{d.has(e)&&d.get(e).delete(t)},h=a||(e=>o[e]),u=r||((e,t)=>o[e]=t);return Object.assign(o,e),new Proxy(o,{get:(e,n)=>"__watch"===n?c:"__unwatch"===n?l:"__isProxy"===n||(t&&(i.has(n)||i.set(n,new Set),i.get(n).add(t),t.node._states||(t.node._states=new Set),t.node._states.add(i)),h(n)),set(e,t,a){if(h(t)!==a&&u(t,a),d.has(t)&&d.get(t).forEach(n=>{const r=n(a);void 0!==r&&(a=r,e[t]=a)}),d.has(null)&&d.get(null).forEach(e=>e(a)),i.has(t)){const e=i.get(t);for(const t of e)t.node.isConnected?n!==t.node&&s.forEach(e=>e(t)):e.delete(t)}return!0}})}const i=(e,t)=>t?e.querySelector(t):document.querySelector(e),d=(e,t)=>t?e.querySelectorAll(t):document.querySelectorAll(e),c=new Map,l=[],h={getTemplate:e=>document.querySelector(`template[component="${e.toUpperCase()}"]`),register:(e,t,n=null,...a)=>{c.set(e.toUpperCase(),t),"loading"!==document.readyState?h._addTemplate(e,n,a):l.push([e,n,a])},exists:e=>c.has(e.toUpperCase()),getSetupFunction:e=>c.get(e.toUpperCase()),_addTemplate:(e,t,n)=>{if(t){const n=document.createElement("TEMPLATE");n.setAttribute("component",e.toUpperCase()),n.content.appendChild(t),document.body.appendChild(n)}n&&n.forEach(e=>document.body.appendChild(e))},_initPending:()=>{l.forEach(([e,t,n])=>h._addTemplate(e,t,n)),l.length=0}};function u(e,t,n,a={}){e.attributes&&Array.from(e.attributes).forEach(e=>{"class"!==e.name&&("style"===e.name?t.hasAttribute("style")?t.setAttribute("style",`${e.value}; ${t.getAttribute("style")}`):t.setAttribute("style",e.value):t.hasAttribute(e.name)||t.setAttribute(e.name,e.value))}),t.classList.add(...e.classList);const r="TEMPLATE"===e.tagName?e.content:e,s="TEMPLATE"===t.tagName?t.content:t;Array.from(r.childNodes).forEach(e=>s.appendChild(e)),e.tagName&&h.exists(e.tagName)&&f(e.tagName,t,n,a)}function f(e,t,n,a={}){if(a[e])return;a[e]=!0,n.thisObj&&Array.from(t.attributes).forEach(e=>{(e.name.startsWith("$")||e.name.startsWith("st-"))&&e.value.includes("this.")&&(e.value=e.value.replace(/\bthis\./g,"this.parent."))});const r=h.getSetupFunction(e),s={};Array.from(t.childNodes).forEach(e=>{e.nodeType===Node.ELEMENT_NODE&&e.hasAttribute("slot")&&(s[e.getAttribute("slot")]=e,e.removeAttribute("slot"))}),t.innerHTML="",t.state=o(t.state||{});const i=h.getTemplate(e);if(i){const e=i.content.cloneNode(!0);if(e.childNodes.length){const r=Array.from(e.childNodes).find(e=>e.nodeType===Node.ELEMENT_NODE);r&&u(r,t,n,a),d(t,"[slot-id]").forEach(e=>{const t=e.getAttribute("slot-id");s[t]&&(e.removeAttribute("slot-id"),e.innerHTML="",u(s[t],e,n,a))})}}r&&r(t)}let b=!1;function p(e){b=e}const m=new Map;function g(e,t,n,a){const r={...a||{},...t||{}},s=Object.keys(r),o=Object.values(r),i=e+s.join(",");try{let t=m.get(i);return t||(t=new Function("Hash","LocalStorage","State",...s,e),m.set(i,t)),t.apply(n,[globalThis.Hash,globalThis.LocalStorage,globalThis.State,...o])}catch(r){return b||console.error(r,a,[e,a,t,n]),null}}function _(e,t,n,a){return e.includes("${")?g("return `"+e+"`",t,n,a):g("return "+e,t,n,a)}let E=(e,t)=>e&&"string"==typeof e?e.replace(/\{(.+?)\}/g,(e,n)=>t.hasOwnProperty(n)?t[n]:e):e;const v=e=>E=e,y=e=>e&&"string"==typeof e&&e.includes("{#")?e.replace(/\{#(.+?)#\}/g,(e,t)=>{const n=t.split("||").map(e=>e.trim()),a={};if(n.length>1){const e=n[0].match(/\{(.+?)\}/g);e&&e.forEach((e,t)=>a[e.substring(1,e.length-1)]=n[t+1]||"")}return E(n[0],a)}):e;if("undefined"!=typeof document)try{document.createElement("div").setAttribute("$t","1")}catch(e){const t=Element.prototype.setAttribute;Element.prototype.setAttribute=function(e,n){return e.startsWith("$")?t.call(this,"st-"+e.substring(1),n):t.call(this,e,n)}}var A;function N(e){e._renderedNodes&&e._renderedNodes.forEach(e=>e.forEach(e=>{e.remove(),e._renderedNodes&&N(e)}))}function T(e){const t=e.node;if(!t.isConnected&&"TEMPLATE"!==t.tagName)return;a(e),window.__perfTrace&&window.__perfTrace.evalCount++;const n=window.__perfTrace?performance.now():0;let r=e.exp?e.tpl?_(e.tpl,{thisNode:t},t._thisObj||t,t._ref||null):null:e.tpl;if(window.__perfTrace&&(window.__perfTrace.evalTotal+=performance.now()-n),a(null),e.prop){const n=e.prop;let a=t;for(let e=0;ew(e,{thisObj:t._thisObj,extendVars:e._ref})):(t._children.forEach(e=>{e._stManaged=!0,t.parentNode.insertBefore(e,t),e._ref={...t._ref}}),t._renderedNodes=[t._children]):(N(t),t._renderedNodes=[]);else if("each"===n)if(r&&"object"==typeof r){const e=t.getAttribute("as")||"item",n=t.getAttribute("index")||"index",a=t.getAttribute("key");let s,o;if(r instanceof Map)s=Array.from(r.keys()),o=e=>r.get(e);else if("function"==typeof r[Symbol.iterator]){const e=Array.isArray(r)?r:Array.from(r);s=new Array(e.length);for(let t=0;te[t]}else s=Object.keys(r),o=e=>r[e];t._keyedNodes||(t._keyedNodes=new Map);const i=new Map,d=[];s.forEach((r,s)=>{const c=o(r),l=a?c&&"object"==typeof c?c[a]:c:r,h=null==l||i.has(l)?`st_key_fallback_${s}_${Math.random()}`:l;let u=t._keyedNodes.get(h);u?(t._keyedNodes.delete(h),u.forEach(a=>{a._ref[n]=r,a._ref[e]!==c?(a._ref[e]=c,w(a,{thisObj:t._thisObj,extendVars:a._ref})):t.parentNode.lastChild!==a&&t.parentNode.insertBefore(a,t)})):(u=[],t._children.forEach(a=>{const s=a.cloneNode(!0);s._stManaged=!0,s._ref={...t._ref,[n]:r,[e]:c},s._thisObj=t._thisObj,t.parentNode.insertBefore(s,t),u.push(s)})),i.set(h,u),d.push(u),u.forEach(e=>t.parentNode.insertBefore(e,t))}),t._keyedNodes.forEach(e=>e.forEach(e=>{N(e),e.remove()})),t._keyedNodes=i,t._renderedNodes=d}else N(t),t._keyedNodes&&t._keyedNodes.forEach(e=>e.forEach(e=>e.remove())),t._keyedNodes=new Map,t._renderedNodes=[];else if("bind"===n){if(["INPUT","SELECT","TEXTAREA"].includes(t.tagName)&&!t.hasAttribute("autocomplete")&&t.setAttribute("autocomplete","off"),"checkbox"===t.type){"on"===t.value||r||(g(`${e.tpl} = []`,{thisNode:t},t._thisObj||t,t._ref||{}),r=[]),t._checkboxMultiMode=r instanceof Array;const n=r instanceof Array?r.includes(t.value):!!r;t.checked!==n&&(t.checked=n)}else"radio"===t.type?t.checked!==(t.value===String(r??""))&&(t.checked=t.value===String(r??"")):"value"in t&&"file"!==t.type?setTimeout(()=>{t.value!==String(r??"")&&(t.value=r)}):t.isContentEditable&&t.innerHTML!==String(r??"")&&(t.innerHTML=r);t.dispatchEvent(new CustomEvent("bind",{bubbles:!1,detail:r}))}else["checked","disabled","readonly"].includes(n)&&(r=!!r),"boolean"==typeof r?r?t.setAttribute(n,""):t.removeAttribute(n):void 0!==r&&("string"!=typeof r&&(r=JSON.stringify(r)),"text"===n?t.textContent=r??"":"html"===n?t.innerHTML=r??"":"IMG"===t.tagName&&"src"===n&&r.includes(".svg")?t.setAttribute("_src",r??""):t.setAttribute(n,r??""))}}A=e=>T(e),s.add(A);const O=e=>{e.node._bindings||(e.node._bindings=[]),e.node._bindings.push({attr:e.attr,prop:e.prop,tpl:e.tpl,exp:e.exp}),T(e)},w=(e,t={})=>{if(3===e.nodeType){if(e._stTranslated)return;const t=y(e.textContent);return t!==e.textContent&&(e.textContent=t),void(e._stTranslated=!0)}if(1!==e.nodeType)return;if(e._stTranslated||(Array.from(e.attributes).forEach(e=>{if(!e.name.startsWith("$")&&!e.name.startsWith("st-")&&!e.name.startsWith(".")){const t=y(e.value);t!==e.value&&(e.value=t)}}),e._stTranslated=!0),"TEMPLATE"!==e.tagName&&(e.hasAttribute("$if")||e.hasAttribute("$each")||e.hasAttribute("st-if")||e.hasAttribute("st-each"))){const t=document.createElement("TEMPLATE");return Array.from(e.attributes).filter(t=>["$if","$each","st-if","st-each"].includes(t.name)||(e.hasAttribute("$each")||e.hasAttribute("st-each"))&&["as","index"].includes(t.name)).forEach(n=>{t.setAttribute(n.name,n.value),e.removeAttribute(n.name)}),e.parentNode.insertBefore(t,e),t.content.appendChild(e),void(t._ref=e._ref)}if("TEMPLATE"===e.tagName&&(e.hasAttribute("$if")||e.hasAttribute("st-if"))&&(e.hasAttribute("$each")||e.hasAttribute("st-each"))){const t=document.createElement("TEMPLATE"),n=Array.from(e.attributes).filter(e=>["$if","$each","st-if","st-each"].includes(e.name)),a=n[n.length-1];t.setAttribute(a.name,a.value),e.removeAttribute(a.name),"$each"!==a.name&&"st-each"!==a.name||Array.from(e.attributes).filter(e=>["as","index"].includes(e.name)).forEach(n=>{t.setAttribute(n.name,n.value),e.removeAttribute(n.name)}),Array.from(e.content.childNodes).forEach(e=>t.content.appendChild(e)),e.content.appendChild(t),t._ref=e._ref}if("IMG"===e.tagName&&(e.hasAttribute("src")||e.hasAttribute("_src")||e.hasAttribute("$src"))){const t=e;Promise.resolve().then(()=>{const e=t.getAttribute("_src")||t.getAttribute("src");e&&fetch(e,{cache:"force-cache"}).then(e=>e.text()).then(e=>{const n=(new DOMParser).parseFromString(e,"image/svg+xml").querySelector("svg");n&&(Array.from(t.attributes).forEach(e=>n.setAttribute(e.name,e.value)),t.replaceWith(n))})})}if(void 0!==e._thisObj)t.thisObj=e._thisObj||null;else{let n=e;for(;n&&void 0===n._thisObj;)n=n.parentNode;t.thisObj=n?n._thisObj:null}if(void 0===e._ref){let t=e;for(;t&&void 0===t._ref;)t=t.parentNode;e._ref=t?{...t._ref}:{}}t.extendVars&&Object.assign(e._ref,t.extendVars),((e,t)=>{let n=!1;if(e._bindings&&(e._states=new Set,e._bindings.forEach(t=>T({node:e,...t})),e._hasOnUpdate&&e.dispatchEvent(new Event("update",{bubbles:!1})),n=!0),h.exists(e.tagName)&&!e._componentInitialized&&(Array.from(e.attributes).forEach(n=>{var a;if(n.name.startsWith("$.")){const r=n.name.slice(2);let s=y(n.value);s.includes("this.")&&(s=s.replace(/\bthis\./g,"this.parent."));const o=_(s,{thisNode:e},{parent:t.thisObj||e},e._ref||{});let i=e;const d=r.split(".");for(let e=0;ee.removeAttribute("slot-id")),e._componentInitialized=!0,e._thisObj||(e._thisObj=e)),"TEMPLATE"===e.tagName&&(e._children=[...e.content.childNodes],e._renderedNodes||(e._renderedNodes=[])),n)return;let a=[];"TEMPLATE"===e.tagName?["$if","$each","st-if","st-each"].forEach(t=>e.hasAttribute(t)&&a.push(e.getAttributeNode(t))):a=Array.from(e.attributes).filter(e=>(e.name.startsWith("$")||e.name.startsWith("st-"))&&!["$if","$each","st-if","st-each"].includes(e.name)||e.name.includes(".")),e._thisObj&&t.thisObj&&(e._thisObj.parent=t.thisObj),e._thisObj||(e._thisObj=t.thisObj||null),e._ref||(e._ref=t.extendVars||{}),e._states=new Set,a.forEach(n=>{const a=n.name.startsWith("$")||n.name.startsWith("st-"),s=a?n.name.slice(n.name.startsWith("$")?1:3):n.name;let o=n.value;if(e.removeAttribute(n.name),s.startsWith("."))O({node:e,prop:s.split("."),tpl:o,exp:a});else if(s.startsWith("on")){const n=s.slice(2);"update"===n&&(e._hasOnUpdate=!0),"load"!==n||["BODY","IMG","IFRAME"].includes(e.tagName)||(e._hasOnLoad=!0),"unload"!==n||["BODY","IMG","IFRAME"].includes(e.tagName)||(e._hasOnUnload=!0),e.addEventListener(n,n=>g(o,{event:n,thisNode:e,...n.detail||{}},t.thisObj||e,e._ref||{}))}else"bind"===s?e.addEventListener("TEXTAREA"===e.tagName||e.isContentEditable||"text"===e.type||"password"===e.type?"input":"change",n=>{let a=e.isContentEditable?n.target.innerHTML:"checkbox"===e.type?n.target.checked:n.target.files||n.target.value||n.detail;r(e),p(!0),"checkbox"===e.type&&e._checkboxMultiMode?g(`!!checked ? (!${o}.includes(val) && ${o}.push(val)) : (index = ${o}.indexOf(val), index > -1 && ${o}.splice(index, 1))`,{val:e.value,checked:a,thisNode:e},t.thisObj||e,e._ref||{}):g(`${o} = val`,{val:a,thisNode:e},t.thisObj||e,e._ref||{}),p(!1),r(null)}):"text"!==s||o||(o=e.textContent,e.textContent=""),o&&(o=y(o),O({node:e,attr:s,tpl:o,exp:a}))}),(e._hasOnLoad||e._componentInitialized)&&Promise.resolve().then(()=>e.dispatchEvent(new Event("load",{bubbles:!1}))),e._hasOnUpdate&&e.dispatchEvent(new Event("update",{bubbles:!1})),e._thisObj&&(t.thisObj=e._thisObj)})(e,t);const n=[...e.childNodes||[]],a={thisObj:t.thisObj,extendVars:{...e._ref}};n.forEach(e=>{e._stManaged||w(e,a)})},j=e=>{1===e.nodeType&&(e._hasOnUnload&&e.dispatchEvent(new Event("unload",{bubbles:!1})),e._states&&e._states.forEach(t=>{for(const[n,a]of t)for(const t of a)t.node===e&&a.delete(t)}),e.childNodes&&e.childNodes.forEach(e=>j(e)))},x=w,M={clone:window.structuredClone||(e=>JSON.parse(JSON.stringify(e))),base64:e=>btoa(String.fromCharCode(...(new TextEncoder).encode(e))),unbase64:e=>(new TextDecoder).decode(Uint8Array.from(atob(e),e=>e.charCodeAt(0))),urlbase64:e=>M.base64(e).replace(/[+/=]/g,e=>({"+":"-","/":"","=":""}[e])),unurlbase64:e=>M.unbase64(e.replace(/[-_.]/g,e=>({"-":"+",_:"/",".":"="}[e])).padEnd(4*Math.ceil(e.length/4),"=")),safeJson:e=>{try{return JSON.parse(e)}catch{return null}},updateDefaults:(e,t)=>{for(const n in t)void 0===e[n]&&(e[n]=t[n])},copyFunction:(e,t,...n)=>{n.forEach(n=>e[n]=t[n].bind(t))},getFunctionBody:e=>{const t=e.toString();return t.slice(t.indexOf("{")+1,t.lastIndexOf("}")).trim()},makeDom:e=>{e.includes(">\n")&&(e=e.replace(/>\s+<").trim());const t=document.createElement("div");return t.innerHTML=e,t.children[0]},newAvg:()=>{let e=0,t=0,n=0;return{add:a=>(e+=a,t++,n=e/t),get:()=>n,clear:()=>{e=0,t=0,n=0}}},newTimeCount:()=>{let e=0,t=0,n=0;return{start:()=>e=(new Date).getTime(),end:()=>{const a=(new Date).getTime(),r=a-e;return e=a,t+=r,n++,r},avg:()=>t/n}}};globalThis.Util=M;let S=new URLSearchParams((null==(e=window.location.hash)?void 0:e.substring(1))||"");const L=o({},e=>M.safeJson(S.get(e)),(e,t)=>{const n=S.get(e),a=void 0===t?void 0:JSON.stringify(t);n===a||null===n&&void 0===a||(void 0===t?S.delete(e):S.set(e,a),window.location.hash="#"+S.toString())});"undefined"!=typeof window&&window.addEventListener("hashchange",()=>{var e;const t=S;S=new URLSearchParams((null==(e=window.location.hash)?void 0:e.substring(1))||""),S.forEach((e,n)=>{t.get(n)!==e&&(L[n]=M.safeJson(e))}),t.forEach((e,t)=>{void 0===S.get(t)&&(L[t]=void 0)})});const $=o({},e=>M.safeJson(localStorage.getItem(e)),(e,t)=>{const n=localStorage.getItem(e),a=void 0===t?void 0:JSON.stringify(t);n===a||null===n&&void 0===a||(void 0===t?localStorage.removeItem(e):localStorage.setItem(e,a))});if(globalThis.Hash=L,globalThis.LocalStorage=$,"undefined"!=typeof document){const e=()=>{h._initPending(),new MutationObserver(e=>{e.forEach(e=>{e.addedNodes.forEach(e=>{e.isConnected&&w(e)}),e.removedNodes.forEach(e=>j(e))})}).observe(document.documentElement,{childList:!0,subtree:!0}),w(document.documentElement)};"loading"!==document.readyState?e():document.addEventListener("DOMContentLoaded",e,!0)}export{i as $,d as $$,h as Component,L as Hash,$ as LocalStorage,o as NewState,x as RefreshState,v as SetTranslator,M as Util,w as _scanTree,j as _unbindTree}; diff --git a/src/dom.js b/src/dom.js index 8a18306..3b565a2 100644 --- a/src/dom.js +++ b/src/dom.js @@ -49,7 +49,10 @@ export function _updateBinding(binding) { if (!node.isConnected && node.tagName !== 'TEMPLATE') return; setActiveBinding(binding); + if (window.__perfTrace) window.__perfTrace.evalCount++; + const evalStart = window.__perfTrace ? performance.now() : 0; let result = binding.exp ? (binding.tpl ? _returnCode(binding.tpl, { thisNode: node }, node._thisObj || node, node._ref || null) : null) : binding.tpl; + if (window.__perfTrace) window.__perfTrace.evalTotal += (performance.now() - evalStart); setActiveBinding(null); if (binding.prop) { @@ -78,10 +81,13 @@ export function _updateBinding(binding) { if (result) { if (!node._renderedNodes || node._renderedNodes.length === 0) { node._children.forEach(child => { + child._stManaged = true; node.parentNode.insertBefore(child, node); child._ref = { ...node._ref }; }); node._renderedNodes = [node._children]; + } else { + node._renderedNodes[0].forEach(child => _scanTree(child, { thisObj: node._thisObj, extendVars: child._ref })); } } else { _clearRenderedNodes(node); @@ -91,48 +97,71 @@ export function _updateBinding(binding) { if (result && typeof result === 'object') { const asName = node.getAttribute('as') || 'item'; const indexName = node.getAttribute('index') || 'index'; + const keyName = node.getAttribute('key'); let keys, getVal; if (result instanceof Map) { - keys = Array.from(result.keys()); - getVal = k => result.get(k); + keys = Array.from(result.keys()); getVal = k => result.get(k); } else if (typeof result[Symbol.iterator] === 'function') { const arr = Array.isArray(result) ? result : Array.from(result); keys = new Array(arr.length); for (let i = 0; i < arr.length; i++) keys[i] = i; getVal = k => arr[k]; } else { - keys = Object.keys(result); - getVal = k => result[k]; + keys = Object.keys(result); getVal = k => result[k]; } + + if (!node._keyedNodes) node._keyedNodes = new Map(); + const newKeyedNodes = new Map(); + const currentRenderedNodes = []; + keys.forEach((k, i) => { const item = getVal(k); - if (node._renderedNodes && i < node._renderedNodes.length) { - node._renderedNodes[i].forEach(child => { + const rawKey = keyName ? (item && typeof item === 'object' ? item[keyName] : item) : k; + // Safety: Handle potential duplicate keys or undefined keys + const keyVal = (rawKey === undefined || rawKey === null || newKeyedNodes.has(rawKey)) ? `st_key_fallback_${i}_${Math.random()}` : rawKey; + + let existingNodes = node._keyedNodes.get(keyVal); + + if (existingNodes) { + // Reuse existing nodes + node._keyedNodes.delete(keyVal); + existingNodes.forEach(child => { child._ref[indexName] = k; - child._ref[asName] = item; - _scanTree(child, { thisObj: node._thisObj, extendVars: child._ref }); + // If data reference hasn't changed, skip heavy scan + if (child._ref[asName] !== item) { + child._ref[asName] = item; + _scanTree(child, { thisObj: node._thisObj, extendVars: child._ref }); + } else if (node.parentNode.lastChild !== child) { + // Just move to the end to maintain order + node.parentNode.insertBefore(child, node); + } }); } else { - const newNodes = []; - if (!node._renderedNodes) node._renderedNodes = []; + // Create new nodes + existingNodes = []; node._children.forEach(child => { const cloned = child.cloneNode(true); + cloned._stManaged = true; cloned._ref = { ...node._ref, [indexName]: k, [asName]: item }; cloned._thisObj = node._thisObj; node.parentNode.insertBefore(cloned, node); - newNodes.push(cloned); + existingNodes.push(cloned); }); - node._renderedNodes.push(newNodes); } + newKeyedNodes.set(keyVal, existingNodes); + currentRenderedNodes.push(existingNodes); + // Ensure DOM order + existingNodes.forEach(child => node.parentNode.insertBefore(child, node)); }); - while (node._renderedNodes && node._renderedNodes.length > keys.length) { - node._renderedNodes.pop().forEach(child => { - _clearRenderedNodes(child); - child.remove(); - }); - } + + // Cleanup old nodes + node._keyedNodes.forEach(nodes => nodes.forEach(child => { _clearRenderedNodes(child); child.remove(); })); + node._keyedNodes = newKeyedNodes; + node._renderedNodes = currentRenderedNodes; } else { _clearRenderedNodes(node); + if (node._keyedNodes) node._keyedNodes.forEach(nodes => nodes.forEach(child => child.remove())); + node._keyedNodes = new Map(); node._renderedNodes = []; } } else if (attr === 'bind') { @@ -322,7 +351,9 @@ export const _scanTree = (node, scanObj = {}) => { const nodes = [...(node.childNodes || [])]; const nextScanObj = { thisObj: scanObj.thisObj, extendVars: { ...node._ref } }; - nodes.forEach(child => _scanTree(child, nextScanObj)); + nodes.forEach(child => { + if (!child._stManaged) _scanTree(child, nextScanObj); + }); }; export const _unbindTree = (node) => { diff --git a/src/dom.js.keyed.bak b/src/dom.js.keyed.bak new file mode 100644 index 0000000..c9ea78d --- /dev/null +++ b/src/dom.js.keyed.bak @@ -0,0 +1,352 @@ +// src/dom.js +import { _runCode, _returnCode, setDisableRunCodeError } from './core.js'; +import { getActiveBinding, setActiveBinding, getNoWriteBack, setNoWriteBack, NewState, onNotifyUpdate } from './observer.js'; +import { Component, _makeComponent, _mergeNode } from './component.js'; +import { $, $$ } from './dom-utils.js'; + +let _translator = (text, args) => { + if (!text || typeof text !== 'string') return text; + return text.replace(/\{(.+?)\}/g, (match, key) => args.hasOwnProperty(key) ? args[key] : match); +}; +export const SetTranslator = (fn) => _translator = fn; + +const _translate = (text) => { + if (!text || typeof text !== 'string' || !text.includes('{#')) return text; + return text.replace(/\{#(.+?)#\}/g, (m, content) => { + const parts = content.split('||').map(s => s.trim()); + const args = {}; + if (parts.length > 1) { + const matches = parts[0].match(/\{(.+?)\}/g); + if (matches) matches.forEach((match, i) => args[match.substring(1, match.length - 1)] = parts[i + 1] || ''); + } + return _translator(parts[0], args); + }); +}; + +if (typeof document !== 'undefined') { + try { document.createElement('div').setAttribute('$t', '1'); } catch (e) { + const originalSetAttribute = Element.prototype.setAttribute; + Element.prototype.setAttribute = function (name, value) { + if (!name.startsWith('$')) return originalSetAttribute.call(this, name, value); + return originalSetAttribute.call(this, 'st-' + name.substring(1), value); + }; + } +} + +export { $, $$ }; + +onNotifyUpdate((binding) => _updateBinding(binding)); + +export function _clearRenderedNodes(node) { + if (node._renderedNodes) node._renderedNodes.forEach(nodes => nodes.forEach(child => { + child.remove(); + if (child._renderedNodes) _clearRenderedNodes(child); + })); +} + +export function _updateBinding(binding) { + const node = binding.node; + if (!node.isConnected && node.tagName !== 'TEMPLATE') return; + + setActiveBinding(binding); + if (window.__perfTrace) window.__perfTrace.evalCount++; + const evalStart = window.__perfTrace ? performance.now() : 0; + let result = binding.exp ? (binding.tpl ? _returnCode(binding.tpl, { thisNode: node }, node._thisObj || node, node._ref || null) : null) : binding.tpl; + if (window.__perfTrace) window.__perfTrace.evalTotal += (performance.now() - evalStart); + setActiveBinding(null); + + if (binding.prop) { + const prop = binding.prop; + let o = node; + for (let i = 0; i < prop.length - 1; i++) { + if (!prop[i]) continue; + if (o[prop[i]] == null) o[prop[i]] = {}; + o = o[prop[i]]; + if (typeof o !== 'object') break; + } + if (typeof o === 'object' && o !== null) { + const lk = prop[prop.length - 1]; + if (lk) { + if (typeof result === 'object' && result != null && !Array.isArray(result) && o[lk] == null) o[lk] = {}; + const lo = o[lk]; + if (typeof lo === 'object' && lo != null && lo.__watch) Object.assign(lo, result); + else o[lk] = result; + } else if (typeof result === 'object' && result != null && !Array.isArray(result)) { + Object.assign(o, result); + } + } + } else if (binding.attr) { + const attr = binding.attr; + if (attr === 'if') { + if (result) { + if (!node._renderedNodes || node._renderedNodes.length === 0) { + node._children.forEach(child => { + child._stManaged = true; + node.parentNode.insertBefore(child, node); + child._ref = { ...node._ref }; + }); + node._renderedNodes = [node._children]; + } else { + node._renderedNodes[0].forEach(child => _scanTree(child, { thisObj: node._thisObj, extendVars: child._ref })); + } + } else { + _clearRenderedNodes(node); + node._renderedNodes = []; + } + } else if (attr === 'each') { + if (result && typeof result === 'object') { + const asName = node.getAttribute('as') || 'item'; + const indexName = node.getAttribute('index') || 'index'; + let keys, getVal; + if (result instanceof Map) { + keys = Array.from(result.keys()); + getVal = k => result.get(k); + } else if (typeof result[Symbol.iterator] === 'function') { + const arr = Array.isArray(result) ? result : Array.from(result); + keys = new Array(arr.length); + for (let i = 0; i < arr.length; i++) keys[i] = i; + getVal = k => arr[k]; + } else { + keys = Object.keys(result); + getVal = k => result[k]; + } + keys.forEach((k, i) => { + const item = getVal(k); + if (node._renderedNodes && i < node._renderedNodes.length) { + node._renderedNodes[i].forEach(child => { + child._ref[indexName] = k; + child._ref[asName] = item; + // 性能探针:记录节点复用时的 _scanTree 耗时 + if (window.__perfTrace) window.__perfTrace.eachUpdateCount++; + const start = window.__perfTrace ? performance.now() : 0; + _scanTree(child, { thisObj: node._thisObj, extendVars: child._ref }); + if (window.__perfTrace) window.__perfTrace.eachUpdateTotal += (performance.now() - start); + }); + } else { + const newNodes = []; + if (!node._renderedNodes) node._renderedNodes = []; + node._children.forEach(child => { + const cloned = child.cloneNode(true); + cloned._stManaged = true; + cloned._ref = { ...node._ref, [indexName]: k, [asName]: item }; + cloned._thisObj = node._thisObj; + node.parentNode.insertBefore(cloned, node); + newNodes.push(cloned); + }); + node._renderedNodes.push(newNodes); + } + }); + while (node._renderedNodes && node._renderedNodes.length > keys.length) { + node._renderedNodes.pop().forEach(child => { + _clearRenderedNodes(child); + child.remove(); + }); + } + } else { + _clearRenderedNodes(node); + node._renderedNodes = []; + } + } else if (attr === 'bind') { + if (['INPUT', 'SELECT', 'TEXTAREA'].includes(node.tagName) && !node.hasAttribute('autocomplete')) node.setAttribute('autocomplete', 'off'); + if (node.type === 'checkbox') { + if (node.value !== 'on' && !result) { _runCode(`${binding.tpl} = []`, { thisNode: node }, node._thisObj || node, node._ref || {}); result = []; } + node._checkboxMultiMode = result instanceof Array; + const isChecked = result instanceof Array ? result.includes(node.value) : !!result; + if (node.checked !== isChecked) node.checked = isChecked; + } else if (node.type === 'radio') { + if (node.checked !== (node.value === String(result ?? ''))) node.checked = (node.value === String(result ?? '')); + } else if ('value' in node && node.type !== 'file') { + setTimeout(() => { if (node.value !== String(result ?? '')) node.value = result; }); + } else if (node.isContentEditable) { + if (node.innerHTML !== String(result ?? '')) node.innerHTML = result; + } + node.dispatchEvent(new CustomEvent('bind', { bubbles: false, detail: result })); + } else { + if (['checked', 'disabled', 'readonly'].includes(attr)) result = !!result; + if (typeof result === 'boolean') result ? node.setAttribute(attr, '') : node.removeAttribute(attr); + else if (result !== undefined) { + if (typeof result !== 'string') result = JSON.stringify(result); + if (attr === 'text') node.textContent = result ?? ''; + else if (attr === 'html') node.innerHTML = result ?? ''; + else if (node.tagName === 'IMG' && attr === 'src' && result.includes('.svg')) node.setAttribute('_src', result ?? ''); + else node.setAttribute(attr, result ?? ''); + } + } + } +} + +export const _initBinding = (binding) => { + if (!binding.node._bindings) binding.node._bindings = []; + binding.node._bindings.push({ attr: binding.attr, prop: binding.prop, tpl: binding.tpl, exp: binding.exp }); + _updateBinding(binding); +}; + +export const _parseNode = (node, scanObj) => { + let hasBindings = false; + if (node._bindings) { + node._states = new Set(); + node._bindings.forEach(b => _updateBinding({ node, ...b })); + if (node._hasOnUpdate) node.dispatchEvent(new Event('update', { bubbles: false })); + hasBindings = true; + } + + if (Component.exists(node.tagName) && !node._componentInitialized) { + Array.from(node.attributes).forEach(attr => { + if (attr.name.startsWith('$.')) { + const realAttrName = attr.name.slice(2); + let tpl = _translate(attr.value); + if (tpl.includes('this.')) tpl = tpl.replace(/\bthis\./g, 'this.parent.'); + const result = _returnCode(tpl, { thisNode: node }, { parent: scanObj.thisObj || node }, node._ref || {}); + let o = node; + const prop = realAttrName.split('.'); + for (let i = 0; i < prop.length - 1; i++) { if (prop[i]) o = (o[prop[i]] ??= {}); } + o[prop[prop.length - 1]] = result; + node.removeAttribute(attr.name); + } + }); + _makeComponent(node.tagName, node, scanObj); + $$(node, '[slot-id]').forEach(p => p.removeAttribute('slot-id')); + node._componentInitialized = true; + if (!node._thisObj) node._thisObj = node; + } + + if (node.tagName === 'TEMPLATE') { + node._children = [...node.content.childNodes]; + if (!node._renderedNodes) node._renderedNodes = []; + } + + if (hasBindings) return; + + let attrs = []; + if (node.tagName === 'TEMPLATE') { + ['$if', '$each', 'st-if', 'st-each'].forEach(n => node.hasAttribute(n) && attrs.push(node.getAttributeNode(n))); + } else { + attrs = Array.from(node.attributes).filter(a => (a.name.startsWith('$') || a.name.startsWith('st-')) && !['$if', '$each', 'st-if', 'st-each'].includes(a.name) || a.name.includes('.')); + } + + if (node._thisObj && scanObj.thisObj) node._thisObj.parent = scanObj.thisObj; + if (!node._thisObj) node._thisObj = scanObj.thisObj || null; + if (!node._ref) node._ref = scanObj.extendVars || {}; + node._states = new Set(); + + attrs.forEach(attr => { + const exp = attr.name.startsWith('$') || attr.name.startsWith('st-'); + const realAttrName = exp ? attr.name.slice(attr.name.startsWith('$') ? 1 : 3) : attr.name; + let tpl = attr.value; + node.removeAttribute(attr.name); + if (realAttrName.startsWith('.')) _initBinding({ node, prop: realAttrName.split('.'), tpl, exp }); + else if (realAttrName.startsWith('on')) { + const eventName = realAttrName.slice(2); + if (eventName === 'update') node._hasOnUpdate = true; + if (eventName === 'load' && !['BODY', 'IMG', 'IFRAME'].includes(node.tagName)) node._hasOnLoad = true; + if (eventName === 'unload' && !['BODY', 'IMG', 'IFRAME'].includes(node.tagName)) node._hasOnUnload = true; + node.addEventListener(eventName, (e) => _runCode(tpl, { event: e, thisNode: node, ...(e.detail || {}) }, scanObj.thisObj || node, node._ref || {})); + } else { + if (realAttrName === 'bind') { + node.addEventListener(node.tagName === 'TEXTAREA' || node.isContentEditable || node.type === 'text' || node.type === 'password' ? 'input' : 'change', (e) => { + let newVal = node.isContentEditable ? e.target.innerHTML : (node.type === 'checkbox' ? e.target.checked : e.target.files || e.target.value || e.detail); + setNoWriteBack(node); setDisableRunCodeError(true); + if (node.type === 'checkbox' && node._checkboxMultiMode) _runCode(`!!checked ? (!${tpl}.includes(val) && ${tpl}.push(val)) : (index = ${tpl}.indexOf(val), index > -1 && ${tpl}.splice(index, 1))`, { val: node.value, checked: newVal, thisNode: node }, scanObj.thisObj || node, node._ref || {}); + else _runCode(`${tpl} = val`, { val: newVal, thisNode: node }, scanObj.thisObj || node, node._ref || {}); + setDisableRunCodeError(false); setNoWriteBack(null); + }); + } else if (realAttrName === 'text' && !tpl) { tpl = node.textContent; node.textContent = ''; } + if (tpl) { tpl = _translate(tpl); _initBinding({ node, attr: realAttrName, tpl, exp }); } + } + }); + + if (node._hasOnLoad || node._componentInitialized) Promise.resolve().then(() => node.dispatchEvent(new Event('load', { bubbles: false }))); + if (node._hasOnUpdate) node.dispatchEvent(new Event('update', { bubbles: false })); + if (node._thisObj) scanObj.thisObj = node._thisObj; +}; + +export const _scanTree = (node, scanObj = {}) => { + if (node.nodeType === 3) { + if (node._stTranslated) return; + const translated = _translate(node.textContent); + if (translated !== node.textContent) node.textContent = translated; + node._stTranslated = true; return; + } + if (node.nodeType !== 1) return; + + if (!node._stTranslated) { + Array.from(node.attributes).forEach(attr => { + if (!attr.name.startsWith('$') && !attr.name.startsWith('st-') && !attr.name.startsWith('.')) { + const translated = _translate(attr.value); + if (translated !== attr.value) attr.value = translated; + } + }); + node._stTranslated = true; + } + + if (node.tagName !== 'TEMPLATE' && (node.hasAttribute('$if') || node.hasAttribute('$each') || node.hasAttribute('st-if') || node.hasAttribute('st-each'))) { + const template = document.createElement('TEMPLATE'); + const attrs = Array.from(node.attributes).filter(attr => ['$if', '$each', 'st-if', 'st-each'].includes(attr.name) || ((node.hasAttribute('$each') || node.hasAttribute('st-each')) && ['as', 'index'].includes(attr.name))); + attrs.forEach(attr => { template.setAttribute(attr.name, attr.value); node.removeAttribute(attr.name); }); + node.parentNode.insertBefore(template, node); + template.content.appendChild(node); + template._ref = node._ref; + return; + } + if (node.tagName === 'TEMPLATE' && (node.hasAttribute('$if') || node.hasAttribute('st-if')) && (node.hasAttribute('$each') || node.hasAttribute('st-each'))) { + const template = document.createElement('TEMPLATE'); + const attrs = Array.from(node.attributes).filter(attr => ['$if', '$each', 'st-if', 'st-each'].includes(attr.name)); + const attr = attrs[attrs.length - 1]; + template.setAttribute(attr.name, attr.value); + node.removeAttribute(attr.name); + if (attr.name === '$each' || attr.name === 'st-each') { + Array.from(node.attributes).filter(attr => ['as', 'index'].includes(attr.name)).forEach(attr => { template.setAttribute(attr.name, attr.value); node.removeAttribute(attr.name); }); + } + Array.from(node.content.childNodes).forEach(child => template.content.appendChild(child)); + node.content.appendChild(template); + template._ref = node._ref; + } + + if (node.tagName === 'IMG' && (node.hasAttribute('src') || node.hasAttribute('_src') || node.hasAttribute('$src'))) { + const imgNode = node; + Promise.resolve().then(() => { + const url = imgNode.getAttribute('_src') || imgNode.getAttribute('src'); + if (url) fetch(url, { cache: 'force-cache' }).then(r => r.text()).then(svgText => { + const realSvg = new DOMParser().parseFromString(svgText, "image/svg+xml").querySelector('svg'); + if (realSvg) { + Array.from(imgNode.attributes).forEach(attr => realSvg.setAttribute(attr.name, attr.value)); + imgNode.replaceWith(realSvg); + } + }); + }); + } + + if (node._thisObj !== undefined) scanObj.thisObj = node._thisObj || null; + else { + let curr = node; + while (curr && curr._thisObj === undefined) curr = curr.parentNode; + scanObj.thisObj = curr ? curr._thisObj : null; + } + if (node._ref === undefined) { + let curr = node; + while (curr && curr._ref === undefined) curr = curr.parentNode; + node._ref = curr ? { ...curr._ref } : {}; + } + if (scanObj.extendVars) Object.assign(node._ref, scanObj.extendVars); + + _parseNode(node, scanObj); + + const nodes = [...(node.childNodes || [])]; + const nextScanObj = { thisObj: scanObj.thisObj, extendVars: { ...node._ref } }; + nodes.forEach(child => { + if (!child._stManaged) _scanTree(child, nextScanObj); + }); +}; + +export const _unbindTree = (node) => { + if (node.nodeType !== 1) return; + if (node._hasOnUnload) node.dispatchEvent(new Event('unload', { bubbles: false })); + if (node._states) node._states.forEach(mappings => { + for (const [key, bindingSet] of mappings) { + for (const binding of bindingSet) { if (binding.node === node) bindingSet.delete(binding); } + } + }); + node.childNodes && node.childNodes.forEach(child => _unbindTree(child)); +}; + +export const RefreshState = _scanTree;