From d4211fc2d3d7f269083c34323f4a93715e45d198 Mon Sep 17 00:00:00 2001 From: AI Engineer Date: Sun, 17 May 2026 16:48:29 +0800 Subject: [PATCH] release: v1.0.2 --- CHANGELOG.md | 8 + dist/state.js | 10 +- dist/state.min.js | 2 +- package-lock.json | 6 +- package.json | 2 +- src/core.js | 26 +- src/dom.js | 662 +++++++++++++++++++++++----------------------- src/observer.js | 1 + test/dom.test.js | 14 +- 9 files changed, 377 insertions(+), 354 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ce5a2e..85f8026 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # CHANGELOG +## v1.0.2 (2026-05-17) + +### 优化 +- **稳定性**: `src/dom.js` 中的 `bind` 属性逻辑改为使用 `setTimeout` 确保 DOM 更新完成后同步值,提高双向绑定在高频操作下的可靠性。 +- **健壮性**: `src/observer.js` 中 `NewState` 增加对已观察对象的检查,防止重复包装导致的性能损耗。 +- **功能**: `src/dom.js` 中 `_translator` 支持 `{key}` 模板替换,增强国际化组件的灵活性。 +- **清理**: 全面规范代码缩进(Tab),移除 `src/core.js` 中的调试注释。 + ## v1.0.1 (2026-05-15) ### 优化 diff --git a/dist/state.js b/dist/state.js index 7eb2fa1..3a0b888 100644 --- a/dist/state.js +++ b/dist/state.js @@ -14,6 +14,7 @@ function onNotifyUpdate(fn) { _updateBindingFn = fn; } function NewState(defaults = {}, getter = null, setter = null) { + if (defaults && defaults.__watch) return defaults; const _defaults = {}; const _stateMappings = /* @__PURE__ */ new Map(); const _watchers = /* @__PURE__ */ new Map(); @@ -101,7 +102,12 @@ function _returnCode(code, vars, thisObj, extendVars) { if (code.includes("${")) return _runCode("return `" + code + "`", vars, thisObj, extendVars); else return _runCode("return " + code, vars, thisObj, extendVars); } -let _translator = (text) => text; +let _translator = (text, args) => { + if (!text || typeof text !== "string") return text; + return text.replace(/\{(.+?)\}/g, (match, key) => { + return args.hasOwnProperty(key) ? args[key] : match; + }); +}; const SetTranslator = (fn) => _translator = fn; const _translate = (text) => { if (!text || typeof text !== "string") return text; @@ -248,7 +254,7 @@ function _updateBinding(binding) { } 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") { - Promise.resolve().then(() => { + setTimeout(() => { if (node.value !== String(result ?? "")) node.value = result; }); } else if (node.isContentEditable) { diff --git a/dist/state.min.js b/dist/state.min.js index dc936d0..e787450 100644 --- a/dist/state.min.js +++ b/dist/state.min.js @@ -1 +1 @@ -var e;let t=null,n=null,r=null,s=0;function a(e){t=e}function o(e){n=e}function i(e={},a=null,o=null){const i={},d=new Map,c=new Map,l=(e,t)=>{c.has(e)||c.set(e,new Set),t?c.get(e).add(t):c.get(e).clear()},h=(e,t)=>{c.has(e)||c.set(e,new Set),c.get(e).delete(t)},u=a||(e=>i[e]),f=o||((e,t)=>i[e]=t);return Object.assign(i,e),new Proxy(i,{get(e,n){if("__watch"===n)return l;if("__unwatch"===n)return h;if(t){d.has(n)||d.set(n,new Set);const e=d.get(n);e.add(t),t._sets||(t._sets=new Set),t._sets.add(e)}return u(n)},set(e,t,a){if(u(t)!==a&&f(t,a),c.has(t)&&c.get(t).forEach(n=>{const r=n(a);void 0!==r&&(a=r,e[t]=a)}),c.has(null)&&c.get(null).forEach(e=>e(a)),d.has(t)){if(s>100)return console.error("Recursive update detected at key:",t),!0;s++;try{const e=d.get(t);for(const t of e)t.node.isConnected?n!==t.node&&r&&r(t):e.delete(t)}finally{s--}}return!0}})}const d=(e,t)=>t?e.querySelector(t):document.querySelector(e),c=(e,t)=>t?e.querySelectorAll(t):document.querySelectorAll(e);let l=!1;function h(e){l=e}function u(e,t,n,r){const s=[...Object.keys(r||{}),...Object.keys(t||{})],a=[...Object.values(r||{}),...Object.values(t||{})];s.push(e);try{return new Function(...s).apply(n,a)}catch(s){return l||console.error(s,r,[e,r,t,n]),null}}let f=e=>e;const b=e=>f=e,m=e=>e&&"string"==typeof e?e.replace(/\{#(.+?)#\}/g,(e,t)=>{const n=t.split("||").map(e=>e.trim()),r=n[0],s={};if(n.length>1){const e=r.match(/\{(.+?)\}/g);e&&e.forEach((e,t)=>{const r=e.substring(1,e.length-1);s[r]=n[t+1]||""})}return f(r,s)}):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)}}function p(e){e._renderedNodes&&e._renderedNodes.forEach(e=>{e.forEach(e=>{e.remove(),e._renderedNodes&&p(e)})})}function g(e){const t=e.node,n=e.tpl,r=e.exp;a(e);let s=r?n?(o=n,i={thisNode:t},d=t._thisObj||t,c=t._ref||null,o.includes("${")?u("return `"+o+"`",i,d,c):u("return "+o,i,d,c)):null:n;var o,i,d,c;if(a(null),e.prop){const n=e.prop;let r=t;for(let e=0;e{t.parentNode.insertBefore(e,t),e._ref={...t._ref}}),t._renderedNodes=[t._children]):(p(t),t._renderedNodes=[]);else if("each"===r)if(s&&"object"==typeof s){const e=t.getAttribute("as")||"item",n=t.getAttribute("index")||"index";let r,a;if(s instanceof Map)r=Array.from(s.keys()),a=e=>s.get(e);else if("function"==typeof s[Symbol.iterator]){const e=Array.isArray(s)?s:Array.from(s);r=new Array(e.length);for(let t=0;te[t]}else r=Object.keys(s),a=e=>s[e];for(r.forEach((r,s)=>{const o=a(r);if(t._renderedNodes&&s{t._ref[n]=r,t._ref[e]=o,v(t)});else{const s=[];t._renderedNodes||(t._renderedNodes=[]),t._children.forEach(a=>{const i=a.cloneNode(!0);i._ref={...t._ref},i._ref[n]=r,i._ref[e]=o,i._thisObj=t._thisObj,t.parentNode.insertBefore(i,t),s.push(i)}),t._renderedNodes.push(s)}});t._renderedNodes&&t._renderedNodes.length>r.length;)t._renderedNodes[t._renderedNodes.length-1].forEach(e=>{p(e),e.remove()}),t._renderedNodes.pop()}else p(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||s||(u(`${n} = []`,{thisNode:t},t._thisObj||t,t._ref||{}),s=[]),t._checkboxMultiMode=s instanceof Array;const e=s instanceof Array?s.includes(t.value):!!s;t.checked!==e&&(t.checked=e)}else"radio"===t.type?t.checked!==(t.value===String(s??""))&&(t.checked=t.value===String(s??"")):"value"in t&&"file"!==t.type?Promise.resolve().then(()=>{t.value!==String(s??"")&&(t.value=s)}):t.isContentEditable&&t.innerHTML!==String(s??"")&&(t.innerHTML=s);t.dispatchEvent(new CustomEvent("bind",{bubbles:!1,detail:s}))}else["checked","disabled","readonly"].includes(r)&&(s=!!s),"boolean"==typeof s?s?t.setAttribute(r,""):t.removeAttribute(r):void 0!==s&&("string"!=typeof s&&(s=JSON.stringify(s)),"text"===r?t.textContent=s??"":"html"===r?t.innerHTML=s??"":"IMG"===t.tagName&&"src"===r&&s.includes(".svg")?t.setAttribute("_src",s??""):t.setAttribute(r,s??""))}}r=e=>g(e);const _=e=>{e.node._bindings||(e.node._bindings=[]),e.node._bindings.push(e),g(e)},v=(e,t={})=>{if(3===e.nodeType){const t=m(e.textContent);return void(t!==e.textContent&&(e.textContent=t))}if(1!==e.nodeType)return;if(Array.from(e.attributes).forEach(e=>{if(!e.name.startsWith("$")&&!e.name.startsWith("st-")&&!e.name.startsWith(".")){const t=m(e.value);t!==e.value&&(e.value=t)}}),"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),t._ref=e._ref,void(e=t)}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),void 0===t.thisObj){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}:{}}void 0===t.extendVars&&(t.extendVars={}),void 0!==e._ref&&(Object.assign(e._ref,t.extendVars),t.extendVars={...e._ref}),((e,t)=>{if(O.exists(e.tagName)&&!e._componentInitialized&&(e._componentInitialized=!0,j(e.tagName,e,t),c(e,"[slot-id]").forEach(e=>e.removeAttribute("slot-id")),e._thisObj||(e._thisObj=e)),e._bindings)return e._bindings.forEach(e=>g(e)),void(e._hasOnUpdate&&e.dispatchEvent(new Event("update",{bubbles:!1})));let n=[];var r;"TEMPLATE"===e.tagName?(e._children=[...e.content.childNodes],e._renderedNodes=[],e.hasAttribute("$if")?n.push(e.getAttributeNode("$if")):e.hasAttribute("$each")?n.push(e.getAttributeNode("$each")):e.hasAttribute("st-if")?n.push(e.getAttributeNode("st-if")):e.hasAttribute("st-each")&&n.push(e.getAttributeNode("st-each"))):n=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||{}),n.forEach(n=>{const r=n.name.startsWith("$")||n.name.startsWith("st-"),s=r?n.name.slice(n.name.startsWith("$")?1:3):n.name;let a=n.value;var i,d;e.removeAttribute(n.name),s.startsWith(".")?_({node:e,prop:s.split("."),tpl:a,exp:r}):s.startsWith("on")?("onupdate"===s&&(e._hasOnUpdate=!0),"onload"!==s||["BODY","IMG","IFRAME"].includes(e.tagName)||(e._hasOnLoad=!0),"onunload"!==s||["BODY","IMG","IFRAME"].includes(e.tagName)||(e._hasOnUnload=!0),i=e,d=t.thisObj,i.addEventListener(s.slice(2),e=>{u(a,{event:e,thisNode:i,...e.detail||{}},d||i,i._ref||{})})):("bind"===s?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;o(e),h(!0),"checkbox"===e.type&&e._checkboxMultiMode?u(`!!checked ? (!${a}.includes(val) && ${a}.push(val)) : (index = ${a}.indexOf(val), index > -1 && ${a}.splice(index, 1))`,{val:e.value,checked:r,thisNode:e},t.thisObj||e,e._ref||{}):u(`${a} = val`,{val:r,thisNode:e},t.thisObj||e,e._ref||{}),h(!1),o(null)}):"text"!==s||a||(a=e.textContent,e.textContent=""),a&&(a=m(a),_({node:e,attr:s,tpl:a,exp:r})))}),(e._hasOnLoad||e._componentInitialized)&&(r=e,Promise.resolve().then(()=>r.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||[]];t.extendVars=e._ref||t.extendVars,n.forEach(n=>v(n,{thisObj:t.thisObj,extendVars:{...e._ref}}))},A=e=>{1===e.nodeType&&(e._hasOnUnload&&e.dispatchEvent(new Event("unload",{bubbles:!1})),e._bindings&&e._bindings.forEach(e=>{e._sets&&(e._sets.forEach(t=>t.delete(e)),e._sets.clear())}),e.childNodes&&e.childNodes.forEach(e=>A(e)))},E=v,y=new Map,N=[],O={getTemplate:e=>document.querySelector(`template[component="${e.toUpperCase()}"]`),register:(e,t,n=null,...r)=>{y.set(e.toUpperCase(),t),"loading"!==document.readyState?O._addTemplate(e,n,r):N.push([e,n,r])},exists:e=>y.has(e.toUpperCase()),getSetupFunction:e=>y.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:()=>{N.forEach(([e,t,n])=>O._addTemplate(e,t,n)),N.length=0}};function T(e,t,n,r={}){e.attributes&&Array.from(e.attributes).forEach(e=>"class"!==e.name&&t.setAttribute(e.name,e.value)),e.classList&&t.classList.add(...e.classList),Array.from(e.childNodes).forEach(e=>t.appendChild(e)),e.tagName&&O.exists(e.tagName)&&j(e.tagName,t,n,r)}function j(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=O.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=i(t.state||{});const o=O.getTemplate(e);if(o){const e=o.content.cloneNode(!0);if(e.childNodes.length){T(e.children[0],t,n,r),c(t,"[slot-id]").forEach(e=>{const t=e.getAttribute("slot-id"),s=a[t];s&&(e.removeAttribute("slot-id"),e.innerHTML="","TEMPLATE"===s.tagName?Array.from(s.content.childNodes).forEach(t=>e.appendChild(t.cloneNode(!0))):T(s,e,n,r))})}}if(s)try{s(t)}catch(t){console.error("Error in component setupFunc for",e,t)}}const x={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=>x.base64(e).replace(/[+/=]/g,e=>({"+":"-","/":"","=":""}[e])),unurlbase64:e=>x.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=x;let w=new URLSearchParams((null==(e=window.location.hash)?void 0:e.substring(1))||"");const S=i({},e=>x.safeJson(w.get(e)),(e,t)=>{const n=w.get(e),r=void 0===t?void 0:JSON.stringify(t);n===r||null===n&&void 0===r||(void 0===t?w.delete(e):w.set(e,r),window.location.hash="#"+w.toString())});"undefined"!=typeof window&&window.addEventListener("hashchange",()=>{var e;const t=w;w=new URLSearchParams((null==(e=window.location.hash)?void 0:e.substring(1))||""),w.forEach((e,n)=>{t.get(n)!==e&&(S[n]=x.safeJson(e))}),t.forEach((e,t)=>{void 0===w.get(t)&&(S[t]=void 0)})});const M=i({},e=>x.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=S,globalThis.LocalStorage=M,"undefined"!=typeof document){const e=()=>{O._initPending(),new MutationObserver(e=>{e.forEach(e=>{e.addedNodes.forEach(e=>{e.isConnected&&v(e)}),e.removedNodes.forEach(e=>A(e))})}).observe(document.documentElement,{childList:!0,subtree:!0});const e=document.documentElement;e.hasAttribute("$data-bs-theme")||e.hasAttribute("data-bs-theme")||e.setAttribute("$data-bs-theme","LocalStorage.darkMode?'dark':'light'"),v(document.documentElement)};"loading"!==document.readyState?e():document.addEventListener("DOMContentLoaded",e,!0)}export{d as $,c as $$,O as Component,S as Hash,M as LocalStorage,i as NewState,E as RefreshState,b as SetTranslator,x as Util,v as _scanTree,A as _unbindTree}; +var e;let t=null,n=null,r=null,s=0;function a(e){t=e}function o(e){n=e}function i(e={},a=null,o=null){if(e&&e.__watch)return e;const i={},d=new Map,c=new Map,l=(e,t)=>{c.has(e)||c.set(e,new Set),t?c.get(e).add(t):c.get(e).clear()},h=(e,t)=>{c.has(e)||c.set(e,new Set),c.get(e).delete(t)},u=a||(e=>i[e]),f=o||((e,t)=>i[e]=t);return Object.assign(i,e),new Proxy(i,{get(e,n){if("__watch"===n)return l;if("__unwatch"===n)return h;if(t){d.has(n)||d.set(n,new Set);const e=d.get(n);e.add(t),t._sets||(t._sets=new Set),t._sets.add(e)}return u(n)},set(e,t,a){if(u(t)!==a&&f(t,a),c.has(t)&&c.get(t).forEach(n=>{const r=n(a);void 0!==r&&(a=r,e[t]=a)}),c.has(null)&&c.get(null).forEach(e=>e(a)),d.has(t)){if(s>100)return console.error("Recursive update detected at key:",t),!0;s++;try{const e=d.get(t);for(const t of e)t.node.isConnected?n!==t.node&&r&&r(t):e.delete(t)}finally{s--}}return!0}})}const d=(e,t)=>t?e.querySelector(t):document.querySelector(e),c=(e,t)=>t?e.querySelectorAll(t):document.querySelectorAll(e);let l=!1;function h(e){l=e}function u(e,t,n,r){const s=[...Object.keys(r||{}),...Object.keys(t||{})],a=[...Object.values(r||{}),...Object.values(t||{})];s.push(e);try{return new Function(...s).apply(n,a)}catch(s){return l||console.error(s,r,[e,r,t,n]),null}}let f=(e,t)=>e&&"string"==typeof e?e.replace(/\{(.+?)\}/g,(e,n)=>t.hasOwnProperty(n)?t[n]:e):e;const b=e=>f=e,m=e=>e&&"string"==typeof e?e.replace(/\{#(.+?)#\}/g,(e,t)=>{const n=t.split("||").map(e=>e.trim()),r=n[0],s={};if(n.length>1){const e=r.match(/\{(.+?)\}/g);e&&e.forEach((e,t)=>{const r=e.substring(1,e.length-1);s[r]=n[t+1]||""})}return f(r,s)}):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)}}function p(e){e._renderedNodes&&e._renderedNodes.forEach(e=>{e.forEach(e=>{e.remove(),e._renderedNodes&&p(e)})})}function g(e){const t=e.node,n=e.tpl,r=e.exp;a(e);let s=r?n?(o=n,i={thisNode:t},d=t._thisObj||t,c=t._ref||null,o.includes("${")?u("return `"+o+"`",i,d,c):u("return "+o,i,d,c)):null:n;var o,i,d,c;if(a(null),e.prop){const n=e.prop;let r=t;for(let e=0;e{t.parentNode.insertBefore(e,t),e._ref={...t._ref}}),t._renderedNodes=[t._children]):(p(t),t._renderedNodes=[]);else if("each"===r)if(s&&"object"==typeof s){const e=t.getAttribute("as")||"item",n=t.getAttribute("index")||"index";let r,a;if(s instanceof Map)r=Array.from(s.keys()),a=e=>s.get(e);else if("function"==typeof s[Symbol.iterator]){const e=Array.isArray(s)?s:Array.from(s);r=new Array(e.length);for(let t=0;te[t]}else r=Object.keys(s),a=e=>s[e];for(r.forEach((r,s)=>{const o=a(r);if(t._renderedNodes&&s{t._ref[n]=r,t._ref[e]=o,v(t)});else{const s=[];t._renderedNodes||(t._renderedNodes=[]),t._children.forEach(a=>{const i=a.cloneNode(!0);i._ref={...t._ref},i._ref[n]=r,i._ref[e]=o,i._thisObj=t._thisObj,t.parentNode.insertBefore(i,t),s.push(i)}),t._renderedNodes.push(s)}});t._renderedNodes&&t._renderedNodes.length>r.length;)t._renderedNodes[t._renderedNodes.length-1].forEach(e=>{p(e),e.remove()}),t._renderedNodes.pop()}else p(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||s||(u(`${n} = []`,{thisNode:t},t._thisObj||t,t._ref||{}),s=[]),t._checkboxMultiMode=s instanceof Array;const e=s instanceof Array?s.includes(t.value):!!s;t.checked!==e&&(t.checked=e)}else"radio"===t.type?t.checked!==(t.value===String(s??""))&&(t.checked=t.value===String(s??"")):"value"in t&&"file"!==t.type?setTimeout(()=>{t.value!==String(s??"")&&(t.value=s)}):t.isContentEditable&&t.innerHTML!==String(s??"")&&(t.innerHTML=s);t.dispatchEvent(new CustomEvent("bind",{bubbles:!1,detail:s}))}else["checked","disabled","readonly"].includes(r)&&(s=!!s),"boolean"==typeof s?s?t.setAttribute(r,""):t.removeAttribute(r):void 0!==s&&("string"!=typeof s&&(s=JSON.stringify(s)),"text"===r?t.textContent=s??"":"html"===r?t.innerHTML=s??"":"IMG"===t.tagName&&"src"===r&&s.includes(".svg")?t.setAttribute("_src",s??""):t.setAttribute(r,s??""))}}r=e=>g(e);const _=e=>{e.node._bindings||(e.node._bindings=[]),e.node._bindings.push(e),g(e)},v=(e,t={})=>{if(3===e.nodeType){const t=m(e.textContent);return void(t!==e.textContent&&(e.textContent=t))}if(1!==e.nodeType)return;if(Array.from(e.attributes).forEach(e=>{if(!e.name.startsWith("$")&&!e.name.startsWith("st-")&&!e.name.startsWith(".")){const t=m(e.value);t!==e.value&&(e.value=t)}}),"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),t._ref=e._ref,void(e=t)}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),void 0===t.thisObj){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}:{}}void 0===t.extendVars&&(t.extendVars={}),void 0!==e._ref&&(Object.assign(e._ref,t.extendVars),t.extendVars={...e._ref}),((e,t)=>{if(O.exists(e.tagName)&&!e._componentInitialized&&(e._componentInitialized=!0,j(e.tagName,e,t),c(e,"[slot-id]").forEach(e=>e.removeAttribute("slot-id")),e._thisObj||(e._thisObj=e)),e._bindings)return e._bindings.forEach(e=>g(e)),void(e._hasOnUpdate&&e.dispatchEvent(new Event("update",{bubbles:!1})));let n=[];var r;"TEMPLATE"===e.tagName?(e._children=[...e.content.childNodes],e._renderedNodes=[],e.hasAttribute("$if")?n.push(e.getAttributeNode("$if")):e.hasAttribute("$each")?n.push(e.getAttributeNode("$each")):e.hasAttribute("st-if")?n.push(e.getAttributeNode("st-if")):e.hasAttribute("st-each")&&n.push(e.getAttributeNode("st-each"))):n=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||{}),n.forEach(n=>{const r=n.name.startsWith("$")||n.name.startsWith("st-"),s=r?n.name.slice(n.name.startsWith("$")?1:3):n.name;let a=n.value;var i,d;e.removeAttribute(n.name),s.startsWith(".")?_({node:e,prop:s.split("."),tpl:a,exp:r}):s.startsWith("on")?("onupdate"===s&&(e._hasOnUpdate=!0),"onload"!==s||["BODY","IMG","IFRAME"].includes(e.tagName)||(e._hasOnLoad=!0),"onunload"!==s||["BODY","IMG","IFRAME"].includes(e.tagName)||(e._hasOnUnload=!0),i=e,d=t.thisObj,i.addEventListener(s.slice(2),e=>{u(a,{event:e,thisNode:i,...e.detail||{}},d||i,i._ref||{})})):("bind"===s?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;o(e),h(!0),"checkbox"===e.type&&e._checkboxMultiMode?u(`!!checked ? (!${a}.includes(val) && ${a}.push(val)) : (index = ${a}.indexOf(val), index > -1 && ${a}.splice(index, 1))`,{val:e.value,checked:r,thisNode:e},t.thisObj||e,e._ref||{}):u(`${a} = val`,{val:r,thisNode:e},t.thisObj||e,e._ref||{}),h(!1),o(null)}):"text"!==s||a||(a=e.textContent,e.textContent=""),a&&(a=m(a),_({node:e,attr:s,tpl:a,exp:r})))}),(e._hasOnLoad||e._componentInitialized)&&(r=e,Promise.resolve().then(()=>r.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||[]];t.extendVars=e._ref||t.extendVars,n.forEach(n=>v(n,{thisObj:t.thisObj,extendVars:{...e._ref}}))},A=e=>{1===e.nodeType&&(e._hasOnUnload&&e.dispatchEvent(new Event("unload",{bubbles:!1})),e._bindings&&e._bindings.forEach(e=>{e._sets&&(e._sets.forEach(t=>t.delete(e)),e._sets.clear())}),e.childNodes&&e.childNodes.forEach(e=>A(e)))},E=v,y=new Map,N=[],O={getTemplate:e=>document.querySelector(`template[component="${e.toUpperCase()}"]`),register:(e,t,n=null,...r)=>{y.set(e.toUpperCase(),t),"loading"!==document.readyState?O._addTemplate(e,n,r):N.push([e,n,r])},exists:e=>y.has(e.toUpperCase()),getSetupFunction:e=>y.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:()=>{N.forEach(([e,t,n])=>O._addTemplate(e,t,n)),N.length=0}};function T(e,t,n,r={}){e.attributes&&Array.from(e.attributes).forEach(e=>"class"!==e.name&&t.setAttribute(e.name,e.value)),e.classList&&t.classList.add(...e.classList),Array.from(e.childNodes).forEach(e=>t.appendChild(e)),e.tagName&&O.exists(e.tagName)&&j(e.tagName,t,n,r)}function j(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=O.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=i(t.state||{});const o=O.getTemplate(e);if(o){const e=o.content.cloneNode(!0);if(e.childNodes.length){T(e.children[0],t,n,r),c(t,"[slot-id]").forEach(e=>{const t=e.getAttribute("slot-id"),s=a[t];s&&(e.removeAttribute("slot-id"),e.innerHTML="","TEMPLATE"===s.tagName?Array.from(s.content.childNodes).forEach(t=>e.appendChild(t.cloneNode(!0))):T(s,e,n,r))})}}if(s)try{s(t)}catch(t){console.error("Error in component setupFunc for",e,t)}}const x={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=>x.base64(e).replace(/[+/=]/g,e=>({"+":"-","/":"","=":""}[e])),unurlbase64:e=>x.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=x;let w=new URLSearchParams((null==(e=window.location.hash)?void 0:e.substring(1))||"");const S=i({},e=>x.safeJson(w.get(e)),(e,t)=>{const n=w.get(e),r=void 0===t?void 0:JSON.stringify(t);n===r||null===n&&void 0===r||(void 0===t?w.delete(e):w.set(e,r),window.location.hash="#"+w.toString())});"undefined"!=typeof window&&window.addEventListener("hashchange",()=>{var e;const t=w;w=new URLSearchParams((null==(e=window.location.hash)?void 0:e.substring(1))||""),w.forEach((e,n)=>{t.get(n)!==e&&(S[n]=x.safeJson(e))}),t.forEach((e,t)=>{void 0===w.get(t)&&(S[t]=void 0)})});const M=i({},e=>x.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=S,globalThis.LocalStorage=M,"undefined"!=typeof document){const e=()=>{O._initPending(),new MutationObserver(e=>{e.forEach(e=>{e.addedNodes.forEach(e=>{e.isConnected&&v(e)}),e.removedNodes.forEach(e=>A(e))})}).observe(document.documentElement,{childList:!0,subtree:!0});const e=document.documentElement;e.hasAttribute("$data-bs-theme")||e.hasAttribute("data-bs-theme")||e.setAttribute("$data-bs-theme","LocalStorage.darkMode?'dark':'light'"),v(document.documentElement)};"loading"!==document.readyState?e():document.addEventListener("DOMContentLoaded",e,!0)}export{d as $,c as $$,O as Component,S as Hash,M as LocalStorage,i as NewState,E as RefreshState,b as SetTranslator,x as Util,v as _scanTree,A as _unbindTree}; diff --git a/package-lock.json b/package-lock.json index de57f6c..f626015 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@web/state", - "version": "1.0.0", + "version": "1.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@web/state", - "version": "1.0.0", + "version": "1.0.2", "devDependencies": { "@playwright/test": "^1.40.0", "@rollup/plugin-terser": "^1.0.0", @@ -1135,4 +1135,4 @@ } } } -} \ No newline at end of file +} diff --git a/package.json b/package.json index 9e3c79d..655ab21 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@web/state", - "version": "1.0.1", + "version": "1.0.2", "type": "module", "main": "dist/state.js", diff --git a/src/core.js b/src/core.js index 4d49663..96a2046 100644 --- a/src/core.js +++ b/src/core.js @@ -2,23 +2,23 @@ let _disableRunCodeError = false; export function setDisableRunCodeError(value) { - _disableRunCodeError = value; + _disableRunCodeError = value; } export function _runCode(code, vars, thisObj, extendVars) { - const argKeys = [...Object.keys(extendVars || {}), ...Object.keys(vars || {})]; - const argValues = [...Object.values(extendVars || {}), ...Object.values(vars || {})]; - argKeys.push(code); - try { - const r = new Function(...argKeys).apply(thisObj, argValues); - return r; - } catch (e) { - if (!_disableRunCodeError) console.error(e, extendVars, [code, extendVars, vars, thisObj]); - return null; - } + const argKeys = [...Object.keys(extendVars || {}), ...Object.keys(vars || {})]; + const argValues = [...Object.values(extendVars || {}), ...Object.values(vars || {})]; + argKeys.push(code); + try { + const r = new Function(...argKeys).apply(thisObj, argValues); + return r; + } catch (e) { + if (!_disableRunCodeError) console.error(e, extendVars, [code, extendVars, vars, thisObj]); + return null; + } } export function _returnCode(code, vars, thisObj, extendVars) { - if (code.includes('${')) return _runCode('return `' + code + '`', vars, thisObj, extendVars); - else return _runCode('return ' + code, vars, thisObj, extendVars); + if (code.includes('${')) return _runCode('return `' + code + '`', vars, thisObj, extendVars); + else return _runCode('return ' + code, vars, thisObj, extendVars); } diff --git a/src/dom.js b/src/dom.js index cc483e5..928270b 100644 --- a/src/dom.js +++ b/src/dom.js @@ -4,36 +4,41 @@ import { getActiveBinding, setActiveBinding, getNoWriteBack, setNoWriteBack, New import { Component, _makeComponent, _mergeNode } from './component.js'; import { $, $$ } from './dom-utils.js'; -let _translator = (text) => text; +let _translator = (text, args) => { + if (!text || typeof text !== 'string') return text; + return text.replace(/\{(.+?)\}/g, (match, key) => { + return args.hasOwnProperty(key) ? args[key] : match; + }) +}; export const SetTranslator = (fn) => _translator = fn; const _translate = (text) => { - if (!text || typeof text !== 'string') return text; - return text.replace(/\{#(.+?)#\}/g, (m, content) => { - const parts = content.split('||').map(s => s.trim()); - const rawText = parts[0]; - const args = {}; - if (parts.length > 1) { - const matches = rawText.match(/\{(.+?)\}/g); - if (matches) matches.forEach((match, i) => { - const key = match.substring(1, match.length - 1); - args[key] = parts[i + 1] || ''; - }); - } - return _translator(rawText, args); - }); + if (!text || typeof text !== 'string') return text; + return text.replace(/\{#(.+?)#\}/g, (m, content) => { + const parts = content.split('||').map(s => s.trim()); + const rawText = parts[0]; + const args = {}; + if (parts.length > 1) { + const matches = rawText.match(/\{(.+?)\}/g); + if (matches) matches.forEach((match, i) => { + const key = match.substring(1, match.length - 1); + args[key] = parts[i + 1] || ''; + }); + } + return _translator(rawText, 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); - }; - } + 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 { $, $$ }; @@ -41,334 +46,335 @@ 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); - }); - }); + 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; - const tpl = binding.tpl; - const exp = binding.exp; - - setActiveBinding(binding); - let result = exp ? (tpl ? _returnCode(tpl, { thisNode: node }, node._thisObj || node, node._ref || null) : null) : tpl; - setActiveBinding(null); + const node = binding.node; + const tpl = binding.tpl; + const exp = binding.exp; - 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 resultIsObject = typeof result === 'object' && result != null && !Array.isArray(result); - const lk = prop[prop.length - 1]; - if (lk) { - if (resultIsObject && 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 (resultIsObject && typeof o === 'object') { - 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 => { - node.parentNode.insertBefore(child, node); - child._ref = { ...node._ref }; - }); - node._renderedNodes = [node._children]; - } - } 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(child); - }); - } else { - const newNodes = []; - if (!node._renderedNodes) node._renderedNodes = []; - node._children.forEach(child => { - const cloned = child.cloneNode(true); - cloned._ref = { ...node._ref }; - cloned._ref[indexName] = k; - cloned._ref[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[node._renderedNodes.length - 1].forEach(child => { - _clearRenderedNodes(child); - child.remove(); - }); - node._renderedNodes.pop(); - } - } else { - _clearRenderedNodes(node); - node._renderedNodes = []; - } - } else if (attr === 'bind') { - if (['INPUT', 'SELECT', 'TEXTAREA'].includes(node.tagName)) { - if (!node.hasAttribute('autocomplete')) node.setAttribute('autocomplete', 'off'); - } - if (node.type === 'checkbox') { - if (node.value !== 'on' && !result) { - _runCode(`${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') { - Promise.resolve().then(() => { - 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 ?? ''); - } - } - } - } + setActiveBinding(binding); + let result = exp ? (tpl ? _returnCode(tpl, { thisNode: node }, node._thisObj || node, node._ref || null) : null) : tpl; + 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 resultIsObject = typeof result === 'object' && result != null && !Array.isArray(result); + const lk = prop[prop.length - 1]; + if (lk) { + if (resultIsObject && 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 (resultIsObject && typeof o === 'object') { + 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 => { + node.parentNode.insertBefore(child, node); + child._ref = { ...node._ref }; + }); + node._renderedNodes = [node._children]; + } + } 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(child); + }); + } else { + const newNodes = []; + if (!node._renderedNodes) node._renderedNodes = []; + node._children.forEach(child => { + const cloned = child.cloneNode(true); + cloned._ref = { ...node._ref }; + cloned._ref[indexName] = k; + cloned._ref[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[node._renderedNodes.length - 1].forEach(child => { + _clearRenderedNodes(child); + child.remove(); + }); + node._renderedNodes.pop(); + } + } else { + _clearRenderedNodes(node); + node._renderedNodes = []; + } + } else if (attr === 'bind') { + if (['INPUT', 'SELECT', 'TEXTAREA'].includes(node.tagName)) { + if (!node.hasAttribute('autocomplete')) node.setAttribute('autocomplete', 'off'); + } + if (node.type === 'checkbox') { + if (node.value !== 'on' && !result) { + _runCode(`${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') { + // 这里必须用宏任务,微任务不足以确保DOM更新完成 + 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(binding); - _updateBinding(binding); + if (!binding.node._bindings) binding.node._bindings = []; + binding.node._bindings.push(binding); + _updateBinding(binding); }; export const _parseNode = (node, scanObj) => { - if (Component.exists(node.tagName) && !node._componentInitialized) { - node._componentInitialized = true; - _makeComponent(node.tagName, node, scanObj); - $$(node, '[slot-id]').forEach(placeholder => placeholder.removeAttribute('slot-id')); - if (!node._thisObj) node._thisObj = node; - } + if (Component.exists(node.tagName) && !node._componentInitialized) { + node._componentInitialized = true; + _makeComponent(node.tagName, node, scanObj); + $$(node, '[slot-id]').forEach(placeholder => placeholder.removeAttribute('slot-id')); + if (!node._thisObj) node._thisObj = node; + } - if (node._bindings) { - node._bindings.forEach(binding => _updateBinding(binding)); - if (node._hasOnUpdate) node.dispatchEvent(new Event('update', { bubbles: false })); - return; - } + if (node._bindings) { + node._bindings.forEach(binding => _updateBinding(binding)); + if (node._hasOnUpdate) node.dispatchEvent(new Event('update', { bubbles: false })); + return; + } - let attrs = []; - if (node.tagName === 'TEMPLATE') { - node._children = [...node.content.childNodes]; - node._renderedNodes = []; - if (node.hasAttribute('$if')) attrs.push(node.getAttributeNode('$if')); - else if (node.hasAttribute('$each')) attrs.push(node.getAttributeNode('$each')); - else if (node.hasAttribute('st-if')) attrs.push(node.getAttributeNode('st-if')); - else if (node.hasAttribute('st-each')) attrs.push(node.getAttributeNode('st-each')); - } else { - attrs = Array.from(node.attributes).filter(attr => (attr.name.startsWith('$') || attr.name.startsWith('st-')) && !['$if', '$each', 'st-if', 'st-each'].includes(attr.name) || attr.name.includes('.')); - } + let attrs = []; + if (node.tagName === 'TEMPLATE') { + node._children = [...node.content.childNodes]; + node._renderedNodes = []; + if (node.hasAttribute('$if')) attrs.push(node.getAttributeNode('$if')); + else if (node.hasAttribute('$each')) attrs.push(node.getAttributeNode('$each')); + else if (node.hasAttribute('st-if')) attrs.push(node.getAttributeNode('st-if')); + else if (node.hasAttribute('st-each')) attrs.push(node.getAttributeNode('st-each')); + } else { + attrs = Array.from(node.attributes).filter(attr => (attr.name.startsWith('$') || attr.name.startsWith('st-')) && !['$if', '$each', 'st-if', 'st-each'].includes(attr.name) || attr.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 || {}; + 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 || {}; - 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: node, prop: realAttrName.split('.'), tpl, exp }); - } else { - if (realAttrName.startsWith('on')) { - if (realAttrName === 'onupdate') node._hasOnUpdate = true; - if (realAttrName === 'onload' && !['BODY', 'IMG', 'IFRAME'].includes(node.tagName)) node._hasOnLoad = true; - if (realAttrName === 'onunload' && !['BODY', 'IMG', 'IFRAME'].includes(node.tagName)) node._hasOnUnload = true; - ((node, thisObj) => { - node.addEventListener(realAttrName.slice(2), (e) => { - _runCode(tpl, { event: e, thisNode: node, ...(e.detail || {}) }, thisObj || node, node._ref || {}); - }); - })(node, scanObj.thisObj); - } 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: node, attr: realAttrName, tpl, exp }); - } - } - } - }); - if (node._hasOnLoad || node._componentInitialized) { - (node => { - Promise.resolve().then(() => node.dispatchEvent(new Event('load', { bubbles: false }))); - })(node); - } - if (node._hasOnUpdate) node.dispatchEvent(new Event('update', { bubbles: false })); - if (node._thisObj) scanObj.thisObj = node._thisObj; + 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: node, prop: realAttrName.split('.'), tpl, exp }); + } else { + if (realAttrName.startsWith('on')) { + if (realAttrName === 'onupdate') node._hasOnUpdate = true; + if (realAttrName === 'onload' && !['BODY', 'IMG', 'IFRAME'].includes(node.tagName)) node._hasOnLoad = true; + if (realAttrName === 'onunload' && !['BODY', 'IMG', 'IFRAME'].includes(node.tagName)) node._hasOnUnload = true; + ((node, thisObj) => { + node.addEventListener(realAttrName.slice(2), (e) => { + _runCode(tpl, { event: e, thisNode: node, ...(e.detail || {}) }, thisObj || node, node._ref || {}); + }); + })(node, scanObj.thisObj); + } 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: node, attr: realAttrName, tpl, exp }); + } + } + } + }); + if (node._hasOnLoad || node._componentInitialized) { + (node => { + Promise.resolve().then(() => node.dispatchEvent(new Event('load', { bubbles: false }))); + })(node); + } + 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) { - const translated = _translate(node.textContent); - if (translated !== node.textContent) node.textContent = translated; - return; - } - if (node.nodeType !== 1) return; + if (node.nodeType === 3) { + const translated = _translate(node.textContent); + if (translated !== node.textContent) node.textContent = translated; + return; + } + if (node.nodeType !== 1) return; - 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; - } - }); + 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; + } + }); - 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; - node = template; - 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 !== '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; + node = template; + 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.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; - if (scanObj.thisObj === undefined) { - 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 === undefined) scanObj.extendVars = {}; - if (node._ref !== undefined) { - Object.assign(node._ref, scanObj.extendVars); - scanObj.extendVars = { ...node._ref }; - } - _parseNode(node, scanObj); - const nodes = [...(node.childNodes || [])]; - scanObj.extendVars = node._ref || scanObj.extendVars; - nodes.forEach(child => _scanTree(child, { thisObj: scanObj.thisObj, extendVars: { ...node._ref } })); + if (node._thisObj !== undefined) scanObj.thisObj = node._thisObj || null; + if (scanObj.thisObj === undefined) { + 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 === undefined) scanObj.extendVars = {}; + if (node._ref !== undefined) { + Object.assign(node._ref, scanObj.extendVars); + scanObj.extendVars = { ...node._ref }; + } + _parseNode(node, scanObj); + const nodes = [...(node.childNodes || [])]; + scanObj.extendVars = node._ref || scanObj.extendVars; + nodes.forEach(child => _scanTree(child, { thisObj: scanObj.thisObj, extendVars: { ...node._ref } })); }; export const _unbindTree = (node) => { - if (node.nodeType !== 1) return; - if (node._hasOnUnload) node.dispatchEvent(new Event('unload', { bubbles: false })); - if (node._bindings) { - node._bindings.forEach(binding => { - if (binding._sets) { - binding._sets.forEach(set => set.delete(binding)); - binding._sets.clear(); - } - }); - } - node.childNodes && node.childNodes.forEach(child => _unbindTree(child)); + if (node.nodeType !== 1) return; + if (node._hasOnUnload) node.dispatchEvent(new Event('unload', { bubbles: false })); + if (node._bindings) { + node._bindings.forEach(binding => { + if (binding._sets) { + binding._sets.forEach(set => set.delete(binding)); + binding._sets.clear(); + } + }); + } + node.childNodes && node.childNodes.forEach(child => _unbindTree(child)); }; export const RefreshState = _scanTree; diff --git a/src/observer.js b/src/observer.js index 613c406..3358405 100644 --- a/src/observer.js +++ b/src/observer.js @@ -15,6 +15,7 @@ export function onNotifyUpdate(fn) { } export function NewState(defaults = {}, getter = null, setter = null) { + if (defaults && defaults.__watch) return defaults const _defaults = {}; const _stateMappings = new Map(); const _watchers = new Map(); diff --git a/test/dom.test.js b/test/dom.test.js index b9eab5a..fe5da61 100644 --- a/test/dom.test.js +++ b/test/dom.test.js @@ -26,12 +26,14 @@ export async function testDom() { RefreshState(document.documentElement); if ($('#if-content')) throw new Error('$if failed: should be hidden'); + const wait = () => new Promise(r => setTimeout(r, 10)); + state.show = true; - await Promise.resolve(); + await wait(); if (!$('#if-content') || $('#if-content').textContent !== 'Visible') throw new Error('$if failed: should be visible'); state.show = false; - await Promise.resolve(); + await wait(); if ($('#if-content')) throw new Error('$if failed: should be hidden again'); // 3. $each directive (Index-based reuse) @@ -44,7 +46,7 @@ export async function testDom() { `; state.items = [{ name: 'A' }, { name: 'B' }]; RefreshState(document.documentElement); - await Promise.resolve(); // Wait for MutationObserver + await wait(); // Wait for MutationObserver let items = $$('#list-test li'); console.log('$each items length:', items.length); if (items.length > 0) console.log('$each first item text:', items[0].textContent); @@ -52,13 +54,13 @@ export async function testDom() { const firstNode = items[0]; state.items = [{ name: 'A-mod' }, { name: 'B' }, { name: 'C' }]; - await Promise.resolve(); + await wait(); items = $$('#list-test li'); if (items.length !== 3 || items[0].textContent !== 'A-mod') throw new Error('$each update failed'); if (items[0] !== firstNode) throw new Error('$each reuse failed: should reuse existing DOM nodes'); state.items = [{ name: 'C' }]; - await Promise.resolve(); + await wait(); items = $$('#list-test li'); if (items.length !== 1 || items[0].textContent !== 'C') throw new Error('$each removal failed'); @@ -66,7 +68,7 @@ export async function testDom() { document.body.innerHTML = ``; state.val = 'initial'; RefreshState(document.documentElement); - await Promise.resolve(); + await wait(); const input = $('#input-test'); if (input.value !== 'initial') throw new Error('$bind initial value failed');