From aaeed2efc85a5638c1f41dc716b6d3e7afac1881 Mon Sep 17 00:00:00 2001 From: AI Engineer Date: Thu, 11 Jun 2026 13:20:39 +0800 Subject: [PATCH] chore: release v1.0.18 with refactored architecture By: AICoder --- dist/state.js | 295 +++++++++-------- dist/state.min.js | 2 +- dist/state.min.mjs | 1 - dist/state.mjs | 659 -------------------------------------- package-lock.json | 4 +- package.json | 2 +- src/component.js | 87 ----- src/core.js | 80 +++-- src/dom-utils.js | 3 - src/dom.badbak.for2exp.js | 366 --------------------- src/dom.js | 353 -------------------- src/dom.js.keyed.bak | 352 -------------------- src/engine.js | 451 ++++++++++++++++++++++++++ src/globals.js | 41 --- src/index.js | 39 ++- src/observer.js | 9 +- src/utils.js | 16 +- vite.config.js | 19 +- 18 files changed, 705 insertions(+), 2074 deletions(-) delete mode 100644 dist/state.min.mjs delete mode 100644 dist/state.mjs delete mode 100644 src/component.js delete mode 100644 src/dom-utils.js delete mode 100644 src/dom.badbak.for2exp.js delete mode 100644 src/dom.js delete mode 100644 src/dom.js.keyed.bak create mode 100644 src/engine.js delete mode 100644 src/globals.js diff --git a/dist/state.js b/dist/state.js index e8629da..19de524 100644 --- a/dist/state.js +++ b/dist/state.js @@ -1,8 +1,72 @@ (function(global, factory) { - typeof exports === "object" && typeof module !== "undefined" ? factory(exports) : typeof define === "function" && define.amd ? define(["exports"], factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self, factory(global.ApigoState = {})); + typeof exports === "object" && typeof module !== "undefined" ? factory(exports) : typeof define === "function" && define.amd ? define(["exports"], factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self, factory(global.ApigoState = global.ApigoState || {})); })(this, function(exports2) { "use strict"; - var _a; + var _a, _b; + const Util = { + clone: globalThis.structuredClone || ((obj) => JSON.parse(JSON.stringify(obj))), + base64: (str) => btoa(String.fromCharCode(...new TextEncoder().encode(str))), + unbase64: (str) => new TextDecoder().decode(Uint8Array.from(atob(str), (c) => c.charCodeAt(0))), + urlbase64: (str) => Util.base64(str).replace(/[+/=]/g, (m) => ({ "+": "-", "/": "", "=": "" })[m]), + unurlbase64: (str) => Util.unbase64(str.replace(/[-_.]/g, (m) => ({ "-": "+", "_": "/", ".": "=" })[m]).padEnd(Math.ceil(str.length / 4) * 4, "=")), + safeJson: (str) => { + try { + return JSON.parse(str); + } catch { + return null; + } + }, + updateDefaults: (obj, defaults) => { + for (const k in defaults) if (obj[k] === void 0) obj[k] = defaults[k]; + }, + copyFunction: (toObj, fromObj, ...funcNames) => { + funcNames.forEach((name) => toObj[name] = fromObj[name].bind(fromObj)); + }, + getFunctionBody: (fn) => { + const code = fn.toString(); + return code.slice(code.indexOf("{") + 1, code.lastIndexOf("}")).trim(); + }, + makeDom: (html) => { + if (html.includes(">\n")) html = html.replace(/>\s+<").trim(); + const node = document.createElement("div"); + node.innerHTML = html; + return node.children[0]; + }, + newAvg: () => { + let total = 0, count = 0, avg = 0; + return { + add: (v) => { + total += v; + count++; + return avg = total / count; + }, + get: () => avg, + clear: () => { + total = 0, count = 0, avg = 0; + } + }; + }, + newTimeCount: () => { + let startTime = 0, total = 0, count = 0; + return { + start: () => startTime = (/* @__PURE__ */ new Date()).getTime(), + end: () => { + const endTime = (/* @__PURE__ */ new Date()).getTime(); + const left = endTime - startTime; + startTime = endTime; + total += left; + count++; + return left; + }, + avg: () => total / count + }; + } + }; + const $ = (a, b) => b ? a.querySelector(b) : document.querySelector(a); + const $$ = (a, b) => b ? a.querySelectorAll(b) : document.querySelectorAll(a); + globalThis.Util = Util; + globalThis.$ = $; + globalThis.$$ = $$; let __activeBinding = null; let __noWriteBack = null; const _setActiveBinding = (val) => __activeBinding = val; @@ -70,14 +134,67 @@ } }); } - const $ = (a, b) => b ? a.querySelector(b) : document.querySelector(a); - const $$ = (a, b) => b ? a.querySelectorAll(b) : document.querySelectorAll(a); + globalThis.NewState = NewState; + let _hashParams = new URLSearchParams(typeof globalThis !== "undefined" ? ((_b = (_a = globalThis.location) == null ? void 0 : _a.hash) == null ? void 0 : _b.substring(1)) || "" : ""); + const Hash = NewState({}, (k) => Util.safeJson(_hashParams.get(k)), (k, v) => { + const oldStr = _hashParams.get(k); + const newStr = v === void 0 ? void 0 : JSON.stringify(v); + if (oldStr === newStr || oldStr === null && newStr === void 0) return; + v === void 0 ? _hashParams.delete(k) : _hashParams.set(k, newStr); + globalThis.location.hash = "#" + _hashParams.toString(); + }); + if (typeof globalThis !== "undefined") { + globalThis.addEventListener("hashchange", () => { + var _a2; + const newParams = new URLSearchParams(((_a2 = globalThis.location.hash) == null ? void 0 : _a2.substring(1)) || ""); + const keys = /* @__PURE__ */ new Set([..._hashParams.keys(), ...newParams.keys()]); + _hashParams = newParams; + keys.forEach((k) => Hash[k] = Hash[k]); + }); + } + const LocalStorage = NewState({}, (k) => Util.safeJson(localStorage.getItem(k)), (k, v) => { + const oldStr = localStorage.getItem(k); + const newStr = v === void 0 ? void 0 : JSON.stringify(v); + if (oldStr === newStr || oldStr === null && newStr === void 0) return; + v === void 0 ? localStorage.removeItem(k) : localStorage.setItem(k, newStr); + }); + const State = NewState({ + exitBlocks: 0 + }); + globalThis.Hash = Hash; + globalThis.LocalStorage = LocalStorage; + globalThis.State = State; + let _disableRunCodeError = false; + const setDisableRunCodeError = (value) => { + _disableRunCodeError = value; + }; + const _fnCache = /* @__PURE__ */ new Map(); + function _runCode(code, vars, thisObj, extendVars) { + const allVars = { ...extendVars || {}, ...vars || {} }; + const argKeys = Object.keys(allVars); + const argValues = Object.values(allVars); + const cacheKey = code + argKeys.join(","); + try { + let fn = _fnCache.get(cacheKey); + if (!fn) { + fn = new Function("Hash", "LocalStorage", "State", ...argKeys, code); + _fnCache.set(cacheKey, fn); + } + return fn.apply(thisObj, [globalThis.Hash, globalThis.LocalStorage, globalThis.State, ...argValues]); + } catch (e) { + if (!_disableRunCodeError) console.error(e, extendVars, [code, extendVars, vars, thisObj]); + return null; + } + } + function _returnCode(code, vars, thisObj, extendVars) { + if (code.includes("${")) return _runCode("return `" + code + "`", vars, thisObj, extendVars); + else return _runCode("return " + code, vars, thisObj, extendVars); + } const _components = /* @__PURE__ */ new Map(); const _pendingTemplates = []; const Component = { getTemplate: (name) => document.querySelector(`template[component="${name.toUpperCase()}"]`), register: (name, setupFunc, templateNode = null, ...globalNodes) => { - console.log("Component.register:", name.toUpperCase()); _components.set(name.toUpperCase(), setupFunc); if (document.readyState !== "loading") Component._addTemplate(name, templateNode, globalNodes); else _pendingTemplates.push([name, templateNode, globalNodes]); @@ -111,7 +228,9 @@ }); } to.classList.add(...from.classList); - Array.from(from.childNodes).forEach((child) => to.appendChild(child)); + const target = to.tagName === "TEMPLATE" ? to.content : to; + const sourceNodes = from.tagName === "TEMPLATE" ? from.content.childNodes : from.childNodes; + Array.from(sourceNodes).forEach((child) => target.appendChild(child)); if (from.tagName && Component.exists(from.tagName)) _makeComponent(from.tagName, to, scanObj, exists); } function _makeComponent(name, node, scanObj, exists = {}) { @@ -152,32 +271,6 @@ } if (componentFunc) componentFunc(node); } - let _disableRunCodeError = false; - function setDisableRunCodeError(value) { - _disableRunCodeError = value; - } - const _fnCache = /* @__PURE__ */ new Map(); - function _runCode(code, vars, thisObj, extendVars) { - const allVars = { ...extendVars || {}, ...vars || {} }; - const argKeys = Object.keys(allVars); - const argValues = Object.values(allVars); - const cacheKey = code + argKeys.join(","); - try { - let fn = _fnCache.get(cacheKey); - if (!fn) { - fn = new Function("Hash", "LocalStorage", "State", ...argKeys, code); - _fnCache.set(cacheKey, fn); - } - return fn.apply(thisObj, [globalThis.Hash, globalThis.LocalStorage, globalThis.State, ...argValues]); - } catch (e) { - if (!_disableRunCodeError) console.error(e, extendVars, [code, extendVars, vars, thisObj]); - return null; - } - } - 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, args) => { if (!text || typeof text !== "string") return text; return text.replace(/\{(.+?)\}/g, (match, key) => args.hasOwnProperty(key) ? args[key] : match); @@ -352,12 +445,12 @@ } } } - const _initBinding = (binding) => { + function _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); - }; - const _parseNode = (node, scanObj) => { + } + function _parseNode(node, scanObj) { if (node._bindings) { node._states = /* @__PURE__ */ new Set(); node._bindings.forEach((b) => _updateBinding({ node, ...b })); @@ -370,7 +463,7 @@ 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."); + if (scanObj.thisObj && 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("."); @@ -396,7 +489,7 @@ } 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 && scanObj.thisObj && 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 = /* @__PURE__ */ new Set(); @@ -438,8 +531,15 @@ 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; - }; + } 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) => { @@ -520,116 +620,14 @@ }); node.childNodes && node.childNodes.forEach((child) => _unbindTree(child)); }; - const _unsafeRefreshState = _scanTree; - const Util = { - clone: window.structuredClone || ((obj) => JSON.parse(JSON.stringify(obj))), - base64: (str) => btoa(String.fromCharCode(...new TextEncoder().encode(str))), - unbase64: (str) => new TextDecoder().decode(Uint8Array.from(atob(str), (c) => c.charCodeAt(0))), - urlbase64: (str) => Util.base64(str).replace(/[+/=]/g, (m) => ({ "+": "-", "/": "", "=": "" })[m]), - unurlbase64: (str) => Util.unbase64(str.replace(/[-_.]/g, (m) => ({ "-": "+", "_": "/", ".": "=" })[m]).padEnd(Math.ceil(str.length / 4) * 4, "=")), - safeJson: (str) => { - try { - return JSON.parse(str); - } catch { - return null; - } - }, - updateDefaults: (obj, defaults) => { - for (const k in defaults) if (obj[k] === void 0) obj[k] = defaults[k]; - }, - copyFunction: (toObj, fromObj, ...funcNames) => { - funcNames.forEach((name) => toObj[name] = fromObj[name].bind(fromObj)); - }, - getFunctionBody: (fn) => { - const code = fn.toString(); - return code.slice(code.indexOf("{") + 1, code.lastIndexOf("}")).trim(); - }, - makeDom: (html) => { - if (html.includes(">\n")) html = html.replace(/>\s+<").trim(); - const node = document.createElement("div"); - node.innerHTML = html; - return node.children[0]; - }, - newAvg: () => { - let total = 0, count = 0, avg = 0; - return { - add: (v) => { - total += v; - count++; - return avg = total / count; - }, - get: () => avg, - clear: () => { - total = 0, count = 0, avg = 0; - } - }; - }, - newTimeCount: () => { - let startTime = 0, total = 0, count = 0; - return { - start: () => startTime = (/* @__PURE__ */ new Date()).getTime(), - end: () => { - const endTime = (/* @__PURE__ */ new Date()).getTime(); - const left = endTime - startTime; - startTime = endTime; - total += left; - count++; - return left; - }, - avg: () => total / count - }; - } - }; - globalThis.Util = Util; - let _hashParams = new URLSearchParams(((_a = window.location.hash) == null ? void 0 : _a.substring(1)) || ""); - const Hash = NewState({}, (k) => Util.safeJson(_hashParams.get(k)), (k, v) => { - const oldStr = _hashParams.get(k); - const newStr = v === void 0 ? void 0 : JSON.stringify(v); - if (oldStr === newStr || oldStr === null && newStr === void 0) return; - v === void 0 ? _hashParams.delete(k) : _hashParams.set(k, newStr); - window.location.hash = "#" + _hashParams.toString(); - }); - if (typeof window !== "undefined") { - window.addEventListener("hashchange", () => { - var _a2; - const newParams = new URLSearchParams(((_a2 = window.location.hash) == null ? void 0 : _a2.substring(1)) || ""); - const keys = /* @__PURE__ */ new Set([..._hashParams.keys(), ...newParams.keys()]); - _hashParams = newParams; - keys.forEach((k) => Hash[k] = Hash[k]); - }); - } - const LocalStorage = NewState({}, (k) => Util.safeJson(localStorage.getItem(k)), (k, v) => { - const oldStr = localStorage.getItem(k); - const newStr = v === void 0 ? void 0 : JSON.stringify(v); - if (oldStr === newStr || oldStr === null && newStr === void 0) return; - v === void 0 ? localStorage.removeItem(k) : localStorage.setItem(k, newStr); - }); - const State = NewState({ - exitBlocks: 0 - }); - globalThis.Hash = Hash; - globalThis.LocalStorage = LocalStorage; - globalThis.State = State; - const ApigoState = { - NewState, - Component, - $, - $$, - RefreshState: _unsafeRefreshState, - SetTranslator, - _scanTree, - _unbindTree, - Util, - Hash, - LocalStorage, - State - }; - if (typeof window !== "undefined") { - window.ApigoState = ApigoState; - } + globalThis.Component = Component; + globalThis.SetTranslator = SetTranslator; + globalThis.__unsafeRefreshState = _scanTree; if (typeof document !== "undefined") { const init = () => { - Component._initPending(); + if (globalThis.Component && globalThis.Component._initPending) { + globalThis.Component._initPending(); + } const htmlNode = document.documentElement; if (!htmlNode.hasAttribute("$data-bs-theme") && !htmlNode.hasAttribute("data-bs-theme")) { htmlNode.setAttribute("$data-bs-theme", "LocalStorage.darkMode?'dark':'light'"); @@ -647,17 +645,16 @@ if (document.readyState !== "loading") init(); else document.addEventListener("DOMContentLoaded", init, true); } + const __unsafeRefreshState = _scanTree; exports2.$ = $; exports2.$$ = $$; exports2.Component = Component; exports2.Hash = Hash; exports2.LocalStorage = LocalStorage; exports2.NewState = NewState; - exports2.RefreshState = _unsafeRefreshState; exports2.SetTranslator = SetTranslator; exports2.State = State; exports2.Util = Util; - exports2._scanTree = _scanTree; - exports2._unbindTree = _unbindTree; + exports2.__unsafeRefreshState = __unsafeRefreshState; Object.defineProperty(exports2, Symbol.toStringTag, { value: "Module" }); }); diff --git a/dist/state.min.js b/dist/state.min.js index b665339..15077da 100644 --- a/dist/state.min.js +++ b/dist/state.min.js @@ -1 +1 @@ -!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).ApigoState={})}(this,function(e){"use strict";var t;let n=null,s=null;const a=e=>n=e,r=e=>s=e,o=new Set;function i(e={},t=null,a=null){const r={},i=new Map,l=new Map,d=(e,t)=>(l.has(e)||l.set(e,new Set),t?l.get(e).add(t):l.get(e).clear(),()=>l.get(e).delete(t)),c=(e,t)=>{l.has(e)&&l.set(e,new Set),l.get(e).delete(t)},h=t||(e=>r[e]),u=a||((e,t)=>r[e]=t);return Object.assign(r,e),new Proxy(r,{get:(e,t)=>"__watch"===t?d:"__unwatch"===t?c:"__isProxy"===t||(n&&(i.has(t)||i.set(t,new Set),i.get(t).add(n),n.node._states||(n.node._states=new Set),n.node._states.add(i)),h(t)),set(e,t,n){if(h(t)!==n&&u(t,n),l.has(t)&&l.get(t).forEach(s=>{const a=s(n);void 0!==a&&(n=a,e[t]=n)}),l.has(null)&&l.get(null).forEach(e=>e(n)),i.has(t)){const e=i.get(t);for(const t of e)t.node.isConnected?s!==t.node&&o.forEach(e=>e(t)):e.delete(t)}return!0}})}const l=(e,t)=>t?e.querySelector(t):document.querySelector(e),d=(e,t)=>t?e.querySelectorAll(t):document.querySelectorAll(e),c=new Map,h=[],u={getTemplate:e=>document.querySelector(`template[component="${e.toUpperCase()}"]`),register:(e,t,n=null,...s)=>{console.log("Component.register:",e.toUpperCase()),c.set(e.toUpperCase(),t),"loading"!==document.readyState?u._addTemplate(e,n,s):h.push([e,n,s])},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:()=>{h.forEach(([e,t,n])=>u._addTemplate(e,t,n)),h.length=0}};function f(e,t,n,s={}){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),Array.from(e.childNodes).forEach(e=>t.appendChild(e)),e.tagName&&u.exists(e.tagName)&&b(e.tagName,t,n,s)}function b(e,t,n,s={}){if(s[e])return;s[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 a=u.getSetupFunction(e),r={};Array.from(t.childNodes).forEach(e=>{e.nodeType===Node.ELEMENT_NODE&&e.hasAttribute("slot")&&(r[e.getAttribute("slot")]=e,e.removeAttribute("slot"))}),t.innerHTML="",t.state=i(t.state||{});const o=u.getTemplate(e);if(o){const e=o.content.cloneNode(!0);if(e.childNodes.length){const a=e.children[0];a&&f(a,t,n,s),d(t,"[slot-id]").forEach(e=>{const t=e.getAttribute("slot-id");r[t]&&(e.removeAttribute("slot-id"),e.innerHTML="",f(r[t],e,n,s))})}}a&&a(t)}let p=!1;function m(e){p=e}const g=new Map;function _(e,t,n,s){const a={...s||{},...t||{}},r=Object.keys(a),o=Object.values(a),i=e+r.join(",");try{let t=g.get(i);return t||(t=new Function("Hash","LocalStorage","State",...r,e),g.set(i,t)),t.apply(n,[globalThis.Hash,globalThis.LocalStorage,globalThis.State,...o])}catch(a){return p||console.error(a,s,[e,s,t,n]),null}}function y(e,t,n,s){return e.includes("${")?_("return `"+e+"`",t,n,s):_("return "+e,t,n,s)}let A=(e,t)=>e&&"string"==typeof e?e.replace(/\{(.+?)\}/g,(e,n)=>t.hasOwnProperty(n)?t[n]:e):e;const v=e=>A=e,E=e=>e&&"string"==typeof e&&e.includes("{#")?e.replace(/\{#(.+?)#\}/g,(e,t)=>{const n=t.split("||").map(e=>e.trim()),s={};if(n.length>1){const e=n[0].match(/\{(.+?)\}/g);e&&e.forEach((e,t)=>s[e.substring(1,e.length-1)]=n[t+1]||"")}return A(n[0],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)}}var N;function O(e){e._renderedNodes&&e._renderedNodes.forEach(e=>e.forEach(e=>{e.remove(),e._renderedNodes&&O(e)}))}function S(e){const t=e.node;if(!t.isConnected&&"TEMPLATE"!==t.tagName)return;a(e);let n=e.exp?e.tpl?y(e.tpl,{thisNode:t},t._thisObj||t,t._ref||null):null:e.tpl;if(2===e.exp&&"string"==typeof n)try{n=y(n,{thisNode:t},t._thisObj||t,t._ref||null)}catch(e){}if(a(null),e.prop){const s=e.prop;let a=t;for(let e=0;e{t.parentNode.insertBefore(e,t),e._ref={...t._ref},e._thisObj=t._thisObj}),t._renderedNodes=[t._children]):(O(t),t._renderedNodes=[]);else if("each"===s)if(n&&"object"==typeof n){const e=t.getAttribute("as")||"item",s=t.getAttribute("index")||"index",a=t.getAttribute("key");let r,o;if(n instanceof Map)r=Array.from(n.keys()),o=e=>n.get(e);else if("function"==typeof n[Symbol.iterator]){const e=Array.isArray(n)?n:Array.from(n);r=new Array(e.length);for(let t=0;te[t]}else r=Object.keys(n),o=e=>n[e];t._keyedNodes||(t._keyedNodes=new Map);const i=new Map,l=[];r.forEach((n,r)=>{const d=o(n),c=a?d&&"object"==typeof d?d[a]:d:n,h=null==c||i.has(c)?`st_key_${r}`:c;let u=t._keyedNodes.get(h);u?(t._keyedNodes.delete(h),u.forEach(t=>{t._ref[s]=n,t._ref[e]=d,w(t)})):(u=[],t._children.forEach(a=>{const r=a.cloneNode(!0);r._ref={...t._ref,[s]:n,[e]:d},r._thisObj=t._thisObj,t.parentNode.insertBefore(r,t),u.push(r)})),i.set(h,u),l.push(u)}),t._keyedNodes.forEach(e=>e.forEach(e=>{O(e),e.remove()})),t._keyedNodes=i,t._renderedNodes=l}else O(t),t._renderedNodes=[];else if("bind"===s){if(["INPUT","SELECT","TEXTAREA"].includes(t.tagName)&&!t.hasAttribute("autocomplete")&&t.setAttribute("autocomplete","off"),"checkbox"===t.type){"on"===t.value||n||(_(`${e.tpl} = []`,{thisNode:t},t._thisObj||t,t._ref||{}),n=[]),t._checkboxMultiMode=n instanceof Array;const s=n instanceof Array?n.includes(t.value):!!n;t.checked!==s&&(t.checked=s)}else"radio"===t.type?t.checked!==(t.value===String(n??""))&&(t.checked=t.value===String(n??"")):"value"in t&&"file"!==t.type?Promise.resolve().then(()=>{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(s)&&(n=!!n),"boolean"==typeof n?n?t.setAttribute(s,""):t.removeAttribute(s):void 0!==n&&("string"!=typeof n&&(n=JSON.stringify(n)),"text"===s?t.textContent=n??"":"html"===s?t.innerHTML=n??"":"IMG"===t.tagName&&"src"===s&&n.includes(".svg")?t.setAttribute("_src",n??""):t.setAttribute(s,n??""))}}N=e=>S(e),o.add(N);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}),S(e)},w=(e,t={})=>{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=E(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 n=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(t=>{n.setAttribute(t.name,t.value),e.removeAttribute(t.name)}),e.parentNode.insertBefore(n,e),n.content.appendChild(e),n._ref=e._ref,void w(n,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)),s=n[n.length-1];t.setAttribute(s.name,s.value),e.removeAttribute(s.name),"$each"!==s.name&&"st-each"!==s.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)=>{if(e._bindings)return e._states=new Set,e._bindings.forEach(t=>S({node:e,...t})),void(e._hasOnUpdate&&e.dispatchEvent(new Event("update",{bubbles:!1})));u.exists(e.tagName)&&!e._componentInitialized&&(Array.from(e.attributes).forEach(n=>{var s;if(n.name.startsWith("$.")){const a=n.name.slice(2);let r=E(n.value);r.includes("this.")&&(r=r.replace(/\bthis\./g,"this.parent."));const o=y(r,{thisNode:e},{parent:t.thisObj||e},e._ref||{});let i=e;const l=a.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=[]));let n=[];"TEMPLATE"===e.tagName?["$if","$each","st-if","st-each"].forEach(t=>e.hasAttribute(t)&&n.push(e.getAttributeNode(t))):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||{}),e._states=new Set,n.forEach(n=>{let s=0;n.name.startsWith("$$")||n.name.startsWith("st-st-")?s=2:(n.name.startsWith("$")||n.name.startsWith("st-"))&&(s=1);const a=2===s?n.name.startsWith("$$")?n.name.slice(2):n.name.slice(6):1===s?n.name.startsWith("$")?n.name.slice(1):n.name.slice(3):n.name;let o=n.value;if(e.removeAttribute(n.name),a.startsWith("."))T({node:e,prop:a.split("."),tpl:o,exp:s});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=>_(o,{event:n,thisNode:e,...n.detail||{}},t.thisObj||e,e._ref||{}))}else"bind"===a?e.addEventListener(["textarea","text","password"].includes(e.type||"text")||e.isContentEditable?"input":"change",n=>{let s=e.isContentEditable?n.target.innerHTML:"checkbox"===e.type?n.target.checked:n.target.files||n.target.value||n.detail;r(e),m(!0),"checkbox"===e.type&&e._checkboxMultiMode?_(`!!checked ? (!${o}.includes(val) && ${o}.push(val)) : (index = ${o}.indexOf(val), index > -1 && ${o}.splice(index, 1))`,{val:e.value,checked:s,thisNode:e},t.thisObj||e,e._ref||{}):_(`${o} = val`,{val:s,thisNode:e},t.thisObj||e,e._ref||{}),m(!1),r(null)}):"text"!==a||o||(o=e.textContent,e.textContent=""),o&&(o=E(o),T({node:e,attr:a,tpl:o,exp:s}))}),(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});[...e.childNodes||[]].forEach(n=>w(n,{thisObj:t.thisObj,extendVars:{...e._ref}}))},j=e=>{1===e.nodeType&&(e._hasOnUnload&&e.dispatchEvent(new Event("unload",{bubbles:!1})),e._states&&e._states.forEach(t=>{for(const[n,s]of t)for(const t of s)t.node===e&&s.delete(t)}),e.childNodes&&e.childNodes.forEach(e=>j(e)))},x=w,$={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=>$.base64(e).replace(/[+/=]/g,e=>({"+":"-","/":"","=":""}[e])),unurlbase64:e=>$.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:s=>(e+=s,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 s=(new Date).getTime(),a=s-e;return e=s,t+=a,n++,a},avg:()=>t/n}}};globalThis.Util=$;let M=new URLSearchParams((null==(t=window.location.hash)?void 0:t.substring(1))||"");const L=i({},e=>$.safeJson(M.get(e)),(e,t)=>{const n=M.get(e),s=void 0===t?void 0:JSON.stringify(t);n===s||null===n&&void 0===s||(void 0===t?M.delete(e):M.set(e,s),window.location.hash="#"+M.toString())});"undefined"!=typeof window&&window.addEventListener("hashchange",()=>{var e;const t=new URLSearchParams((null==(e=window.location.hash)?void 0:e.substring(1))||""),n=new Set([...M.keys(),...t.keys()]);M=t,n.forEach(e=>L[e]=L[e])});const C=i({},e=>$.safeJson(localStorage.getItem(e)),(e,t)=>{const n=localStorage.getItem(e),s=void 0===t?void 0:JSON.stringify(t);n===s||null===n&&void 0===s||(void 0===t?localStorage.removeItem(e):localStorage.setItem(e,s))}),k=i({exitBlocks:0});globalThis.Hash=L,globalThis.LocalStorage=C,globalThis.State=k;const P={NewState:i,Component:u,$:l,$$:d,RefreshState:x,SetTranslator:v,_scanTree:w,_unbindTree:j,Util:$,Hash:L,LocalStorage:C,State:k};if("undefined"!=typeof window&&(window.ApigoState=P),"undefined"!=typeof document){const e=()=>{u._initPending();const e=document.documentElement;e.hasAttribute("$data-bs-theme")||e.hasAttribute("data-bs-theme")||e.setAttribute("$data-bs-theme","LocalStorage.darkMode?'dark':'light'"),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)}e.$=l,e.$$=d,e.Component=u,e.Hash=L,e.LocalStorage=C,e.NewState=i,e.RefreshState=x,e.SetTranslator=v,e.State=k,e.Util=$,e._scanTree=w,e._unbindTree=j,Object.defineProperty(e,Symbol.toStringTag,{value:"Module"})}); +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).ApigoState=e.ApigoState||{})}(this,function(e){"use strict";var t,n;const s={clone:globalThis.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:s=>(e+=s,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 s=(new Date).getTime(),a=s-e;return e=s,t+=a,n++,a},avg:()=>t/n}}},a=(e,t)=>t?e.querySelector(t):document.querySelector(e),o=(e,t)=>t?e.querySelectorAll(t):document.querySelectorAll(e);globalThis.Util=s,globalThis.$=a,globalThis.$$=o;let r=null,i=null;const l=e=>r=e,c=e=>i=e,d=new Set;function h(e={},t=null,n=null){const s={},a=new Map,o=new Map,l=(e,t)=>(o.has(e)||o.set(e,new Set),t?o.get(e).add(t):o.get(e).clear(),()=>o.get(e).delete(t)),c=(e,t)=>{o.has(e)&&o.set(e,new Set),o.get(e).delete(t)},h=t||(e=>s[e]),u=n||((e,t)=>s[e]=t);return Object.assign(s,e),new Proxy(s,{get:(e,t)=>"__watch"===t?l:"__unwatch"===t?c:"__isProxy"===t||(r&&(a.has(t)||a.set(t,new Set),a.get(t).add(r),r.node._states||(r.node._states=new Set),r.node._states.add(a)),h(t)),set(e,t,n){if(h(t)!==n&&u(t,n),o.has(t)&&o.get(t).forEach(s=>{const a=s(n);void 0!==a&&(n=a,e[t]=n)}),o.has(null)&&o.get(null).forEach(e=>e(n)),a.has(t)){const e=a.get(t);for(const t of e)t.node.isConnected?i!==t.node&&d.forEach(e=>e(t)):e.delete(t)}return!0}})}globalThis.NewState=h;let u=new URLSearchParams("undefined"!=typeof globalThis&&(null==(n=null==(t=globalThis.location)?void 0:t.hash)?void 0:n.substring(1))||"");const f=h({},e=>s.safeJson(u.get(e)),(e,t)=>{const n=u.get(e),s=void 0===t?void 0:JSON.stringify(t);n===s||null===n&&void 0===s||(void 0===t?u.delete(e):u.set(e,s),globalThis.location.hash="#"+u.toString())});"undefined"!=typeof globalThis&&globalThis.addEventListener("hashchange",()=>{var e;const t=new URLSearchParams((null==(e=globalThis.location.hash)?void 0:e.substring(1))||""),n=new Set([...u.keys(),...t.keys()]);u=t,n.forEach(e=>f[e]=f[e])});const b=h({},e=>s.safeJson(localStorage.getItem(e)),(e,t)=>{const n=localStorage.getItem(e),s=void 0===t?void 0:JSON.stringify(t);n===s||null===n&&void 0===s||(void 0===t?localStorage.removeItem(e):localStorage.setItem(e,s))}),m=h({exitBlocks:0});globalThis.Hash=f,globalThis.LocalStorage=b,globalThis.State=m;let p=!1;const g=e=>{p=e},_=new Map;function y(e,t,n,s){const a={...s||{},...t||{}},o=Object.keys(a),r=Object.values(a),i=e+o.join(",");try{let t=_.get(i);return t||(t=new Function("Hash","LocalStorage","State",...o,e),_.set(i,t)),t.apply(n,[globalThis.Hash,globalThis.LocalStorage,globalThis.State,...r])}catch(a){return p||console.error(a,s,[e,s,t,n]),null}}function A(e,t,n,s){return e.includes("${")?y("return `"+e+"`",t,n,s):y("return "+e,t,n,s)}const v=new Map,E=[],T={getTemplate:e=>document.querySelector(`template[component="${e.toUpperCase()}"]`),register:(e,t,n=null,...s)=>{v.set(e.toUpperCase(),t),"loading"!==document.readyState?T._addTemplate(e,n,s):E.push([e,n,s])},exists:e=>v.has(e.toUpperCase()),getSetupFunction:e=>v.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:()=>{E.forEach(([e,t,n])=>T._addTemplate(e,t,n)),E.length=0}};function N(e,t,n,s={}){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 a="TEMPLATE"===t.tagName?t.content:t,o="TEMPLATE"===e.tagName?e.content.childNodes:e.childNodes;Array.from(o).forEach(e=>a.appendChild(e)),e.tagName&&T.exists(e.tagName)&&O(e.tagName,t,n,s)}function O(e,t,n,s={}){if(s[e])return;s[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 a=T.getSetupFunction(e),r={};Array.from(t.childNodes).forEach(e=>{e.nodeType===Node.ELEMENT_NODE&&e.hasAttribute("slot")&&(r[e.getAttribute("slot")]=e,e.removeAttribute("slot"))}),t.innerHTML="",t.state=h(t.state||{});const i=T.getTemplate(e);if(i){const e=i.content.cloneNode(!0);if(e.childNodes.length){const a=e.children[0];a&&N(a,t,n,s),o(t,"[slot-id]").forEach(e=>{const t=e.getAttribute("slot-id");r[t]&&(e.removeAttribute("slot-id"),e.innerHTML="",N(r[t],e,n,s))})}}a&&a(t)}let S=(e,t)=>e&&"string"==typeof e?e.replace(/\{(.+?)\}/g,(e,n)=>t.hasOwnProperty(n)?t[n]:e):e;const j=e=>S=e,x=e=>e&&"string"==typeof e&&e.includes("{#")?e.replace(/\{#(.+?)#\}/g,(e,t)=>{const n=t.split("||").map(e=>e.trim()),s={};if(n.length>1){const e=n[0].match(/\{(.+?)\}/g);e&&e.forEach((e,t)=>s[e.substring(1,e.length-1)]=n[t+1]||"")}return S(n[0],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)}}var $;function M(e){e._renderedNodes&&e._renderedNodes.forEach(e=>e.forEach(e=>{e.remove(),e._renderedNodes&&M(e)}))}function w(e){const t=e.node;if(!t.isConnected&&"TEMPLATE"!==t.tagName)return;l(e);let n=e.exp?e.tpl?A(e.tpl,{thisNode:t},t._thisObj||t,t._ref||null):null:e.tpl;if(2===e.exp&&"string"==typeof n)try{n=A(n,{thisNode:t},t._thisObj||t,t._ref||null)}catch(e){}if(l(null),e.prop){const s=e.prop;let a=t;for(let e=0;e{t.parentNode.insertBefore(e,t),e._ref={...t._ref},e._thisObj=t._thisObj}),t._renderedNodes=[t._children]):(M(t),t._renderedNodes=[]);else if("each"===s)if(n&&"object"==typeof n){const e=t.getAttribute("as")||"item",s=t.getAttribute("index")||"index",a=t.getAttribute("key");let o,r;if(n instanceof Map)o=Array.from(n.keys()),r=e=>n.get(e);else if("function"==typeof n[Symbol.iterator]){const e=Array.isArray(n)?n:Array.from(n);o=new Array(e.length);for(let t=0;te[t]}else o=Object.keys(n),r=e=>n[e];t._keyedNodes||(t._keyedNodes=new Map);const i=new Map,l=[];o.forEach((n,o)=>{const c=r(n),d=a?c&&"object"==typeof c?c[a]:c:n,h=null==d||i.has(d)?`st_key_${o}`:d;let u=t._keyedNodes.get(h);u?(t._keyedNodes.delete(h),u.forEach(t=>{t._ref[s]=n,t._ref[e]=c,L(t)})):(u=[],t._children.forEach(a=>{const o=a.cloneNode(!0);o._ref={...t._ref,[s]:n,[e]:c},o._thisObj=t._thisObj,t.parentNode.insertBefore(o,t),u.push(o)})),i.set(h,u),l.push(u)}),t._keyedNodes.forEach(e=>e.forEach(e=>{M(e),e.remove()})),t._keyedNodes=i,t._renderedNodes=l}else M(t),t._renderedNodes=[];else if("bind"===s){if(["INPUT","SELECT","TEXTAREA"].includes(t.tagName)&&!t.hasAttribute("autocomplete")&&t.setAttribute("autocomplete","off"),"checkbox"===t.type){"on"===t.value||n||(y(`${e.tpl} = []`,{thisNode:t},t._thisObj||t,t._ref||{}),n=[]),t._checkboxMultiMode=n instanceof Array;const s=n instanceof Array?n.includes(t.value):!!n;t.checked!==s&&(t.checked=s)}else"radio"===t.type?t.checked!==(t.value===String(n??""))&&(t.checked=t.value===String(n??"")):"value"in t&&"file"!==t.type?Promise.resolve().then(()=>{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(s)&&(n=!!n),"boolean"==typeof n?n?t.setAttribute(s,""):t.removeAttribute(s):void 0!==n&&("string"!=typeof n&&(n=JSON.stringify(n)),"text"===s?t.textContent=n??"":"html"===s?t.innerHTML=n??"":"IMG"===t.tagName&&"src"===s&&n.includes(".svg")?t.setAttribute("_src",n??""):t.setAttribute(s,n??""))}}function C(e){e.node._bindings||(e.node._bindings=[]),e.node._bindings.push({attr:e.attr,prop:e.prop,tpl:e.tpl,exp:e.exp}),w(e)}$=e=>w(e),d.add($);const L=(e,t={})=>{if(3===e.nodeType){if(e._stTranslated)return;const t=x(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=x(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 n=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(t=>{n.setAttribute(t.name,t.value),e.removeAttribute(t.name)}),e.parentNode.insertBefore(n,e),n.content.appendChild(e),n._ref=e._ref,void L(n,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)),s=n[n.length-1];t.setAttribute(s.name,s.value),e.removeAttribute(s.name),"$each"!==s.name&&"st-each"!==s.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),function(e,t){if(e._bindings)return e._states=new Set,e._bindings.forEach(t=>w({node:e,...t})),void(e._hasOnUpdate&&e.dispatchEvent(new Event("update",{bubbles:!1})));T.exists(e.tagName)&&!e._componentInitialized&&(Array.from(e.attributes).forEach(n=>{var s;if(n.name.startsWith("$.")){const a=n.name.slice(2);let o=x(n.value);t.thisObj&&o.includes("this.")&&(o=o.replace(/\bthis\./g,"this.parent."));const r=A(o,{thisNode:e},{parent:t.thisObj||e},e._ref||{});let i=e;const l=a.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=[]));let n=[];"TEMPLATE"===e.tagName?["$if","$each","st-if","st-each"].forEach(t=>e.hasAttribute(t)&&n.push(e.getAttributeNode(t))):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!==t.thisObj&&(e._thisObj.parent=t.thisObj),e._thisObj||(e._thisObj=t.thisObj||null),e._ref||(e._ref=t.extendVars||{}),e._states=new Set,n.forEach(n=>{let s=0;n.name.startsWith("$$")||n.name.startsWith("st-st-")?s=2:(n.name.startsWith("$")||n.name.startsWith("st-"))&&(s=1);const a=2===s?n.name.startsWith("$$")?n.name.slice(2):n.name.slice(6):1===s?n.name.startsWith("$")?n.name.slice(1):n.name.slice(3):n.name;let o=n.value;if(e.removeAttribute(n.name),a.startsWith("."))C({node:e,prop:a.split("."),tpl:o,exp:s});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=>y(o,{event:n,thisNode:e,...n.detail||{}},t.thisObj||e,e._ref||{}))}else"bind"===a?e.addEventListener(["textarea","text","password"].includes(e.type||"text")||e.isContentEditable?"input":"change",n=>{let s=e.isContentEditable?n.target.innerHTML:"checkbox"===e.type?n.target.checked:n.target.files||n.target.value||n.detail;c(e),g(!0),"checkbox"===e.type&&e._checkboxMultiMode?y(`!!checked ? (!${o}.includes(val) && ${o}.push(val)) : (index = ${o}.indexOf(val), index > -1 && ${o}.splice(index, 1))`,{val:e.value,checked:s,thisNode:e},t.thisObj||e,e._ref||{}):y(`${o} = val`,{val:s,thisNode:e},t.thisObj||e,e._ref||{}),g(!1),c(null)}):"text"!==a||o||(o=e.textContent,e.textContent=""),o&&(o=x(o),C({node:e,attr:a,tpl:o,exp:s}))}),(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});[...e.childNodes||[]].forEach(n=>L(n,{thisObj:t.thisObj,extendVars:{...e._ref}}))},k=e=>{1===e.nodeType&&(e._hasOnUnload&&e.dispatchEvent(new Event("unload",{bubbles:!1})),e._states&&e._states.forEach(t=>{for(const[n,s]of t)for(const t of s)t.node===e&&s.delete(t)}),e.childNodes&&e.childNodes.forEach(e=>k(e)))};if(globalThis.Component=T,globalThis.SetTranslator=j,globalThis.__unsafeRefreshState=L,"undefined"!=typeof document){const e=()=>{globalThis.Component&&globalThis.Component._initPending&&globalThis.Component._initPending();const e=document.documentElement;e.hasAttribute("$data-bs-theme")||e.hasAttribute("data-bs-theme")||e.setAttribute("$data-bs-theme","LocalStorage.darkMode?'dark':'light'"),new MutationObserver(e=>{e.forEach(e=>{e.addedNodes.forEach(e=>{e.isConnected&&L(e)}),e.removedNodes.forEach(e=>k(e))})}).observe(document.documentElement,{childList:!0,subtree:!0}),L(document.documentElement)};"loading"!==document.readyState?e():document.addEventListener("DOMContentLoaded",e,!0)}const P=L;e.$=a,e.$$=o,e.Component=T,e.Hash=f,e.LocalStorage=b,e.NewState=h,e.SetTranslator=j,e.State=m,e.Util=s,e.__unsafeRefreshState=P,Object.defineProperty(e,Symbol.toStringTag,{value:"Module"})}); diff --git a/dist/state.min.mjs b/dist/state.min.mjs deleted file mode 100644 index 5899f9a..0000000 --- a/dist/state.min.mjs +++ /dev/null @@ -1 +0,0 @@ -var e;let t=null,n=null;const s=e=>t=e,a=e=>n=e,r=new Set;function o(e={},s=null,a=null){const o={},i=new Map,l=new Map,d=(e,t)=>(l.has(e)||l.set(e,new Set),t?l.get(e).add(t):l.get(e).clear(),()=>l.get(e).delete(t)),c=(e,t)=>{l.has(e)&&l.set(e,new Set),l.get(e).delete(t)},h=s||(e=>o[e]),u=a||((e,t)=>o[e]=t);return Object.assign(o,e),new Proxy(o,{get:(e,n)=>"__watch"===n?d:"__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,s){if(h(t)!==s&&u(t,s),l.has(t)&&l.get(t).forEach(n=>{const a=n(s);void 0!==a&&(s=a,e[t]=s)}),l.has(null)&&l.get(null).forEach(e=>e(s)),i.has(t)){const e=i.get(t);for(const t of e)t.node.isConnected?n!==t.node&&r.forEach(e=>e(t)):e.delete(t)}return!0}})}const i=(e,t)=>t?e.querySelector(t):document.querySelector(e),l=(e,t)=>t?e.querySelectorAll(t):document.querySelectorAll(e),d=new Map,c=[],h={getTemplate:e=>document.querySelector(`template[component="${e.toUpperCase()}"]`),register:(e,t,n=null,...s)=>{console.log("Component.register:",e.toUpperCase()),d.set(e.toUpperCase(),t),"loading"!==document.readyState?h._addTemplate(e,n,s):c.push([e,n,s])},exists:e=>d.has(e.toUpperCase()),getSetupFunction:e=>d.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,s={}){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),Array.from(e.childNodes).forEach(e=>t.appendChild(e)),e.tagName&&h.exists(e.tagName)&&f(e.tagName,t,n,s)}function f(e,t,n,s={}){if(s[e])return;s[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 a=h.getSetupFunction(e),r={};Array.from(t.childNodes).forEach(e=>{e.nodeType===Node.ELEMENT_NODE&&e.hasAttribute("slot")&&(r[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 a=e.children[0];a&&u(a,t,n,s),l(t,"[slot-id]").forEach(e=>{const t=e.getAttribute("slot-id");r[t]&&(e.removeAttribute("slot-id"),e.innerHTML="",u(r[t],e,n,s))})}}a&&a(t)}let b=!1;function m(e){b=e}const p=new Map;function g(e,t,n,s){const a={...s||{},...t||{}},r=Object.keys(a),o=Object.values(a),i=e+r.join(",");try{let t=p.get(i);return t||(t=new Function("Hash","LocalStorage","State",...r,e),p.set(i,t)),t.apply(n,[globalThis.Hash,globalThis.LocalStorage,globalThis.State,...o])}catch(a){return b||console.error(a,s,[e,s,t,n]),null}}function _(e,t,n,s){return e.includes("${")?g("return `"+e+"`",t,n,s):g("return "+e,t,n,s)}let y=(e,t)=>e&&"string"==typeof e?e.replace(/\{(.+?)\}/g,(e,n)=>t.hasOwnProperty(n)?t[n]:e):e;const A=e=>y=e,v=e=>e&&"string"==typeof e&&e.includes("{#")?e.replace(/\{#(.+?)#\}/g,(e,t)=>{const n=t.split("||").map(e=>e.trim()),s={};if(n.length>1){const e=n[0].match(/\{(.+?)\}/g);e&&e.forEach((e,t)=>s[e.substring(1,e.length-1)]=n[t+1]||"")}return y(n[0],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)}}var E;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;s(e);let n=e.exp?e.tpl?_(e.tpl,{thisNode:t},t._thisObj||t,t._ref||null):null:e.tpl;if(2===e.exp&&"string"==typeof n)try{n=_(n,{thisNode:t},t._thisObj||t,t._ref||null)}catch(e){}if(s(null),e.prop){const s=e.prop;let a=t;for(let e=0;e{t.parentNode.insertBefore(e,t),e._ref={...t._ref},e._thisObj=t._thisObj}),t._renderedNodes=[t._children]):(N(t),t._renderedNodes=[]);else if("each"===s)if(n&&"object"==typeof n){const e=t.getAttribute("as")||"item",s=t.getAttribute("index")||"index",a=t.getAttribute("key");let r,o;if(n instanceof Map)r=Array.from(n.keys()),o=e=>n.get(e);else if("function"==typeof n[Symbol.iterator]){const e=Array.isArray(n)?n:Array.from(n);r=new Array(e.length);for(let t=0;te[t]}else r=Object.keys(n),o=e=>n[e];t._keyedNodes||(t._keyedNodes=new Map);const i=new Map,l=[];r.forEach((n,r)=>{const d=o(n),c=a?d&&"object"==typeof d?d[a]:d:n,h=null==c||i.has(c)?`st_key_${r}`:c;let u=t._keyedNodes.get(h);u?(t._keyedNodes.delete(h),u.forEach(t=>{t._ref[s]=n,t._ref[e]=d,S(t)})):(u=[],t._children.forEach(a=>{const r=a.cloneNode(!0);r._ref={...t._ref,[s]:n,[e]:d},r._thisObj=t._thisObj,t.parentNode.insertBefore(r,t),u.push(r)})),i.set(h,u),l.push(u)}),t._keyedNodes.forEach(e=>e.forEach(e=>{N(e),e.remove()})),t._keyedNodes=i,t._renderedNodes=l}else N(t),t._renderedNodes=[];else if("bind"===s){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 s=n instanceof Array?n.includes(t.value):!!n;t.checked!==s&&(t.checked=s)}else"radio"===t.type?t.checked!==(t.value===String(n??""))&&(t.checked=t.value===String(n??"")):"value"in t&&"file"!==t.type?Promise.resolve().then(()=>{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(s)&&(n=!!n),"boolean"==typeof n?n?t.setAttribute(s,""):t.removeAttribute(s):void 0!==n&&("string"!=typeof n&&(n=JSON.stringify(n)),"text"===s?t.textContent=n??"":"html"===s?t.innerHTML=n??"":"IMG"===t.tagName&&"src"===s&&n.includes(".svg")?t.setAttribute("_src",n??""):t.setAttribute(s,n??""))}}E=e=>O(e),r.add(E);const w=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)},S=(e,t={})=>{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=v(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 n=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(t=>{n.setAttribute(t.name,t.value),e.removeAttribute(t.name)}),e.parentNode.insertBefore(n,e),n.content.appendChild(e),n._ref=e._ref,void S(n,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)),s=n[n.length-1];t.setAttribute(s.name,s.value),e.removeAttribute(s.name),"$each"!==s.name&&"st-each"!==s.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)=>{if(e._bindings)return e._states=new Set,e._bindings.forEach(t=>O({node:e,...t})),void(e._hasOnUpdate&&e.dispatchEvent(new Event("update",{bubbles:!1})));h.exists(e.tagName)&&!e._componentInitialized&&(Array.from(e.attributes).forEach(n=>{var s;if(n.name.startsWith("$.")){const a=n.name.slice(2);let r=v(n.value);r.includes("this.")&&(r=r.replace(/\bthis\./g,"this.parent."));const o=_(r,{thisNode:e},{parent:t.thisObj||e},e._ref||{});let i=e;const l=a.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=[]));let n=[];"TEMPLATE"===e.tagName?["$if","$each","st-if","st-each"].forEach(t=>e.hasAttribute(t)&&n.push(e.getAttributeNode(t))):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||{}),e._states=new Set,n.forEach(n=>{let s=0;n.name.startsWith("$$")||n.name.startsWith("st-st-")?s=2:(n.name.startsWith("$")||n.name.startsWith("st-"))&&(s=1);const r=2===s?n.name.startsWith("$$")?n.name.slice(2):n.name.slice(6):1===s?n.name.startsWith("$")?n.name.slice(1):n.name.slice(3):n.name;let o=n.value;if(e.removeAttribute(n.name),r.startsWith("."))w({node:e,prop:r.split("."),tpl:o,exp:s});else if(r.startsWith("on")){const n=r.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"===r?e.addEventListener(["textarea","text","password"].includes(e.type||"text")||e.isContentEditable?"input":"change",n=>{let s=e.isContentEditable?n.target.innerHTML:"checkbox"===e.type?n.target.checked:n.target.files||n.target.value||n.detail;a(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:s,thisNode:e},t.thisObj||e,e._ref||{}):g(`${o} = val`,{val:s,thisNode:e},t.thisObj||e,e._ref||{}),m(!1),a(null)}):"text"!==r||o||(o=e.textContent,e.textContent=""),o&&(o=v(o),w({node:e,attr:r,tpl:o,exp:s}))}),(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});[...e.childNodes||[]].forEach(n=>S(n,{thisObj:t.thisObj,extendVars:{...e._ref}}))},T=e=>{1===e.nodeType&&(e._hasOnUnload&&e.dispatchEvent(new Event("unload",{bubbles:!1})),e._states&&e._states.forEach(t=>{for(const[n,s]of t)for(const t of s)t.node===e&&s.delete(t)}),e.childNodes&&e.childNodes.forEach(e=>T(e)))},j=S,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:s=>(e+=s,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 s=(new Date).getTime(),a=s-e;return e=s,t+=a,n++,a},avg:()=>t/n}}};globalThis.Util=x;let $=new URLSearchParams((null==(e=window.location.hash)?void 0:e.substring(1))||"");const M=o({},e=>x.safeJson($.get(e)),(e,t)=>{const n=$.get(e),s=void 0===t?void 0:JSON.stringify(t);n===s||null===n&&void 0===s||(void 0===t?$.delete(e):$.set(e,s),window.location.hash="#"+$.toString())});"undefined"!=typeof window&&window.addEventListener("hashchange",()=>{var e;const t=new URLSearchParams((null==(e=window.location.hash)?void 0:e.substring(1))||""),n=new Set([...$.keys(),...t.keys()]);$=t,n.forEach(e=>M[e]=M[e])});const L=o({},e=>x.safeJson(localStorage.getItem(e)),(e,t)=>{const n=localStorage.getItem(e),s=void 0===t?void 0:JSON.stringify(t);n===s||null===n&&void 0===s||(void 0===t?localStorage.removeItem(e):localStorage.setItem(e,s))}),C=o({exitBlocks:0});globalThis.Hash=M,globalThis.LocalStorage=L,globalThis.State=C;const k={NewState:o,Component:h,$:i,$$:l,RefreshState:j,SetTranslator:A,_scanTree:S,_unbindTree:T,Util:x,Hash:M,LocalStorage:L,State:C};if("undefined"!=typeof window&&(window.ApigoState=k),"undefined"!=typeof document){const e=()=>{h._initPending();const e=document.documentElement;e.hasAttribute("$data-bs-theme")||e.hasAttribute("data-bs-theme")||e.setAttribute("$data-bs-theme","LocalStorage.darkMode?'dark':'light'"),new MutationObserver(e=>{e.forEach(e=>{e.addedNodes.forEach(e=>{e.isConnected&&S(e)}),e.removedNodes.forEach(e=>T(e))})}).observe(document.documentElement,{childList:!0,subtree:!0}),S(document.documentElement)};"loading"!==document.readyState?e():document.addEventListener("DOMContentLoaded",e,!0)}export{i as $,l as $$,h as Component,M as Hash,L as LocalStorage,o as NewState,j as RefreshState,A as SetTranslator,C as State,x as Util,S as _scanTree,T as _unbindTree}; diff --git a/dist/state.mjs b/dist/state.mjs deleted file mode 100644 index 1b0d500..0000000 --- a/dist/state.mjs +++ /dev/null @@ -1,659 +0,0 @@ -var _a; -let __activeBinding = null; -let __noWriteBack = null; -const _setActiveBinding = (val) => __activeBinding = val; -const _setNoWriteBack = (val) => __noWriteBack = val; -const _notifiers = /* @__PURE__ */ new Set(); -const _onNotifyUpdate = (fn) => _notifiers.add(fn); -function NewState(defaults = {}, getter = null, setter = null) { - const _defaults = {}; - const _stateMappings = /* @__PURE__ */ new Map(); - const _watchers = /* @__PURE__ */ new Map(); - const _watchFunc = (k, cb) => { - if (!_watchers.has(k)) _watchers.set(k, /* @__PURE__ */ new Set()); - !cb ? _watchers.get(k).clear() : _watchers.get(k).add(cb); - return () => _watchers.get(k).delete(cb); - }; - const _unwatchFunc = (k, cb) => { - if (_watchers.has(k)) _watchers.set(k, /* @__PURE__ */ new Set()); - _watchers.get(k).delete(cb); - }; - const __getter = getter || ((k) => _defaults[k]); - const __setter = setter || ((k, v) => _defaults[k] = v); - Object.assign(_defaults, defaults); - return new Proxy(_defaults, { - get(target, key) { - if (key === "__watch") return _watchFunc; - if (key === "__unwatch") return _unwatchFunc; - if (key === "__isProxy") return true; - if (__activeBinding) { - if (!_stateMappings.has(key)) _stateMappings.set(key, /* @__PURE__ */ new Set()); - _stateMappings.get(key).add(__activeBinding); - if (!__activeBinding.node._states) __activeBinding.node._states = /* @__PURE__ */ new Set(); - __activeBinding.node._states.add(_stateMappings); - } - return __getter(key); - }, - set(target, key, value) { - if (__getter(key) !== value) { - __setter(key, value); - } - if (_watchers.has(key)) { - _watchers.get(key).forEach((cb) => { - const r = cb(value); - if (r !== void 0) { - value = r; - target[key] = value; - } - }); - } - if (_watchers.has(null)) { - _watchers.get(null).forEach((cb) => cb(value)); - } - if (_stateMappings.has(key)) { - const bindings = _stateMappings.get(key); - for (const binding of bindings) { - if (!binding.node.isConnected) { - bindings.delete(binding); - continue; - } - if (__noWriteBack !== binding.node) { - _notifiers.forEach((fn) => fn(binding)); - } - } - } - return true; - } - }); -} -const $ = (a, b) => b ? a.querySelector(b) : document.querySelector(a); -const $$ = (a, b) => b ? a.querySelectorAll(b) : document.querySelectorAll(a); -const _components = /* @__PURE__ */ new Map(); -const _pendingTemplates = []; -const Component = { - getTemplate: (name) => document.querySelector(`template[component="${name.toUpperCase()}"]`), - register: (name, setupFunc, templateNode = null, ...globalNodes) => { - console.log("Component.register:", name.toUpperCase()); - _components.set(name.toUpperCase(), setupFunc); - if (document.readyState !== "loading") Component._addTemplate(name, templateNode, globalNodes); - else _pendingTemplates.push([name, templateNode, globalNodes]); - }, - exists: (name) => _components.has(name.toUpperCase()), - getSetupFunction: (name) => _components.get(name.toUpperCase()), - _addTemplate: (name, templateNode, globalNodes) => { - if (templateNode) { - const template = document.createElement("TEMPLATE"); - template.setAttribute("component", name.toUpperCase()); - template.content.appendChild(templateNode); - document.body.appendChild(template); - } - if (globalNodes) globalNodes.forEach((node) => document.body.appendChild(node)); - }, - _initPending: () => { - _pendingTemplates.forEach(([name, templateNode, globalNodes]) => Component._addTemplate(name, templateNode, globalNodes)); - _pendingTemplates.length = 0; - } -}; -function _mergeNode(from, to, scanObj, exists = {}) { - if (from.attributes) { - Array.from(from.attributes).forEach((attr) => { - if (attr.name === "class") return; - if (attr.name === "style") { - if (to.hasAttribute("style")) to.setAttribute("style", `${attr.value}; ${to.getAttribute("style")}`); - else to.setAttribute("style", attr.value); - } else if (!to.hasAttribute(attr.name)) { - to.setAttribute(attr.name, attr.value); - } - }); - } - to.classList.add(...from.classList); - Array.from(from.childNodes).forEach((child) => to.appendChild(child)); - if (from.tagName && Component.exists(from.tagName)) _makeComponent(from.tagName, to, scanObj, exists); -} -function _makeComponent(name, node, scanObj, exists = {}) { - if (exists[name]) return; - exists[name] = true; - if (scanObj.thisObj) { - Array.from(node.attributes).forEach((attr) => { - if ((attr.name.startsWith("$") || attr.name.startsWith("st-")) && attr.value.includes("this.")) { - attr.value = attr.value.replace(/\bthis\./g, "this.parent."); - } - }); - } - const componentFunc = Component.getSetupFunction(name); - const slots = {}; - Array.from(node.childNodes).forEach((child) => { - if (child.nodeType === Node.ELEMENT_NODE && child.hasAttribute("slot")) { - slots[child.getAttribute("slot")] = child; - child.removeAttribute("slot"); - } - }); - node.innerHTML = ""; - node.state = NewState(node.state || {}); - const template = Component.getTemplate(name); - if (template) { - const tplnode = template.content.cloneNode(true); - if (tplnode.childNodes.length) { - const rootNode = tplnode.children[0]; - if (rootNode) _mergeNode(rootNode, node, scanObj, exists); - $$(node, "[slot-id]").forEach((placeholder) => { - const slotName = placeholder.getAttribute("slot-id"); - if (slots[slotName]) { - placeholder.removeAttribute("slot-id"); - placeholder.innerHTML = ""; - _mergeNode(slots[slotName], placeholder, scanObj, exists); - } - }); - } - } - if (componentFunc) componentFunc(node); -} -let _disableRunCodeError = false; -function setDisableRunCodeError(value) { - _disableRunCodeError = value; -} -const _fnCache = /* @__PURE__ */ new Map(); -function _runCode(code, vars, thisObj, extendVars) { - const allVars = { ...extendVars || {}, ...vars || {} }; - const argKeys = Object.keys(allVars); - const argValues = Object.values(allVars); - const cacheKey = code + argKeys.join(","); - try { - let fn = _fnCache.get(cacheKey); - if (!fn) { - fn = new Function("Hash", "LocalStorage", "State", ...argKeys, code); - _fnCache.set(cacheKey, fn); - } - return fn.apply(thisObj, [globalThis.Hash, globalThis.LocalStorage, globalThis.State, ...argValues]); - } catch (e) { - if (!_disableRunCodeError) console.error(e, extendVars, [code, extendVars, vars, thisObj]); - return null; - } -} -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, args) => { - if (!text || typeof text !== "string") return text; - return text.replace(/\{(.+?)\}/g, (match, key) => args.hasOwnProperty(key) ? args[key] : match); -}; -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); - }; - } -} -_onNotifyUpdate((binding) => _updateBinding(binding)); -function _clearRenderedNodes(node) { - if (node._renderedNodes) node._renderedNodes.forEach((nodes) => nodes.forEach((child) => { - child.remove(); - if (child._renderedNodes) _clearRenderedNodes(child); - })); -} -function _updateBinding(binding) { - const node = binding.node; - if (!node.isConnected && node.tagName !== "TEMPLATE") return; - _setActiveBinding(binding); - let result = binding.exp ? binding.tpl ? _returnCode(binding.tpl, { thisNode: node }, node._thisObj || node, node._ref || null) : null : binding.tpl; - if (binding.exp === 2 && typeof result === "string") { - try { - result = _returnCode(result, { thisNode: node }, node._thisObj || node, node._ref || null); - } catch (e) { - } - } - _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 { - if (o[lk] !== result) 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) => { - node.parentNode.insertBefore(child, node); - child._ref = { ...node._ref }; - child._thisObj = node._thisObj; - }); - 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"; - const keyName = node.getAttribute("key"); - 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]; - } - if (!node._keyedNodes) node._keyedNodes = /* @__PURE__ */ new Map(); - const newKeyedNodes = /* @__PURE__ */ new Map(); - const currentRenderedNodes = []; - keys.forEach((k, i) => { - const item = getVal(k); - const rawKey = keyName ? item && typeof item === "object" ? item[keyName] : item : k; - const keyVal = rawKey === void 0 || rawKey === null || newKeyedNodes.has(rawKey) ? `st_key_${i}` : 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); - }); - } else { - existingNodes = []; - node._children.forEach((child) => { - const cloned = child.cloneNode(true); - cloned._ref = { ...node._ref, [indexName]: k, [asName]: item }; - cloned._thisObj = node._thisObj; - node.parentNode.insertBefore(cloned, node); - existingNodes.push(cloned); - }); - } - newKeyedNodes.set(keyVal, existingNodes); - currentRenderedNodes.push(existingNodes); - }); - node._keyedNodes.forEach((nodes) => nodes.forEach((child) => { - _clearRenderedNodes(child); - child.remove(); - })); - node._keyedNodes = newKeyedNodes; - node._renderedNodes = currentRenderedNodes; - } 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") { - 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 !== void 0) { - 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 ?? ""); - } - } - } -} -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); -}; -const _parseNode = (node, scanObj) => { - if (node._bindings) { - node._states = /* @__PURE__ */ new Set(); - node._bindings.forEach((b) => _updateBinding({ node, ...b })); - if (node._hasOnUpdate) node.dispatchEvent(new Event("update", { bubbles: false })); - return; - } - if (Component.exists(node.tagName) && !node._componentInitialized) { - Array.from(node.attributes).forEach((attr) => { - var _a2; - 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[_a2 = prop[i]] ?? (o[_a2] = {}); - } - 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 = []; - } - 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 = /* @__PURE__ */ new Set(); - attrs.forEach((attr) => { - let exp = 0; - if (attr.name.startsWith("$$") || attr.name.startsWith("st-st-")) exp = 2; - else if (attr.name.startsWith("$") || attr.name.startsWith("st-")) exp = 1; - const realAttrName = exp === 2 ? attr.name.startsWith("$$") ? attr.name.slice(2) : attr.name.slice(6) : exp === 1 ? attr.name.startsWith("$") ? attr.name.slice(1) : attr.name.slice(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(["textarea", "text", "password"].includes(node.type || "text") || node.isContentEditable ? "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; -}; -const _scanTree = (node, scanObj = {}) => { - 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; - _scanTree(template, scanObj); - 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((attr2) => ["$if", "$each", "st-if", "st-each"].includes(attr2.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((attr2) => ["as", "index"].includes(attr2.name)).forEach((attr2) => { - template.setAttribute(attr2.name, attr2.value); - node.removeAttribute(attr2.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 !== void 0) scanObj.thisObj = node._thisObj || null; - else { - let curr = node; - while (curr && curr._thisObj === void 0) curr = curr.parentNode; - scanObj.thisObj = curr ? curr._thisObj : null; - } - if (node._ref === void 0) { - let curr = node; - while (curr && curr._ref === void 0) curr = curr.parentNode; - node._ref = curr ? { ...curr._ref } : {}; - } - if (scanObj.extendVars) Object.assign(node._ref, scanObj.extendVars); - _parseNode(node, { ...scanObj }); - const nodes = [...node.childNodes || []]; - nodes.forEach((child) => _scanTree(child, { thisObj: scanObj.thisObj, extendVars: { ...node._ref } })); -}; -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)); -}; -const _unsafeRefreshState = _scanTree; -const Util = { - clone: window.structuredClone || ((obj) => JSON.parse(JSON.stringify(obj))), - base64: (str) => btoa(String.fromCharCode(...new TextEncoder().encode(str))), - unbase64: (str) => new TextDecoder().decode(Uint8Array.from(atob(str), (c) => c.charCodeAt(0))), - urlbase64: (str) => Util.base64(str).replace(/[+/=]/g, (m) => ({ "+": "-", "/": "", "=": "" })[m]), - unurlbase64: (str) => Util.unbase64(str.replace(/[-_.]/g, (m) => ({ "-": "+", "_": "/", ".": "=" })[m]).padEnd(Math.ceil(str.length / 4) * 4, "=")), - safeJson: (str) => { - try { - return JSON.parse(str); - } catch { - return null; - } - }, - updateDefaults: (obj, defaults) => { - for (const k in defaults) if (obj[k] === void 0) obj[k] = defaults[k]; - }, - copyFunction: (toObj, fromObj, ...funcNames) => { - funcNames.forEach((name) => toObj[name] = fromObj[name].bind(fromObj)); - }, - getFunctionBody: (fn) => { - const code = fn.toString(); - return code.slice(code.indexOf("{") + 1, code.lastIndexOf("}")).trim(); - }, - makeDom: (html) => { - if (html.includes(">\n")) html = html.replace(/>\s+<").trim(); - const node = document.createElement("div"); - node.innerHTML = html; - return node.children[0]; - }, - newAvg: () => { - let total = 0, count = 0, avg = 0; - return { - add: (v) => { - total += v; - count++; - return avg = total / count; - }, - get: () => avg, - clear: () => { - total = 0, count = 0, avg = 0; - } - }; - }, - newTimeCount: () => { - let startTime = 0, total = 0, count = 0; - return { - start: () => startTime = (/* @__PURE__ */ new Date()).getTime(), - end: () => { - const endTime = (/* @__PURE__ */ new Date()).getTime(); - const left = endTime - startTime; - startTime = endTime; - total += left; - count++; - return left; - }, - avg: () => total / count - }; - } -}; -globalThis.Util = Util; -let _hashParams = new URLSearchParams(((_a = window.location.hash) == null ? void 0 : _a.substring(1)) || ""); -const Hash = NewState({}, (k) => Util.safeJson(_hashParams.get(k)), (k, v) => { - const oldStr = _hashParams.get(k); - const newStr = v === void 0 ? void 0 : JSON.stringify(v); - if (oldStr === newStr || oldStr === null && newStr === void 0) return; - v === void 0 ? _hashParams.delete(k) : _hashParams.set(k, newStr); - window.location.hash = "#" + _hashParams.toString(); -}); -if (typeof window !== "undefined") { - window.addEventListener("hashchange", () => { - var _a2; - const newParams = new URLSearchParams(((_a2 = window.location.hash) == null ? void 0 : _a2.substring(1)) || ""); - const keys = /* @__PURE__ */ new Set([..._hashParams.keys(), ...newParams.keys()]); - _hashParams = newParams; - keys.forEach((k) => Hash[k] = Hash[k]); - }); -} -const LocalStorage = NewState({}, (k) => Util.safeJson(localStorage.getItem(k)), (k, v) => { - const oldStr = localStorage.getItem(k); - const newStr = v === void 0 ? void 0 : JSON.stringify(v); - if (oldStr === newStr || oldStr === null && newStr === void 0) return; - v === void 0 ? localStorage.removeItem(k) : localStorage.setItem(k, newStr); -}); -const State = NewState({ - exitBlocks: 0 -}); -globalThis.Hash = Hash; -globalThis.LocalStorage = LocalStorage; -globalThis.State = State; -const ApigoState = { - NewState, - Component, - $, - $$, - RefreshState: _unsafeRefreshState, - SetTranslator, - _scanTree, - _unbindTree, - Util, - Hash, - LocalStorage, - State -}; -if (typeof window !== "undefined") { - window.ApigoState = ApigoState; -} -if (typeof document !== "undefined") { - const init = () => { - Component._initPending(); - const htmlNode = document.documentElement; - if (!htmlNode.hasAttribute("$data-bs-theme") && !htmlNode.hasAttribute("data-bs-theme")) { - htmlNode.setAttribute("$data-bs-theme", "LocalStorage.darkMode?'dark':'light'"); - } - new MutationObserver((mutations) => { - mutations.forEach((mutation) => { - mutation.addedNodes.forEach((newNode) => { - if (newNode.isConnected) _scanTree(newNode); - }); - mutation.removedNodes.forEach((oldNode) => _unbindTree(oldNode)); - }); - }).observe(document.documentElement, { childList: true, subtree: true }); - _scanTree(document.documentElement); - }; - if (document.readyState !== "loading") init(); - else document.addEventListener("DOMContentLoaded", init, true); -} -export { - $, - $$, - Component, - Hash, - LocalStorage, - NewState, - _unsafeRefreshState as RefreshState, - SetTranslator, - State, - Util, - _scanTree, - _unbindTree -}; diff --git a/package-lock.json b/package-lock.json index 1cdf614..905bdde 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@apigo.cc/state", - "version": "1.0.12", + "version": "1.0.18", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@apigo.cc/state", - "version": "1.0.12", + "version": "1.0.18", "devDependencies": { "@playwright/test": "^1.40.0", "@rollup/plugin-terser": "^1.0.0", diff --git a/package.json b/package.json index 86d0ee0..4f11441 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@apigo.cc/state", - "version": "1.0.17", + "version": "1.0.18", "type": "module", "main": "dist/state.js", "module": "dist/state.js", diff --git a/src/component.js b/src/component.js deleted file mode 100644 index 2ea49fb..0000000 --- a/src/component.js +++ /dev/null @@ -1,87 +0,0 @@ -// src/component.js -import { NewState } from './observer.js'; -import { $, $$ } from './dom-utils.js'; - -const _components = new Map(); -const _pendingTemplates = []; - -export const Component = { - getTemplate: name => document.querySelector(`template[component="${name.toUpperCase()}"]`), - register: (name, setupFunc, templateNode = null, ...globalNodes) => { - console.log('Component.register:', name.toUpperCase()); - _components.set(name.toUpperCase(), setupFunc); - if (document.readyState !== 'loading') Component._addTemplate(name, templateNode, globalNodes); - else _pendingTemplates.push([name, templateNode, globalNodes]); - }, - exists: (name) => _components.has(name.toUpperCase()), - getSetupFunction: (name) => _components.get(name.toUpperCase()), - _addTemplate: (name, templateNode, globalNodes) => { - if (templateNode) { - const template = document.createElement('TEMPLATE'); - template.setAttribute('component', name.toUpperCase()); - template.content.appendChild(templateNode); - document.body.appendChild(template); - } - if (globalNodes) globalNodes.forEach(node => document.body.appendChild(node)); - }, - _initPending: () => { - _pendingTemplates.forEach(([name, templateNode, globalNodes]) => Component._addTemplate(name, templateNode, globalNodes)); - _pendingTemplates.length = 0; - } -}; - -export function _mergeNode(from, to, scanObj, exists = {}) { - if (from.attributes) { - Array.from(from.attributes).forEach(attr => { - if (attr.name === 'class') return; - if (attr.name === 'style') { - if (to.hasAttribute('style')) to.setAttribute('style', `${attr.value}; ${to.getAttribute('style')}`); - else to.setAttribute('style', attr.value); - } else if (!to.hasAttribute(attr.name)) { - to.setAttribute(attr.name, attr.value); - } - }); - } - to.classList.add(...from.classList); - Array.from(from.childNodes).forEach(child => to.appendChild(child)); - if (from.tagName && Component.exists(from.tagName)) _makeComponent(from.tagName, to, scanObj, exists); -} - -export function _makeComponent(name, node, scanObj, exists = {}) { - if (exists[name]) return; - exists[name] = true; - if (scanObj.thisObj) { - Array.from(node.attributes).forEach(attr => { - if ((attr.name.startsWith('$') || attr.name.startsWith('st-')) && attr.value.includes('this.')) { - attr.value = attr.value.replace(/\bthis\./g, 'this.parent.'); - } - }); - } - const componentFunc = Component.getSetupFunction(name); - const slots = {}; - Array.from(node.childNodes).forEach(child => { - if (child.nodeType === Node.ELEMENT_NODE && child.hasAttribute('slot')) { - slots[child.getAttribute('slot')] = child; - child.removeAttribute('slot'); - } - }); - node.innerHTML = ''; - node.state = NewState(node.state || {}); - const template = Component.getTemplate(name); - if (template) { - const tplnode = template.content.cloneNode(true); - if (tplnode.childNodes.length) { - const rootNode = tplnode.children[0]; - if (rootNode) _mergeNode(rootNode, node, scanObj, exists); - $$(node, '[slot-id]').forEach(placeholder => { - const slotName = placeholder.getAttribute('slot-id'); - if (slots[slotName]) { - placeholder.removeAttribute('slot-id'); - placeholder.innerHTML = ''; - _mergeNode(slots[slotName], placeholder, scanObj, exists); - } - }); - } - } - if (componentFunc) componentFunc(node); -} diff --git a/src/core.js b/src/core.js index da92fa3..d7f36fc 100644 --- a/src/core.js +++ b/src/core.js @@ -1,31 +1,69 @@ -// src/core.js -let _disableRunCodeError = false; +/** + * Core Execution Module + * Provides _runCode, _returnCode, and Global States. + */ -export function setDisableRunCodeError(value) { - _disableRunCodeError = value; +import { NewState } from './observer.js'; +import { Util } from './utils.js'; + +let _hashParams = new URLSearchParams(typeof globalThis !== 'undefined' ? (globalThis.location?.hash?.substring(1) || '') : ''); +export const Hash = NewState({}, k => Util.safeJson(_hashParams.get(k)), (k, v) => { + const oldStr = _hashParams.get(k) + const newStr = v === undefined ? undefined : JSON.stringify(v) + if (oldStr === newStr || (oldStr === null && newStr === undefined)) return + v === undefined ? _hashParams.delete(k) : _hashParams.set(k, newStr) + globalThis.location.hash = '#' + _hashParams.toString() +}); + +if (typeof globalThis !== 'undefined') { + globalThis.addEventListener('hashchange', () => { + const newParams = new URLSearchParams(globalThis.location.hash?.substring(1) || '') + const keys = new Set([..._hashParams.keys(), ...newParams.keys()]) + _hashParams = newParams + keys.forEach(k => Hash[k] = Hash[k]) // 触发更新 + }) } +export const LocalStorage = NewState({}, k => Util.safeJson(localStorage.getItem(k)), (k, v) => { + const oldStr = localStorage.getItem(k) + const newStr = v === undefined ? undefined : JSON.stringify(v) + if (oldStr === newStr || (oldStr === null && newStr === undefined)) return + v === undefined ? localStorage.removeItem(k) : localStorage.setItem(k, newStr) +}) + +export const State = NewState({ + exitBlocks: 0 +}); + +// 全局挂载 +globalThis.Hash = Hash; +globalThis.LocalStorage = LocalStorage; +globalThis.State = State; + +let _disableRunCodeError = false; +export const setDisableRunCodeError = (value) => { _disableRunCodeError = value; }; + const _fnCache = new Map(); export function _runCode(code, vars, thisObj, extendVars) { - const allVars = { ...(extendVars || {}), ... (vars || {}) }; - const argKeys = Object.keys(allVars); - const argValues = Object.values(allVars); - const cacheKey = code + argKeys.join(','); - try { - let fn = _fnCache.get(cacheKey); - if (!fn) { - fn = new Function('Hash', 'LocalStorage', 'State', ...argKeys, code); - _fnCache.set(cacheKey, fn); - } - return fn.apply(thisObj, [globalThis.Hash, globalThis.LocalStorage, globalThis.State, ...argValues]); - } catch (e) { - if (!_disableRunCodeError) console.error(e, extendVars, [code, extendVars, vars, thisObj]); - return null; - } + const allVars = { ...(extendVars || {}), ...(vars || {}) }; + const argKeys = Object.keys(allVars); + const argValues = Object.values(allVars); + const cacheKey = code + argKeys.join(','); + try { + let fn = _fnCache.get(cacheKey); + if (!fn) { + fn = new Function('Hash', 'LocalStorage', 'State', ...argKeys, code); + _fnCache.set(cacheKey, fn); + } + return fn.apply(thisObj, [globalThis.Hash, globalThis.LocalStorage, globalThis.State, ...argValues]); + } 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-utils.js b/src/dom-utils.js deleted file mode 100644 index 59c7c04..0000000 --- a/src/dom-utils.js +++ /dev/null @@ -1,3 +0,0 @@ -// src/dom-utils.js -export const $ = (a, b) => b ? a.querySelector(b) : document.querySelector(a); -export const $$ = (a, b) => b ? a.querySelectorAll(b) : document.querySelectorAll(a); diff --git a/src/dom.badbak.for2exp.js b/src/dom.badbak.for2exp.js deleted file mode 100644 index 935ce69..0000000 --- a/src/dom.badbak.for2exp.js +++ /dev/null @@ -1,366 +0,0 @@ -// 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); - }); -}; - -const _isExpression = (name) => name.startsWith('$') || name.startsWith('st-'); -const _getExpressionLevel = (name) => { - if (name.startsWith('$$') || name.startsWith('st-st-')) return 2; - if (name.startsWith('$') || name.startsWith('st-')) return 1; - return 0; -}; -const _getRealAttrName = (name, level) => { - if (level === 2) return name.startsWith('$$') ? name.slice(2) : name.slice(6); - if (level === 1) { - if (name.startsWith('$')) return name.slice(1); - if (name.startsWith('st-')) return name.slice(3); - } - return name; -}; - -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); - let prefix = name.startsWith('$$') ? 'st-st-' : 'st-'; - return originalSetAttribute.call(this, prefix + name.slice(name.startsWith('$$') ? 2 : 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); - let result = binding.exp ? (binding.tpl ? _returnCode(binding.tpl, { thisNode: node }, node._thisObj || node, node._ref || null) : null) : binding.tpl; - - // Level 2 evaluation: Evaluate the result string as code again - if (binding.exp === 2 && typeof result === 'string') { - try { - result = _returnCode(result, { thisNode: node }, node._thisObj || node, { ...node._ref }); - } catch (e) { } - } - 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 === !!node._lastIfResult) return; - node._lastIfResult = !!result; - 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 { - _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]; - } - - if (!node._renderedNodes) node._renderedNodes = []; - keys.forEach((k, i) => { - const item = getVal(k); - if (i < node._renderedNodes.length) { - node._renderedNodes[i].forEach(child => { - child._ref[indexName] = k; - child._ref[asName] = item; - _scanTree(child, { thisObj: node._thisObj, extendVars: child._ref }); - }); - } else { - const newNodes = []; - 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.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') { - 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) => { - if (node._bindings) { - node._states = new Set(); - node._bindings.forEach(b => _updateBinding({ node, ...b })); - if (node._hasOnUpdate) node.dispatchEvent(new Event('update', { bubbles: false })); - if (node.tagName !== 'TEMPLATE') return; - } - - 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') { - if (!node._children) node._children = [...node.content.childNodes]; - if (!node._renderedNodes) node._renderedNodes = []; - } - - if (node._bindings && node.tagName !== 'TEMPLATE') return; - - let attrs = Array.from(node.attributes).filter(a => _isExpression(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 = _getExpressionLevel(attr.name); - const realAttrName = exp ? _getRealAttrName(attr.name, exp) : attr.name; - if (exp && (node.tagName === 'TEMPLATE' || !['$if', '$each', 'st-if', 'st-each', '$$if', '$$each', 'st-st-if', 'st-st-each'].includes(attr.name))) node.removeAttribute(attr.name); - if (realAttrName.startsWith('.')) _initBinding({ node, prop: realAttrName.split('.').filter(Boolean), tpl: attr.value, exp: 0 }); - 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(attr.value, { 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 ? (!${attr.value}.includes(val) && ${attr.value}.push(val)) : (index = ${attr.value}.indexOf(val), index > -1 && ${attr.value}.splice(index, 1))`, { val: node.value, checked: newVal, thisNode: node }, scanObj.thisObj || node, node._ref || {}); - else _runCode(attr.value + ' = val', { val: newVal, thisNode: node }, scanObj.thisObj || node, node._ref || {}); - setDisableRunCodeError(false); setNoWriteBack(null); - }); - _initBinding({ node, attr: realAttrName, tpl: attr.value, exp }); - } else if (realAttrName === 'text' && !attr.value) { _initBinding({ node, attr: realAttrName, tpl: node.textContent, exp: 1 }); node.textContent = ''; } - else if (attr.value) { - let tpl = _translate(attr.value); - _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 (!_isExpression(attr.name) && !attr.name.startsWith('.')) { - const translated = _translate(attr.value); - if (translated !== attr.value) attr.value = translated; - } - }); - node._stTranslated = true; - } - - const triggerAttrs = ['$if', '$each', 'st-if', 'st-each', '$$if', '$$each', 'st-st-if', 'st-st-each']; - if (node.tagName !== 'TEMPLATE' && triggerAttrs.some(t => node.hasAttribute(t))) { - const template = document.createElement('TEMPLATE'); - const managed = Array.from(node.attributes).filter(attr => triggerAttrs.includes(attr.name) || ['as', 'index'].includes(attr.name)); - managed.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('$$if') || node.hasAttribute('st-st-if')) && (node.hasAttribute('$each') || node.hasAttribute('st-each') || node.hasAttribute('$$each') || node.hasAttribute('st-st-each'))) { - const template = document.createElement('TEMPLATE'); - const attrs = Array.from(node.attributes).filter(attr => triggerAttrs.includes(attr.name)); - const attr = attrs[attrs.length - 1]; - template.setAttribute(attr.name, attr.value); - node.removeAttribute(attr.name); - if (attr.name.includes('each')) { - Array.from(node.attributes).filter(a => ['as', 'index'].includes(a.name)).forEach(a => { template.setAttribute(a.name, a.value); node.removeAttribute(a.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); - - if (!scanObj.noBind) _parseNode(node, scanObj); - - const nodes = [...(node.childNodes || []), ...(node.tagName === 'TEMPLATE' ? Array.from(node.content.childNodes).map(c => (c._stNoBind = true, c)) : [])]; - const nextScanObj = { thisObj: scanObj.thisObj, extendVars: { ...node._ref }, noBind: scanObj.noBind }; - nodes.forEach(child => { - if (!child._stManaged) { - const wasNoBind = nextScanObj.noBind; - if (child._stNoBind) { nextScanObj.noBind = true; delete child._stNoBind; } - _scanTree(child, nextScanObj); - nextScanObj.noBind = wasNoBind; - } - }); -}; - -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 ___unsafeRefreshState = _scanTree; diff --git a/src/dom.js b/src/dom.js deleted file mode 100644 index ecc10e8..0000000 --- a/src/dom.js +++ /dev/null @@ -1,353 +0,0 @@ -// 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); - let result = binding.exp ? (binding.tpl ? _returnCode(binding.tpl, { thisNode: node }, node._thisObj || node, node._ref || null) : null) : binding.tpl; - - if (binding.exp === 2 && typeof result === 'string') { - try { result = _returnCode(result, { thisNode: node }, node._thisObj || node, node._ref || null); } catch (e) { } - } - _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 { if (o[lk] !== result) 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 => { - node.parentNode.insertBefore(child, node); - child._ref = { ...node._ref }; - child._thisObj = node._thisObj; - }); - 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'; - const keyName = node.getAttribute('key'); - 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]; - } - - if (!node._keyedNodes) node._keyedNodes = new Map(); - const newKeyedNodes = new Map(); - const currentRenderedNodes = []; - - keys.forEach((k, i) => { - const item = getVal(k); - const rawKey = keyName ? (item && typeof item === 'object' ? item[keyName] : item) : k; - const keyVal = (rawKey === undefined || rawKey === null || newKeyedNodes.has(rawKey)) ? `st_key_${i}` : 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); - }); - } else { - existingNodes = []; - node._children.forEach(child => { - const cloned = child.cloneNode(true); - cloned._ref = { ...node._ref, [indexName]: k, [asName]: item }; - cloned._thisObj = node._thisObj; - node.parentNode.insertBefore(cloned, node); - existingNodes.push(cloned); - }); - } - newKeyedNodes.set(keyVal, existingNodes); - currentRenderedNodes.push(existingNodes); - }); - - node._keyedNodes.forEach(nodes => nodes.forEach(child => { _clearRenderedNodes(child); child.remove(); })); - node._keyedNodes = newKeyedNodes; - node._renderedNodes = currentRenderedNodes; - } 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') { - 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 ?? ''); - } - } - } -} - -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) => { - if (node._bindings) { - node._states = new Set(); - node._bindings.forEach(b => _updateBinding({ node, ...b })); - if (node._hasOnUpdate) node.dispatchEvent(new Event('update', { bubbles: false })); - return; - } - - 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 = []; - } - - 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 => { - let exp = 0; - if (attr.name.startsWith('$$') || attr.name.startsWith('st-st-')) exp = 2; - else if (attr.name.startsWith('$') || attr.name.startsWith('st-')) exp = 1; - - const realAttrName = exp === 2 ? (attr.name.startsWith('$$') ? attr.name.slice(2) : attr.name.slice(6)) : (exp === 1 ? (attr.name.startsWith('$') ? attr.name.slice(1) : attr.name.slice(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(['textarea', 'text', 'password'].includes(node.type || 'text') || node.isContentEditable ? '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 !== 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 和 $each 指令创建模版节点 - 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; - // 修正原始版本可能存在的漏扫:转换后立即对新生成的 TEMPLATE 发起递归扫描 - _scanTree(template, scanObj); - 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 || [])]; - 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._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 _unsafeRefreshState = _scanTree; diff --git a/src/dom.js.keyed.bak b/src/dom.js.keyed.bak deleted file mode 100644 index 04e5de2..0000000 --- a/src/dom.js.keyed.bak +++ /dev/null @@ -1,352 +0,0 @@ -// 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 _unsafeRefreshState = _scanTree; diff --git a/src/engine.js b/src/engine.js new file mode 100644 index 0000000..ff03d8b --- /dev/null +++ b/src/engine.js @@ -0,0 +1,451 @@ +/** + * Engine Module + * Component Management and DOM Engine. + */ + +import { Util, $, $$ } from './utils.js'; +import { NewState, _setActiveBinding, _setNoWriteBack, _onNotifyUpdate } from './observer.js'; +import { _runCode, _returnCode, setDisableRunCodeError } from './core.js'; + +// --- Component Logic --- +const _components = new Map(); +const _pendingTemplates = []; + +export const Component = { + getTemplate: name => document.querySelector(`template[component="${name.toUpperCase()}"]`), + register: (name, setupFunc, templateNode = null, ...globalNodes) => { + _components.set(name.toUpperCase(), setupFunc); + if (document.readyState !== 'loading') Component._addTemplate(name, templateNode, globalNodes); + else _pendingTemplates.push([name, templateNode, globalNodes]); + }, + exists: (name) => _components.has(name.toUpperCase()), + getSetupFunction: (name) => _components.get(name.toUpperCase()), + _addTemplate: (name, templateNode, globalNodes) => { + if (templateNode) { + const template = document.createElement('TEMPLATE'); + template.setAttribute('component', name.toUpperCase()); + template.content.appendChild(templateNode); + document.body.appendChild(template); + } + if (globalNodes) globalNodes.forEach(node => document.body.appendChild(node)); + }, + _initPending: () => { + _pendingTemplates.forEach(([name, templateNode, globalNodes]) => Component._addTemplate(name, templateNode, globalNodes)); + _pendingTemplates.length = 0; + } +}; + +export function _mergeNode(from, to, scanObj, exists = {}) { + if (from.attributes) { + Array.from(from.attributes).forEach(attr => { + if (attr.name === 'class') return; + if (attr.name === 'style') { + if (to.hasAttribute('style')) to.setAttribute('style', `${attr.value}; ${to.getAttribute('style')}`); + else to.setAttribute('style', attr.value); + } else if (!to.hasAttribute(attr.name)) { + to.setAttribute(attr.name, attr.value); + } + }); + } + to.classList.add(...from.classList); + + const target = to.tagName === 'TEMPLATE' ? to.content : to; + const sourceNodes = from.tagName === 'TEMPLATE' ? from.content.childNodes : from.childNodes; + Array.from(sourceNodes).forEach(child => target.appendChild(child)); + + if (from.tagName && Component.exists(from.tagName)) _makeComponent(from.tagName, to, scanObj, exists); +} + +export function _makeComponent(name, node, scanObj, exists = {}) { + if (exists[name]) return; + exists[name] = true; + if (scanObj.thisObj) { + Array.from(node.attributes).forEach(attr => { + if ((attr.name.startsWith('$') || attr.name.startsWith('st-')) && attr.value.includes('this.')) { + attr.value = attr.value.replace(/\bthis\./g, 'this.parent.'); + } + }); + } + const componentFunc = Component.getSetupFunction(name); + const slots = {}; + Array.from(node.childNodes).forEach(child => { + if (child.nodeType === Node.ELEMENT_NODE && child.hasAttribute('slot')) { + slots[child.getAttribute('slot')] = child; + child.removeAttribute('slot'); + } + }); + node.innerHTML = ''; + node.state = NewState(node.state || {}); + const template = Component.getTemplate(name); + if (template) { + const tplnode = template.content.cloneNode(true); + if (tplnode.childNodes.length) { + const rootNode = tplnode.children[0]; + if (rootNode) _mergeNode(rootNode, node, scanObj, exists); + $$(node, '[slot-id]').forEach(placeholder => { + const slotName = placeholder.getAttribute('slot-id'); + if (slots[slotName]) { + placeholder.removeAttribute('slot-id'); + placeholder.innerHTML = ''; + _mergeNode(slots[slotName], placeholder, scanObj, exists); + } + }); + } + } + if (componentFunc) componentFunc(node); +} + +// --- DOM Engine Logic --- +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); + }; + } +} + +_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); + let result = binding.exp ? (binding.tpl ? _returnCode(binding.tpl, { thisNode: node }, node._thisObj || node, node._ref || null) : null) : binding.tpl; + + if (binding.exp === 2 && typeof result === 'string') { + try { result = _returnCode(result, { thisNode: node }, node._thisObj || node, node._ref || null); } catch (e) { } + } + _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 { if (o[lk] !== result) 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 => { + node.parentNode.insertBefore(child, node); + child._ref = { ...node._ref }; + child._thisObj = node._thisObj; + }); + 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'; + const keyName = node.getAttribute('key'); + 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]; + } + + if (!node._keyedNodes) node._keyedNodes = new Map(); + const newKeyedNodes = new Map(); + const currentRenderedNodes = []; + + keys.forEach((k, i) => { + const item = getVal(k); + const rawKey = keyName ? (item && typeof item === 'object' ? item[keyName] : item) : k; + const keyVal = (rawKey === undefined || rawKey === null || newKeyedNodes.has(rawKey)) ? `st_key_${i}` : 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); + }); + } else { + existingNodes = []; + node._children.forEach(child => { + const cloned = child.cloneNode(true); + cloned._ref = { ...node._ref, [indexName]: k, [asName]: item }; + cloned._thisObj = node._thisObj; + node.parentNode.insertBefore(cloned, node); + existingNodes.push(cloned); + }); + } + newKeyedNodes.set(keyVal, existingNodes); + currentRenderedNodes.push(existingNodes); + }); + + node._keyedNodes.forEach(nodes => nodes.forEach(child => { _clearRenderedNodes(child); child.remove(); })); + node._keyedNodes = newKeyedNodes; + node._renderedNodes = currentRenderedNodes; + } 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') { + 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 ?? ''); + } + } + } +} + +export function _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 function _parseNode(node, scanObj) { + if (node._bindings) { + node._states = new Set(); + node._bindings.forEach(b => _updateBinding({ node, ...b })); + if (node._hasOnUpdate) node.dispatchEvent(new Event('update', { bubbles: false })); + return; + } + + 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 (scanObj.thisObj && 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 = []; + } + + 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 !== 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 => { + let exp = 0; + if (attr.name.startsWith('$$') || attr.name.startsWith('st-st-')) exp = 2; + else if (attr.name.startsWith('$') || attr.name.startsWith('st-')) exp = 1; + + const realAttrName = exp === 2 ? (attr.name.startsWith('$$') ? attr.name.slice(2) : attr.name.slice(6)) : (exp === 1 ? (attr.name.startsWith('$') ? attr.name.slice(1) : attr.name.slice(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(['textarea', 'text', 'password'].includes(node.type || 'text') || node.isContentEditable ? '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; + _scanTree(template, scanObj); + 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 || [])]; + 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._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)); +}; + +// 全局挂载 +globalThis.Component = Component; +globalThis.SetTranslator = SetTranslator; +globalThis.__unsafeRefreshState = _scanTree; diff --git a/src/globals.js b/src/globals.js deleted file mode 100644 index 66494b3..0000000 --- a/src/globals.js +++ /dev/null @@ -1,41 +0,0 @@ -// src/globals.js -import { NewState } from './observer.js'; -import { Util } from './utils.js'; - -// url hash 状态 -let _hashParams = new URLSearchParams(window.location.hash?.substring(1) || '') -export const Hash = NewState({}, k => Util.safeJson(_hashParams.get(k)), (k, v) => { - const oldStr = _hashParams.get(k) - const newStr = v === undefined ? undefined : JSON.stringify(v) - if (oldStr === newStr || (oldStr === null && newStr === undefined)) return - v === undefined ? _hashParams.delete(k) : _hashParams.set(k, newStr) - - // 增量 5: 立即同步 Hash 字符串,确保响应式实时可见 - window.location.hash = '#' + _hashParams.toString() -}) - -if (typeof window !== 'undefined') { - window.addEventListener('hashchange', () => { - const newParams = new URLSearchParams(window.location.hash?.substring(1) || '') - const keys = new Set([..._hashParams.keys(), ...newParams.keys()]) - _hashParams = newParams - keys.forEach(k => Hash[k] = Hash[k]) // 触发更新 - }) -} - -// localstorage 状态 -export const LocalStorage = NewState({}, k => Util.safeJson(localStorage.getItem(k)), (k, v) => { - const oldStr = localStorage.getItem(k) - const newStr = v === undefined ? undefined : JSON.stringify(v) - if (oldStr === newStr || (oldStr === null && newStr === undefined)) return - v === undefined ? localStorage.removeItem(k) : localStorage.setItem(k, newStr) -}) - -// 增量:显式定义 State 单例 -export const State = NewState({ - exitBlocks: 0 -}); - -globalThis.Hash = Hash; -globalThis.LocalStorage = LocalStorage; -globalThis.State = State; diff --git a/src/index.js b/src/index.js index 99f018c..8f9a8d0 100644 --- a/src/index.js +++ b/src/index.js @@ -1,28 +1,19 @@ -// src/index.js -export { NewState } from './observer.js'; -export { Component } from './component.js'; -export { $, $$, _unsafeRefreshState as RefreshState, SetTranslator, _scanTree, _unbindTree } from './dom.js'; -export { Util } from './utils.js'; -export { Hash, LocalStorage, State } from './globals.js'; +import './utils.js'; +import './observer.js'; +import { _unbindTree, _scanTree } from './engine.js'; +import './core.js'; -import { Component } from './component.js'; -import { _scanTree, _unbindTree, $, $$, _unsafeRefreshState, SetTranslator } from './dom.js'; -import { LocalStorage, Hash, State } from './globals.js'; -import { NewState, _onNotifyUpdate, _setActiveBinding } from './observer.js'; -import { Util } from './utils.js'; -import { _runCode, _returnCode } from './core.js'; - -const ApigoState = { - NewState, Component, $, $$, RefreshState: _unsafeRefreshState, SetTranslator, _scanTree, _unbindTree, Util, Hash, LocalStorage, State -}; - -if (typeof window !== 'undefined') { - window.ApigoState = ApigoState; -} +/** + * Entry Point + * Orchestrates initialization and MutationObserver. + */ if (typeof document !== 'undefined') { const init = () => { - Component._initPending(); + if (globalThis.Component && globalThis.Component._initPending) { + globalThis.Component._initPending(); + } + const htmlNode = document.documentElement; if (!htmlNode.hasAttribute('$data-bs-theme') && !htmlNode.hasAttribute('data-bs-theme')) { htmlNode.setAttribute('$data-bs-theme', "LocalStorage.darkMode?'dark':'light'"); @@ -43,3 +34,9 @@ if (typeof document !== 'undefined') { if (document.readyState !== 'loading') init(); else document.addEventListener('DOMContentLoaded', init, true); } + +export { NewState } from './observer.js'; +export { Component, SetTranslator } from './engine.js'; +export { $, $$, Util } from './utils.js'; +export { Hash, LocalStorage, State } from './core.js'; +export const __unsafeRefreshState = _scanTree; diff --git a/src/observer.js b/src/observer.js index 477fb74..2ec3007 100644 --- a/src/observer.js +++ b/src/observer.js @@ -1,4 +1,8 @@ -// src/observer.js +/** + * Observer Module + * Core reactivity logic. + */ + let __activeBinding = null; let __noWriteBack = null; @@ -76,3 +80,6 @@ export function NewState(defaults = {}, getter = null, setter = null) { } }); } + +// 全局挂载 +globalThis.NewState = NewState; diff --git a/src/utils.js b/src/utils.js index e091c72..19296f9 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,6 +1,10 @@ -// src/utils.js +/** + * Utils Module + * Provides Util, $, and $$ helpers. + */ + export const Util = { - clone: window.structuredClone || (obj => JSON.parse(JSON.stringify(obj))), + clone: globalThis.structuredClone || (obj => JSON.parse(JSON.stringify(obj))), base64: str => btoa(String.fromCharCode(...new TextEncoder().encode(str))), unbase64: str => new TextDecoder().decode(Uint8Array.from(atob(str), c => c.charCodeAt(0))), urlbase64: str => Util.base64(str).replace(/[+/=]/g, m => ({ '+': '-', '/': '', '=': '' }[m])), @@ -42,6 +46,12 @@ export const Util = { avg: () => total / count } }, -} +}; +export const $ = (a, b) => b ? a.querySelector(b) : document.querySelector(a); +export const $$ = (a, b) => b ? a.querySelectorAll(b) : document.querySelectorAll(a); + +// 全局挂载 globalThis.Util = Util; +globalThis.$ = $; +globalThis.$$ = $$; diff --git a/vite.config.js b/vite.config.js index cc64563..792520b 100644 --- a/vite.config.js +++ b/vite.config.js @@ -6,30 +6,23 @@ export default defineConfig({ build: { lib: { entry: resolve(__dirname, 'src/index.js'), - name: 'ApigoState', // UMD 内部名称,逻辑主要靠 globalThis 挂载 - formats: ['umd', 'es'] + name: 'ApigoState', + formats: ['umd'] }, rollupOptions: { output: [ { format: 'umd', name: 'ApigoState', - entryFileNames: 'state.js' + entryFileNames: 'state.js', + extend: true }, { format: 'umd', name: 'ApigoState', entryFileNames: 'state.min.js', - plugins: [terser()] - }, - { - format: 'es', - entryFileNames: 'state.mjs' - }, - { - format: 'es', - entryFileNames: 'state.min.mjs', - plugins: [terser()] + plugins: [terser()], + extend: true } ] },