diff --git a/dist/sigpro.js b/dist/sigpro.js index a098b0c..7f7f2a0 100644 --- a/dist/sigpro.js +++ b/dist/sigpro.js @@ -316,32 +316,39 @@ return container; }; $if.not = (condition, thenVal, otherwiseVal) => $if(() => !(typeof condition === "function" ? condition() : condition), thenVal, otherwiseVal); - var $for = (source, render, keyFn = (item, index) => index) => { + var $for = (source, render, keyFn) => { const marker = document.createTextNode(""); const container = $html("div", { style: "display:contents" }, [marker]); - const cache = new Map; + let cache = new Map; $watch(() => { const items = (typeof source === "function" ? source() : source) || []; - const newKeys = new Set; - items.forEach((item, index) => { - const key = keyFn(item, index); - newKeys.add(key); + const newCache = new Map; + const newOrder = []; + for (let i = 0;i < items.length; i++) { + const item = items[i]; + const key = keyFn ? keyFn(item, i) : i; let run = cache.get(key); if (!run) { - run = _view(() => render(item, index)); - cache.set(key, run); - } - container.insertBefore(run.container, marker); - }); - cache.forEach((run, key) => { - if (!newKeys.has(key)) { - run.destroy(); - if (run.container && run.container.parentNode) { - run.container.remove(); - } + run = _view(() => render(item, i)); + } else { cache.delete(key); } + newCache.set(key, run); + newOrder.push(key); + } + cache.forEach((run) => { + run.destroy(); + run.container.remove(); }); + let anchor = marker; + for (let i = newOrder.length - 1;i >= 0; i--) { + const run = newCache.get(newOrder[i]); + if (run.container.nextSibling !== anchor) { + container.insertBefore(run.container, anchor); + } + anchor = run.container; + } + cache = newCache; }); return container; }; diff --git a/dist/sigpro.min.js b/dist/sigpro.min.js index 90d07b2..044c32b 100644 --- a/dist/sigpro.min.js +++ b/dist/sigpro.min.js @@ -1 +1 @@ -(()=>{var{defineProperty:M,getOwnPropertyNames:w,getOwnPropertyDescriptor:$}=Object,k=Object.prototype.hasOwnProperty;var N=new WeakMap,g=(X)=>{var J=N.get(X),W;if(J)return J;if(J=M({},"__esModule",{value:!0}),X&&typeof X==="object"||typeof X==="function")w(X).map((q)=>!k.call(J,q)&&M(J,q,{get:()=>X[q],enumerable:!(W=$(X,q))||W.enumerable}));return N.set(X,J),J};var v=(X,J)=>{for(var W in J)M(X,W,{get:J[W],enumerable:!0,configurable:!0,set:(q)=>J[W]=()=>q})};var m={};v(m,{$watch:()=>C,$router:()=>H,$mount:()=>E,$if:()=>T,$html:()=>I,$for:()=>b,$:()=>D});var R=null,A=null,P=new Set,F=!1,S=new WeakMap,y=()=>{if(F)return;F=!0;while(P.size>0){let X=Array.from(P).sort((J,W)=>(J.depth||0)-(W.depth||0));P.clear();for(let J of X)if(!J._deleted)J()}F=!1},O=(X)=>{if(R&&!R._deleted)X.add(R),R._deps.add(X)},x=(X)=>{for(let J of X){if(J===R||J._deleted)continue;if(J._isComputed){if(J.markDirty(),J._subs)x(J._subs)}else P.add(J)}if(!F)queueMicrotask(y)},V=(X)=>{if(X._cleanups)X._cleanups.forEach((J)=>J()),X._cleanups.clear();X.childNodes?.forEach(V)},_=(X)=>{let J=new Set,W=A,q=document.createElement("div");q.style.display="contents",A={cleanups:J};try{let Y=X({onCleanup:(j)=>J.add(j)}),z=(j)=>{if(!j)return;if(j._isRuntime)J.add(j.destroy),q.appendChild(j.container);else if(Array.isArray(j))j.forEach(z);else q.appendChild(j instanceof Node?j:document.createTextNode(String(j)))};z(Y)}finally{A=W}return{_isRuntime:!0,container:q,destroy:()=>{J.forEach((Y)=>Y()),V(q),q.remove()}}},D=(X,J=null)=>{if(typeof X==="function"){let Y=new Set,z,j=!0,Z=()=>{if(Z._deleted)return;Z._deps.forEach((G)=>G.delete(Z)),Z._deps.clear();let B=R;R=Z;try{let G=X();if(!Object.is(z,G)||j)z=G,j=!1,x(Y)}finally{R=B}};if(Z._deps=new Set,Z._isComputed=!0,Z._subs=Y,Z._deleted=!1,Z.markDirty=()=>j=!0,Z.stop=()=>{Z._deleted=!0,Z._deps.forEach((B)=>B.delete(Z)),Y.clear()},A)A.cleanups.add(Z.stop);return()=>{if(j)Z();return O(Y),z}}let W=X;if(J)try{let Y=localStorage.getItem(J);if(Y!==null)W=JSON.parse(Y)}catch(Y){console.warn("SigPro: LocalStorage locked",Y)}let q=new Set;return(...Y)=>{if(Y.length){let z=typeof Y[0]==="function"?Y[0](W):Y[0];if(!Object.is(W,z)){if(W=z,J)localStorage.setItem(J,JSON.stringify(W));x(q)}}return O(q),W}},C=(X,J)=>{let W=Array.isArray(X),q=W?J:X,Y=W?X:null;if(typeof q!=="function")return()=>{};let z=A,j=()=>{if(j._deleted)return;j._deps.forEach((G)=>G.delete(j)),j._deps.clear(),j._cleanups.forEach((G)=>G()),j._cleanups.clear();let Z=R,B=A;R=j,A={cleanups:j._cleanups},j.depth=Z?Z.depth+1:0;try{if(W)R=null,q(),R=j,Y.forEach((G)=>typeof G==="function"&&G());else q()}finally{R=Z,A=B}};if(j._deps=new Set,j._cleanups=new Set,j._deleted=!1,j.stop=()=>{if(j._deleted)return;if(j._deleted=!0,P.delete(j),j._deps.forEach((Z)=>Z.delete(j)),j._cleanups.forEach((Z)=>Z()),z)z.cleanups.delete(j.stop)},z)z.cleanups.add(j.stop);return j(),j.stop},I=(X,J={},W=[])=>{if(J instanceof Node||Array.isArray(J)||typeof J!=="object")W=J,J={};let q=document.createElement(X),Y=(j,Z)=>(j==="src"||j==="href")&&String(Z).toLowerCase().includes("javascript:")?"#":Z;q._cleanups=new Set;for(let[j,Z]of Object.entries(J)){if(j==="ref"){typeof Z==="function"?Z(q):Z.current=q;continue}let B=typeof Z==="function";if(["INPUT","TEXTAREA","SELECT"].includes(q.tagName)&&(j==="value"||j==="checked")&&B){q._cleanups.add(C(()=>{let L=Z();if(q[j]!==L)q[j]=L}));let K=j==="checked"?"change":"input",Q=(L)=>Z(L.target[j]);q.addEventListener(K,Q),q._cleanups.add(()=>q.removeEventListener(K,Q))}else if(j.startsWith("on")){let K=j.slice(2).toLowerCase().split(".")[0],Q=(L)=>Z(L);q.addEventListener(K,Q),q._cleanups.add(()=>q.removeEventListener(K,Q))}else if(B)q._cleanups.add(C(()=>{let K=Y(j,Z());if(j==="class")q.className=K||"";else K==null?q.removeAttribute(j):q.setAttribute(j,K)}));else q.setAttribute(j,Y(j,Z))}let z=(j)=>{if(Array.isArray(j))return j.forEach(z);if(typeof j==="function"){let Z=document.createTextNode("");q.appendChild(Z);let B=[];q._cleanups.add(C(()=>{let G=j(),U=(Array.isArray(G)?G:[G]).map((K)=>K?._isRuntime?K.container:K instanceof Node?K:document.createTextNode(K??""));B.forEach((K)=>{V(K),K.remove()}),U.forEach((K)=>Z.parentNode?.insertBefore(K,Z)),B=U}))}else q.appendChild(j instanceof Node?j:document.createTextNode(j??""))};return z(W),q},T=(X,J,W=null)=>{let q=document.createTextNode(""),Y=I("div",{style:"display:contents"},[q]),z=null,j=null;return C(()=>{let Z=!!(typeof X==="function"?X():X);if(Z!==j){if(j=Z,z)z.destroy();let B=Z?J:W;if(B)z=_(()=>typeof B==="function"?B():B),Y.insertBefore(z.container,q)}}),Y};T.not=(X,J,W)=>T(()=>!(typeof X==="function"?X():X),J,W);var b=(X,J,W=(q,Y)=>Y)=>{let q=document.createTextNode(""),Y=I("div",{style:"display:contents"},[q]),z=new Map;return C(()=>{let j=(typeof X==="function"?X():X)||[],Z=new Set;j.forEach((B,G)=>{let U=W(B,G);Z.add(U);let K=z.get(U);if(!K)K=_(()=>J(B,G)),z.set(U,K);Y.insertBefore(K.container,q)}),z.forEach((B,G)=>{if(!Z.has(G)){if(B.destroy(),B.container&&B.container.parentNode)B.container.remove();z.delete(G)}})}),Y},H=(X)=>{let J=D(window.location.hash.replace(/^#/,"")||"/");window.addEventListener("hashchange",()=>J(window.location.hash.replace(/^#/,"")||"/"));let W=I("div",{class:"router-outlet"}),q=null;return C([J],async()=>{let Y=J(),z=X.find((j)=>{let Z=j.path.split("/").filter(Boolean),B=Y.split("/").filter(Boolean);return Z.length===B.length&&Z.every((G,U)=>G.startsWith(":")||G===B[U])})||X.find((j)=>j.path==="*");if(z){let j=z.component;if(typeof j==="function"&&j.toString().includes("import"))j=(await j()).default||await j();let Z={};if(z.path.split("/").filter(Boolean).forEach((B,G)=>{if(B.startsWith(":"))Z[B.slice(1)]=Y.split("/").filter(Boolean)[G]}),q)q.destroy();if(H.params)H.params(Z);q=_(()=>{try{return typeof j==="function"?j(Z):j}catch(B){return I("div",{class:"p-4 text-error"},"Error loading view")}}),W.appendChild(q.container)}}),W};H.params=D({});H.to=(X)=>window.location.hash=X.replace(/^#?\/?/,"#/");H.back=()=>window.history.back();H.path=()=>window.location.hash.replace(/^#/,"")||"/";var E=(X,J)=>{let W=typeof J==="string"?document.querySelector(J):J;if(!W)return;if(S.has(W))S.get(W).destroy();let q=_(typeof X==="function"?X:()=>X);return W.replaceChildren(q.container),S.set(W,q),q},h={$:D,$watch:C,$html:I,$if:T,$for:b,$router:H,$mount:E};if(typeof window<"u")((J)=>{Object.keys(J).forEach((q)=>{window[q]=J[q]}),"div span p h1 h2 h3 h4 h5 h6 br hr section article aside nav main header footer address ul ol li dl dt dd a em strong small i b u mark time sub sup pre code blockquote details summary dialog form label input textarea select button option fieldset legend table thead tbody tfoot tr th td caption img video audio canvas svg iframe picture source progress meter".split(/\s+/).forEach((q)=>{let Y=q.charAt(0).toUpperCase()+q.slice(1);if(!(Y in window))window[Y]=(z,j)=>I(q,z,j)}),window.SigPro=Object.freeze(J)})(h);})(); +(()=>{var{defineProperty:D,getOwnPropertyNames:$,getOwnPropertyDescriptor:w}=Object,k=Object.prototype.hasOwnProperty;var N=new WeakMap,y=(K)=>{var J=N.get(K),Z;if(J)return J;if(J=D({},"__esModule",{value:!0}),K&&typeof K==="object"||typeof K==="function")$(K).map((q)=>!k.call(J,q)&&D(J,q,{get:()=>K[q],enumerable:!(Z=w(K,q))||Z.enumerable}));return N.set(K,J),J};var g=(K,J)=>{for(var Z in J)D(K,Z,{get:J[Z],enumerable:!0,configurable:!0,set:(q)=>J[Z]=()=>q})};var m={};g(m,{$watch:()=>L,$router:()=>Q,$mount:()=>E,$if:()=>T,$html:()=>x,$for:()=>O,$:()=>_});var U=null,I=null,P=new Set,C=!1,M=new WeakMap,v=()=>{if(C)return;C=!0;while(P.size>0){let K=Array.from(P).sort((J,Z)=>(J.depth||0)-(Z.depth||0));P.clear();for(let J of K)if(!J._deleted)J()}C=!1},b=(K)=>{if(U&&!U._deleted)K.add(U),U._deps.add(K)},S=(K)=>{for(let J of K){if(J===U||J._deleted)continue;if(J._isComputed){if(J.markDirty(),J._subs)S(J._subs)}else P.add(J)}if(!C)queueMicrotask(v)},V=(K)=>{if(K._cleanups)K._cleanups.forEach((J)=>J()),K._cleanups.clear();K.childNodes?.forEach(V)},F=(K)=>{let J=new Set,Z=I,q=document.createElement("div");q.style.display="contents",I={cleanups:J};try{let W=K({onCleanup:(j)=>J.add(j)}),Y=(j)=>{if(!j)return;if(j._isRuntime)J.add(j.destroy),q.appendChild(j.container);else if(Array.isArray(j))j.forEach(Y);else q.appendChild(j instanceof Node?j:document.createTextNode(String(j)))};Y(W)}finally{I=Z}return{_isRuntime:!0,container:q,destroy:()=>{J.forEach((W)=>W()),V(q),q.remove()}}},_=(K,J=null)=>{if(typeof K==="function"){let W=new Set,Y,j=!0,X=()=>{if(X._deleted)return;X._deps.forEach((G)=>G.delete(X)),X._deps.clear();let z=U;U=X;try{let G=K();if(!Object.is(Y,G)||j)Y=G,j=!1,S(W)}finally{U=z}};if(X._deps=new Set,X._isComputed=!0,X._subs=W,X._deleted=!1,X.markDirty=()=>j=!0,X.stop=()=>{X._deleted=!0,X._deps.forEach((z)=>z.delete(X)),W.clear()},I)I.cleanups.add(X.stop);return()=>{if(j)X();return b(W),Y}}let Z=K;if(J)try{let W=localStorage.getItem(J);if(W!==null)Z=JSON.parse(W)}catch(W){console.warn("SigPro: LocalStorage locked",W)}let q=new Set;return(...W)=>{if(W.length){let Y=typeof W[0]==="function"?W[0](Z):W[0];if(!Object.is(Z,Y)){if(Z=Y,J)localStorage.setItem(J,JSON.stringify(Z));S(q)}}return b(q),Z}},L=(K,J)=>{let Z=Array.isArray(K),q=Z?J:K,W=Z?K:null;if(typeof q!=="function")return()=>{};let Y=I,j=()=>{if(j._deleted)return;j._deps.forEach((G)=>G.delete(j)),j._deps.clear(),j._cleanups.forEach((G)=>G()),j._cleanups.clear();let X=U,z=I;U=j,I={cleanups:j._cleanups},j.depth=X?X.depth+1:0;try{if(Z)U=null,q(),U=j,W.forEach((G)=>typeof G==="function"&&G());else q()}finally{U=X,I=z}};if(j._deps=new Set,j._cleanups=new Set,j._deleted=!1,j.stop=()=>{if(j._deleted)return;if(j._deleted=!0,P.delete(j),j._deps.forEach((X)=>X.delete(j)),j._cleanups.forEach((X)=>X()),Y)Y.cleanups.delete(j.stop)},Y)Y.cleanups.add(j.stop);return j(),j.stop},x=(K,J={},Z=[])=>{if(J instanceof Node||Array.isArray(J)||typeof J!=="object")Z=J,J={};let q=document.createElement(K),W=(j,X)=>(j==="src"||j==="href")&&String(X).toLowerCase().includes("javascript:")?"#":X;q._cleanups=new Set;for(let[j,X]of Object.entries(J)){if(j==="ref"){typeof X==="function"?X(q):X.current=q;continue}let z=typeof X==="function";if(["INPUT","TEXTAREA","SELECT"].includes(q.tagName)&&(j==="value"||j==="checked")&&z){q._cleanups.add(L(()=>{let H=X();if(q[j]!==H)q[j]=H}));let B=j==="checked"?"change":"input",A=(H)=>X(H.target[j]);q.addEventListener(B,A),q._cleanups.add(()=>q.removeEventListener(B,A))}else if(j.startsWith("on")){let B=j.slice(2).toLowerCase().split(".")[0],A=(H)=>X(H);q.addEventListener(B,A),q._cleanups.add(()=>q.removeEventListener(B,A))}else if(z)q._cleanups.add(L(()=>{let B=W(j,X());if(j==="class")q.className=B||"";else B==null?q.removeAttribute(j):q.setAttribute(j,B)}));else q.setAttribute(j,W(j,X))}let Y=(j)=>{if(Array.isArray(j))return j.forEach(Y);if(typeof j==="function"){let X=document.createTextNode("");q.appendChild(X);let z=[];q._cleanups.add(L(()=>{let G=j(),R=(Array.isArray(G)?G:[G]).map((B)=>B?._isRuntime?B.container:B instanceof Node?B:document.createTextNode(B??""));z.forEach((B)=>{V(B),B.remove()}),R.forEach((B)=>X.parentNode?.insertBefore(B,X)),z=R}))}else q.appendChild(j instanceof Node?j:document.createTextNode(j??""))};return Y(Z),q},T=(K,J,Z=null)=>{let q=document.createTextNode(""),W=x("div",{style:"display:contents"},[q]),Y=null,j=null;return L(()=>{let X=!!(typeof K==="function"?K():K);if(X!==j){if(j=X,Y)Y.destroy();let z=X?J:Z;if(z)Y=F(()=>typeof z==="function"?z():z),W.insertBefore(Y.container,q)}}),W};T.not=(K,J,Z)=>T(()=>!(typeof K==="function"?K():K),J,Z);var O=(K,J,Z)=>{let q=document.createTextNode(""),W=x("div",{style:"display:contents"},[q]),Y=new Map;return L(()=>{let j=(typeof K==="function"?K():K)||[],X=new Map,z=[];for(let R=0;RJ(B,R));else Y.delete(A);X.set(A,H),z.push(A)}Y.forEach((R)=>{R.destroy(),R.container.remove()});let G=q;for(let R=z.length-1;R>=0;R--){let B=X.get(z[R]);if(B.container.nextSibling!==G)W.insertBefore(B.container,G);G=B.container}Y=X}),W},Q=(K)=>{let J=_(window.location.hash.replace(/^#/,"")||"/");window.addEventListener("hashchange",()=>J(window.location.hash.replace(/^#/,"")||"/"));let Z=x("div",{class:"router-outlet"}),q=null;return L([J],async()=>{let W=J(),Y=K.find((j)=>{let X=j.path.split("/").filter(Boolean),z=W.split("/").filter(Boolean);return X.length===z.length&&X.every((G,R)=>G.startsWith(":")||G===z[R])})||K.find((j)=>j.path==="*");if(Y){let j=Y.component;if(typeof j==="function"&&j.toString().includes("import"))j=(await j()).default||await j();let X={};if(Y.path.split("/").filter(Boolean).forEach((z,G)=>{if(z.startsWith(":"))X[z.slice(1)]=W.split("/").filter(Boolean)[G]}),q)q.destroy();if(Q.params)Q.params(X);q=F(()=>{try{return typeof j==="function"?j(X):j}catch(z){return x("div",{class:"p-4 text-error"},"Error loading view")}}),Z.appendChild(q.container)}}),Z};Q.params=_({});Q.to=(K)=>window.location.hash=K.replace(/^#?\/?/,"#/");Q.back=()=>window.history.back();Q.path=()=>window.location.hash.replace(/^#/,"")||"/";var E=(K,J)=>{let Z=typeof J==="string"?document.querySelector(J):J;if(!Z)return;if(M.has(Z))M.get(Z).destroy();let q=F(typeof K==="function"?K:()=>K);return Z.replaceChildren(q.container),M.set(Z,q),q},h={$:_,$watch:L,$html:x,$if:T,$for:O,$router:Q,$mount:E};if(typeof window<"u")((J)=>{Object.keys(J).forEach((q)=>{window[q]=J[q]}),"div span p h1 h2 h3 h4 h5 h6 br hr section article aside nav main header footer address ul ol li dl dt dd a em strong small i b u mark time sub sup pre code blockquote details summary dialog form label input textarea select button option fieldset legend table thead tbody tfoot tr th td caption img video audio canvas svg iframe picture source progress meter".split(/\s+/).forEach((q)=>{let W=q.charAt(0).toUpperCase()+q.slice(1);if(!(W in window))window[W]=(Y,j)=>x(q,Y,j)}),window.SigPro=Object.freeze(J)})(h);})(); diff --git a/sigpro/index.js b/sigpro/index.js index 14e5668..fb6f106 100644 --- a/sigpro/index.js +++ b/sigpro/index.js @@ -307,37 +307,46 @@ $if.not = (condition, thenVal, otherwiseVal) => $if(() => !(typeof condition === * @returns {HTMLElement} A reactive container (display: contents). */ -const $for = (source, render, keyFn = (item, index) => index) => { +const $for = (source, render, keyFn) => { const marker = document.createTextNode(""); const container = $html("div", { style: "display:contents" }, [marker]); - const cache = new Map(); + let cache = new Map(); $watch(() => { const items = (typeof source === "function" ? source() : source) || []; - const newKeys = new Set(); - - items.forEach((item, index) => { - const key = keyFn(item, index); - newKeys.add(key); + const newCache = new Map(); + const newOrder = []; + for (let i = 0; i < items.length; i++) { + const item = items[i]; + const key = keyFn ? keyFn(item, i) : i; + let run = cache.get(key); if (!run) { - run = _view(() => render(item, index)); - cache.set(key, run); - } - - container.insertBefore(run.container, marker); - }); - - cache.forEach((run, key) => { - if (!newKeys.has(key)) { - run.destroy(); - if (run.container && run.container.parentNode) { - run.container.remove(); - } + run = _view(() => render(item, i)); + } else { cache.delete(key); } + + newCache.set(key, run); + newOrder.push(key); + } + + cache.forEach(run => { + run.destroy(); + run.container.remove(); }); + + let anchor = marker; + for (let i = newOrder.length - 1; i >= 0; i--) { + const run = newCache.get(newOrder[i]); + if (run.container.nextSibling !== anchor) { + container.insertBefore(run.container, anchor); + } + anchor = run.container; + } + + cache = newCache; }); return container;